import React from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Receipt, ShieldCheck, ArrowRight, ArrowLeft, CreditCard, AlertTriangle, Loader2, } from "lucide-react"; import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"; import { loadStripe } from "@stripe/stripe-js"; import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js"; import { TenantWelcomeLayout, WelcomeStepCard, OnboardingCTAList, useOnboardingProgress, } from ".."; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ADMIN_BILLING_PATH, ADMIN_WELCOME_EVENT_PATH, ADMIN_WELCOME_PACKAGES_PATH } from "../../constants"; import { useTenantPackages } from "../hooks/useTenantPackages"; import { assignFreeTenantPackage, completeTenantPackagePurchase, createTenantPackagePaymentIntent, createTenantPayPalOrder, captureTenantPayPalOrder, } from "../../api"; const stripePublishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ?? ""; const paypalClientId = import.meta.env.VITE_PAYPAL_CLIENT_ID ?? ""; const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : null; type StripeCheckoutProps = { clientSecret: string; packageId: number; onSuccess: () => void; t: ReturnType["t"]; }; type PayPalCheckoutProps = { packageId: number; onSuccess: () => void; t: ReturnType["t"]; currency?: string; }; function useLocaleFormats(locale: string) { const currencyFormatter = React.useMemo( () => new Intl.NumberFormat(locale, { style: "currency", currency: "EUR", minimumFractionDigits: 0, }), [locale] ); const dateFormatter = React.useMemo( () => new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", }), [locale] ); return { currencyFormatter, dateFormatter }; } function humanizeFeature(t: ReturnType["t"], key: string): string { const translationKey = `summary.details.features.${key}`; const translated = t(translationKey); if (translated !== translationKey) { return translated; } return key .split("_") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function StripeCheckoutForm({ clientSecret, packageId, onSuccess, t }: StripeCheckoutProps) { const stripe = useStripe(); const elements = useElements(); const [submitting, setSubmitting] = React.useState(false); const [error, setError] = React.useState(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) { setError(t("summary.stripe.notReady")); return; } setSubmitting(true); setError(null); const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: window.location.href, }, redirect: "if_required", }); if (result.error) { setError(result.error.message ?? t("summary.stripe.genericError")); setSubmitting(false); return; } const paymentIntent = result.paymentIntent; const paymentMethodId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : typeof paymentIntent?.id === "string" ? paymentIntent.id : null; if (!paymentMethodId) { setError(t("summary.stripe.missingPaymentId")); setSubmitting(false); return; } try { await completeTenantPackagePurchase({ packageId, paymentMethodId, }); onSuccess(); } catch (purchaseError) { console.error("[Onboarding] Purchase completion failed", purchaseError); setError( purchaseError instanceof Error ? purchaseError.message : t("summary.stripe.completionFailed") ); setSubmitting(false); } }; return (

{t("summary.stripe.heading")}

{error && ( {t("summary.stripe.errorTitle")} {error} )}

{t("summary.stripe.hint")}

); } function PayPalCheckout({ packageId, onSuccess, t, currency = "EUR" }: PayPalCheckoutProps) { const [status, setStatus] = React.useState<"idle" | "creating" | "capturing" | "error" | "success">("idle"); const [error, setError] = React.useState(null); const handleCreateOrder = React.useCallback(async () => { try { setStatus("creating"); const orderId = await createTenantPayPalOrder(packageId); setStatus("idle"); setError(null); return orderId; } catch (err) { console.error("[Onboarding] PayPal create order failed", err); setStatus("error"); setError( err instanceof Error ? err.message : t("summary.paypal.createFailed") ); throw err; } }, [packageId, t]); const handleApprove = React.useCallback( async (orderId: string) => { try { setStatus("capturing"); await captureTenantPayPalOrder(orderId); setStatus("success"); setError(null); onSuccess(); } catch (err) { console.error("[Onboarding] PayPal capture failed", err); setStatus("error"); setError( err instanceof Error ? err.message : t("summary.paypal.captureFailed") ); throw err; } }, [onSuccess, t] ); return (

{t("summary.paypal.heading")}

{error && ( {t("summary.paypal.errorTitle")} {error} )} handleCreateOrder()} onApprove={async (data) => { if (!data.orderID) { setError(t("summary.paypal.missingOrderId")); setStatus("error"); return; } await handleApprove(data.orderID); }} onError={(err) => { console.error("[Onboarding] PayPal onError", err); setStatus("error"); setError(t("summary.paypal.genericError")); }} onCancel={() => { setStatus("idle"); setError(t("summary.paypal.cancelled")); }} disabled={status === "creating" || status === "capturing"} />

{t("summary.paypal.hint")}

); } export default function WelcomeOrderSummaryPage() { const navigate = useNavigate(); const location = useLocation(); const { progress, markStep } = useOnboardingProgress(); const packagesState = useTenantPackages(); const { t, i18n } = useTranslation("onboarding"); const locale = i18n.language?.startsWith("en") ? "en-GB" : "de-DE"; const { currencyFormatter, dateFormatter } = useLocaleFormats(locale); const packageIdFromState = typeof location.state === "object" ? (location.state as any)?.packageId : undefined; const selectedPackageId = progress.selectedPackage?.id ?? packageIdFromState ?? null; React.useEffect(() => { if (!selectedPackageId && packagesState.status !== "loading") { navigate(ADMIN_WELCOME_PACKAGES_PATH, { replace: true }); } }, [selectedPackageId, packagesState.status, navigate]); React.useEffect(() => { markStep({ lastStep: "summary" }); }, [markStep]); const packageDetails = packagesState.status === "success" && selectedPackageId ? packagesState.catalog.find((pkg) => pkg.id === selectedPackageId) : null; const activePackage = packagesState.status === "success" ? packagesState.purchasedPackages.find((pkg) => pkg.package_id === selectedPackageId) : null; const isSubscription = Boolean(packageDetails?.features?.subscription); const requiresPayment = Boolean(packageDetails && packageDetails.price > 0); const [clientSecret, setClientSecret] = React.useState(null); const [intentStatus, setIntentStatus] = React.useState<"idle" | "loading" | "error" | "ready">("idle"); const [intentError, setIntentError] = React.useState(null); const [freeAssignStatus, setFreeAssignStatus] = React.useState<"idle" | "loading" | "success" | "error">("idle"); const [freeAssignError, setFreeAssignError] = React.useState(null); React.useEffect(() => { if (!requiresPayment || !packageDetails) { setClientSecret(null); setIntentStatus("idle"); setIntentError(null); return; } if (!stripePromise) { setIntentError(t("summary.stripe.missingKey")); setIntentStatus("error"); return; } let cancelled = false; setIntentStatus("loading"); setIntentError(null); createTenantPackagePaymentIntent(packageDetails.id) .then((secret) => { if (cancelled) return; setClientSecret(secret); setIntentStatus("ready"); }) .catch((error) => { console.error("[Onboarding] Payment intent failed", error); if (cancelled) return; setIntentStatus("error"); setIntentError(error instanceof Error ? error.message : t("summary.stripe.intentFailed")); }); return () => { cancelled = true; }; }, [requiresPayment, packageDetails, t]); const priceText = progress.selectedPackage?.priceText ?? (packageDetails && typeof packageDetails.price === "number" ? currencyFormatter.format(packageDetails.price) : null); const detailBadges = React.useMemo(() => { if (!packageDetails) { return [] as string[]; } const badges: string[] = []; if (packageDetails.max_photos) { badges.push(t("summary.details.photos", { count: packageDetails.max_photos })); } if (packageDetails.gallery_days) { badges.push(t("summary.details.galleryDays", { count: packageDetails.gallery_days })); } if (packageDetails.max_guests) { badges.push(t("summary.details.guests", { count: packageDetails.max_guests })); } return badges; }, [packageDetails, t]); const featuresList = React.useMemo(() => { if (!packageDetails) { return [] as string[]; } return Object.entries(packageDetails.features ?? {}) .filter(([, enabled]) => Boolean(enabled)) .map(([feature]) => humanizeFeature(t, feature)); }, [packageDetails, t]); const nextSteps = t("summary.nextSteps", { returnObjects: true }) as string[]; return ( navigate(ADMIN_WELCOME_PACKAGES_PATH)} > {t("summary.footer.back")} } > {packagesState.status === "loading" && (
{t("summary.state.loading")}
)} {packagesState.status === "error" && ( {t("summary.state.errorTitle")} {packagesState.message ?? t("summary.state.errorDescription")} )} {packagesState.status === "success" && !packageDetails && ( {t("summary.state.missingTitle")} {t("summary.state.missingDescription")} )} {packagesState.status === "success" && packageDetails && (

{t(isSubscription ? "summary.details.subscription" : "summary.details.creditPack")}

{packageDetails.name}

{priceText && ( {priceText} )}
{t("summary.details.section.photosTitle")}
{packageDetails.max_photos ? t("summary.details.section.photosValue", { count: packageDetails.max_photos, days: packageDetails.gallery_days ?? t("summary.details.infinity"), }) : t("summary.details.section.photosUnlimited")}
{t("summary.details.section.guestsTitle")}
{packageDetails.max_guests ? t("summary.details.section.guestsValue", { count: packageDetails.max_guests }) : t("summary.details.section.guestsUnlimited")}
{t("summary.details.section.featuresTitle")}
{featuresList.length ? featuresList.join(", ") : t("summary.details.section.featuresNone")}
{t("summary.details.section.statusTitle")}
{activePackage ? t("summary.details.section.statusActive") : t("summary.details.section.statusInactive")}
{detailBadges.length > 0 && (
{detailBadges.map((badge) => ( {badge} ))}
)}
{!activePackage && ( {t("summary.status.pendingTitle")} {t("summary.status.pendingDescription")} )} {packageDetails.price === 0 && (

{t("summary.free.description")}

{freeAssignStatus === "success" ? ( {t("summary.free.successTitle")} {t("summary.free.successDescription")} ) : ( )} {freeAssignStatus === "error" && freeAssignError && ( {t("summary.free.failureTitle")} {freeAssignError} )}
)} {requiresPayment && (

{t("summary.stripe.sectionTitle")}

{intentStatus === "loading" && (
{t("summary.stripe.loading")}
)} {intentStatus === "error" && ( {t("summary.stripe.unavailableTitle")} {intentError ?? t("summary.stripe.unavailableDescription")} )} {intentStatus === "ready" && clientSecret && stripePromise && ( { markStep({ packageSelected: true }); navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true }); }} t={t} /> )}
{paypalClientId ? (

{t("summary.paypal.sectionTitle")}

{ markStep({ packageSelected: true }); navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true }); }} t={t} />
) : ( {t("summary.paypal.notConfiguredTitle")} {t("summary.paypal.notConfiguredDescription")} )}
)}

{t("summary.nextStepsTitle")}

    {nextSteps.map((step, index) => (
  1. {step}
  2. ))}
)}
{ markStep({ lastStep: "event-setup" }); navigate(ADMIN_WELCOME_EVENT_PATH); }, icon: ArrowRight, }, ]} />
); } export { StripeCheckoutForm, PayPalCheckout };