import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { Receipt, ShieldCheck, ArrowRight, ArrowLeft, CreditCard, AlertTriangle, Loader2 } from 'lucide-react'; 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'; import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; 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; }; function StripeCheckoutForm({ clientSecret, packageId, onSuccess }: 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('Zahlungsmodul noch nicht bereit. Bitte lade die Seite neu.'); 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 ?? 'Zahlung fehlgeschlagen. Bitte erneut versuchen.'); 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('Zahlung konnte nicht bestätigt werden (fehlende Zahlungs-ID).'); setSubmitting(false); return; } try { await completeTenantPackagePurchase({ packageId, paymentMethodId, }); onSuccess(); } catch (purchaseError) { console.error('[Onboarding] Purchase completion failed', purchaseError); setError( purchaseError instanceof Error ? purchaseError.message : 'Der Kauf wurde bei uns noch nicht verbucht. Bitte kontaktiere den Support mit deiner Zahlungsbestätigung.' ); setSubmitting(false); } }; return (

Kartenzahlung

{error && ( Zahlung fehlgeschlagen {error} )}

Sicherer Checkout über Stripe. Du erhältst eine Bestätigung, sobald der Kauf verbucht wurde.

); } type PayPalCheckoutProps = { packageId: number; onSuccess: () => void; currency?: string; }; function PayPalCheckout({ packageId, onSuccess, 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 : 'PayPal-Bestellung konnte nicht erstellt werden. Bitte versuche es erneut.' ); throw err; } }, [packageId]); 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 : 'PayPal-Zahlung konnte nicht abgeschlossen werden. Bitte kontaktiere den Support, falls der Betrag bereits abgebucht wurde.' ); throw err; } }, [onSuccess] ); return (

PayPal

{error && ( PayPal-Fehler {error} )} handleCreateOrder()} onApprove={async (data) => { if (!data.orderID) { setError('PayPal hat keine Order-ID geliefert.'); setStatus('error'); return; } await handleApprove(data.orderID); }} onError={(err) => { console.error('[Onboarding] PayPal onError', err); setStatus('error'); setError('PayPal hat ein Problem gemeldet. Bitte versuche es in wenigen Minuten erneut.'); }} onCancel={() => { setStatus('idle'); setError('PayPal-Zahlung wurde abgebrochen.'); }} disabled={status === 'creating' || status === 'capturing'} />

PayPal leitet dich ggf. kurz weiter, um die Zahlung zu bestätigen. Anschließend wirst du automatisch zum Setup zurückgebracht.

); } export default function WelcomeOrderSummaryPage() { const navigate = useNavigate(); const location = useLocation(); const { progress, markStep } = useOnboardingProgress(); const packagesState = useTenantPackages(); 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('Stripe Publishable Key fehlt. Bitte konfiguriere VITE_STRIPE_PUBLISHABLE_KEY.'); 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] Failed to create payment intent', error); if (cancelled) return; setIntentError( error instanceof Error ? error.message : 'PaymentIntent konnte nicht erstellt werden. Bitte später erneut versuchen.' ); setIntentStatus('error'); }); return () => { cancelled = true; }; }, [requiresPayment, packageDetails?.id]); const priceText = progress.selectedPackage?.priceText ?? (packageDetails && typeof packageDetails.price === 'number' ? new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0, }).format(packageDetails.price) : null); return ( navigate(ADMIN_WELCOME_PACKAGES_PATH)} > Zurück zur Paketauswahl } > {packagesState.status === 'loading' && (
Wir prüfen verfügbare Pakete …
)} {packagesState.status === 'error' && ( Paketdaten derzeit nicht verfügbar {packagesState.message} Du kannst das Event trotzdem vorbereiten und später zurückkehren. )} {packagesState.status === 'success' && !packageDetails && ( Keine Paketauswahl gefunden Bitte wähle zuerst ein Paket aus oder aktualisiere die Seite, falls sich die Daten geändert haben. )} {packagesState.status === 'success' && packageDetails && (

{isSubscription ? 'Abo' : 'Credit-Paket'}

{packageDetails.name}

{priceText && ( {priceText} )}
Fotos & Galerie
{packageDetails.max_photos ? `Bis zu ${packageDetails.max_photos} Fotos, Galerie ${packageDetails.gallery_days ?? '∞'} Tage` : 'Unbegrenzte Fotos, flexible Galerie'}
Gäste & Team
{packageDetails.max_guests ? `${packageDetails.max_guests} Gäste inklusive, Co-Hosts frei planbar` : 'Unbegrenzte Gästeliste'}
Highlights
{Object.entries(packageDetails.features ?? {}) .filter(([, enabled]) => enabled) .map(([feature]) => feature.replace(/_/g, ' ')) .join(', ') || 'Standard'}
Status
{activePackage ? 'Bereits gebucht' : 'Noch nicht gebucht'}
{!activePackage && ( Abrechnung steht noch aus Du kannst das Event bereits vorbereiten. Spätestens zur Veröffentlichung benötigst du ein aktives Paket. )} {packageDetails.price === 0 && (

Dieses Paket ist kostenlos. Du kannst es sofort deinem Tenant zuweisen und direkt mit dem Setup weitermachen.

{freeAssignStatus === 'success' ? ( Gratis-Paket aktiviert Deine Credits wurden hinzugefügt. Weiter geht's mit dem Event-Setup. ) : ( )} {freeAssignStatus === 'error' && freeAssignError && ( Aktivierung fehlgeschlagen {freeAssignError} )}
)} {requiresPayment && (

Kartenzahlung (Stripe)

{intentStatus === 'loading' && (
Zahlungsdetails werden geladen …
)} {intentStatus === 'error' && ( Stripe nicht verfügbar {intentError} )} {intentStatus === 'ready' && clientSecret && stripePromise && ( { markStep({ packageSelected: true }); navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true }); }} /> )}
{paypalClientId ? (

PayPal

{ markStep({ packageSelected: true }); navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true }); }} />
) : ( PayPal nicht konfiguriert Hinterlege `VITE_PAYPAL_CLIENT_ID`, damit Gastgeber optional mit PayPal bezahlen können. )}
)}

Nächste Schritte

  1. Optional: Abrechnung abschließen (Stripe/PayPal) im Billing-Bereich.
  2. Event-Setup durchlaufen und Aufgaben, Team & Galerie konfigurieren.
  3. Vor dem Go-Live Credits prüfen und Gäste-Link teilen.
)}
{ markStep({ lastStep: 'event-setup' }); navigate(ADMIN_WELCOME_EVENT_PATH); }, icon: ArrowRight, }, ]} />
); }