import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Elements, CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; import type { Stripe as StripeInstance } from '@stripe/stripe-js'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Loader2 } from 'lucide-react'; type StripePromise = Promise; interface PaymentFormProps { packageId: number; packageName: string; price: number; currency?: string; stripePromise: StripePromise; paypalClientId?: string | null; onSuccess: () => void; } declare global { interface Window { paypal?: any; } } const formatCurrency = (value: number, currency = 'EUR') => new Intl.NumberFormat('de-DE', { style: 'currency', currency, }).format(value); const getCsrfToken = () => (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content ?? ''; async function postJson(url: string, body: unknown, csrfToken: string): Promise { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken, }, body: JSON.stringify(body), }); if (response.status === 204) { return {} as T; } const data = await response.json().catch(() => ({})); if (!response.ok) { const message = (data as { message?: string; error?: string }).message ?? (data as { message?: string; error?: string }).error ?? 'Request failed.'; throw new Error(message); } return data as T; } export default function PaymentForm({ packageId, packageName, price, currency = 'EUR', stripePromise, paypalClientId, onSuccess, }: PaymentFormProps) { const { t } = useTranslation('marketing'); const csrfToken = useMemo(getCsrfToken, []); const [provider, setProvider] = useState<'stripe' | 'paypal'>('stripe'); const [statusMessage, setStatusMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [freeStatus, setFreeStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle'); useEffect(() => { setErrorMessage(null); setStatusMessage(null); }, [provider]); useEffect(() => { if (price === 0 && freeStatus === 'idle') { const assignFree = async () => { try { setFreeStatus('loading'); await postJson<{ status: string }>('/purchase/free', { package_id: packageId }, csrfToken); setFreeStatus('done'); setStatusMessage( t('payment.free_assigned', { defaultValue: 'Kostenloses Paket wurde zugewiesen.', package: packageName, }) ); onSuccess(); } catch (error) { setFreeStatus('error'); setErrorMessage((error as Error).message ?? 'Free package assignment failed.'); } }; assignFree(); } }, [csrfToken, freeStatus, onSuccess, packageId, packageName, price, t]); if (price === 0) { return ( {t('payment.title', { defaultValue: 'Zahlung' })} {freeStatus === 'loading' && (
{t('payment.processing_free', { defaultValue: 'Paket wird freigeschaltet �' })}
)} {statusMessage && ( {statusMessage} )} {errorMessage && ( {errorMessage} )}
); } return ( {t('payment.title', { defaultValue: 'Zahlung' })}

{t('payment.total_due', { defaultValue: 'Gesamtbetrag' })}

{formatCurrency(price, currency)}

{provider === 'stripe' ? ( { setStatusMessage(t('payment.success_stripe', { defaultValue: 'Stripe-Zahlung erfolgreich.' })); onSuccess(); }} onError={(message) => setErrorMessage(message)} /> ) : ( { setStatusMessage(t('payment.success_paypal', { defaultValue: 'PayPal-Zahlung erfolgreich.' })); onSuccess(); }} onError={(message) => setErrorMessage(message)} /> )} {statusMessage && ( {statusMessage} )} {errorMessage && ( {errorMessage} )}
); } interface StripeCardFormProps { packageId: number; csrfToken: string; amountLabel: string; onSuccess: () => void; onError: (message: string) => void; } const StripeCardForm: React.FC = ({ packageId, csrfToken, amountLabel, onSuccess, onError }) => { const { t } = useTranslation('marketing'); const stripe = useStripe(); const elements = useElements(); const [isSubmitting, setIsSubmitting] = useState(false); const [localError, setLocalError] = useState(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) { return; } const cardElement = elements.getElement(CardElement); if (!cardElement) { setLocalError('Card element not found.'); return; } try { setIsSubmitting(true); setLocalError(null); const { client_secret: clientSecret, payment_intent_id: paymentIntentId } = await postJson<{ client_secret: string; payment_intent_id: string; }>('/purchase/stripe/intent', { package_id: packageId }, csrfToken); const confirmation = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: cardElement, }, }); if (confirmation.error) { throw new Error(confirmation.error.message || 'Card confirmation failed.'); } if (confirmation.paymentIntent?.status !== 'succeeded') { throw new Error('Stripe did not confirm the payment.'); } await postJson('/purchase/stripe/complete', { package_id: packageId, payment_intent_id: confirmation.paymentIntent.id || paymentIntentId, }, csrfToken); onSuccess(); } catch (error) { const message = (error as Error).message || 'Stripe payment failed.'; setLocalError(message); onError(message); } finally { setIsSubmitting(false); } }; return (
{localError && ( {localError} )}
); }; interface PayPalSectionProps { packageId: number; amount: number; currency: string; clientId?: string | null; csrfToken: string; onSuccess: () => void; onError: (message: string) => void; } const PayPalSection: React.FC = ({ packageId, amount, currency, clientId, csrfToken, onSuccess, onError, }) => { const { t } = useTranslation('marketing'); const containerRef = useRef(null); const [isSdkReady, setIsSdkReady] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [localError, setLocalError] = useState(null); useEffect(() => { if (!clientId) { const message = t('payment.paypal_missing_key', { defaultValue: 'PayPal ist derzeit nicht konfiguriert.' }); setLocalError(message); onError(message); return; } if (window.paypal) { setIsSdkReady(true); return; } const script = document.createElement('script'); script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}¤cy=${currency}&intent=capture&components=buttons`; script.async = true; script.onload = () => setIsSdkReady(true); script.onerror = () => { const message = t('payment.paypal_sdk_failed', { defaultValue: 'PayPal-SDK konnte nicht geladen werden.' }); setLocalError(message); onError(message); }; document.body.appendChild(script); return () => { script.remove(); }; }, [clientId, currency, onError, t]); useEffect(() => { if (!isSdkReady || !window.paypal || !containerRef.current) { return; } const buttons = window.paypal.Buttons({ style: { layout: 'vertical', color: 'gold', shape: 'rect', }, createOrder: async () => { try { setIsProcessing(true); const { order_id: orderId } = await postJson<{ order_id: string }>('/purchase/paypal/order', { package_id: packageId, }, csrfToken); return orderId; } catch (error) { const message = (error as Error).message || 'PayPal order creation failed.'; setLocalError(message); onError(message); setIsProcessing(false); throw error; } }, onApprove: async (data: { orderID: string }) => { try { await postJson('/purchase/paypal/capture', { order_id: data.orderID, package_id: packageId, }, csrfToken); setIsProcessing(false); setLocalError(null); onSuccess(); } catch (error) { const message = (error as Error).message || 'PayPal capture failed.'; setLocalError(message); onError(message); setIsProcessing(false); } }, onError: (error: Error) => { const message = error?.message || 'PayPal payment failed.'; setLocalError(message); onError(message); setIsProcessing(false); }, }); buttons.render(containerRef.current); return () => { try { buttons.close(); } catch (error) { // ignore close errors } }; }, [csrfToken, isSdkReady, onError, onSuccess, packageId]); return (
{isProcessing && (
{t('payment.processing_paypal', { defaultValue: 'PayPal-Zahlung wird verarbeitet �' })}
)} {localError && ( {localError} )}

{t('payment.paypal_hint', { defaultValue: 'Der Betrag von {{amount}} wird bei PayPal angezeigt.', amount: formatCurrency(amount, currency), })}

); };