"use client"; import { useEffect, useRef, useState } from "react"; import { loadStripe } from "@stripe/stripe-js/pure"; import type { Stripe } from "@stripe/stripe-js"; import { EmbeddedCheckout, EmbeddedCheckoutProvider } from "@stripe/react-stripe-js"; import { useI18n } from "@/i18n/i18n-provider"; /** * Stripe.js genau einmal je Schluessel laden. "pure" statt des Standard- * Einstiegs: der haengt das Skript bereits beim blossen Import ein, also auf * jeder Seite, die dieses Modul irgendwann mitbringt. So kommt Stripe erst * ins Spiel, wenn wirklich bezahlt wird. */ const stripeByKey = new Map>(); function getStripe(publishableKey: string): Promise { const cached = stripeByKey.get(publishableKey); if (cached) return cached; const pending = loadStripe(publishableKey); stripeByKey.set(publishableKey, pending); // Nach einem Fehlschlag (Netz, Werbeblocker) darf "Erneut versuchen" nicht // auf demselben abgelehnten Versprechen haengen bleiben. pending.catch(() => stripeByKey.delete(publishableKey)); return pending; } type LoadState = { status: "loading" } | { status: "ready"; stripe: Stripe } | { status: "failed" }; export interface EmbeddedPaymentProps { publishableKey: string; clientSecret: string; /** Abbruch-Token der Bestellung (siehe api/checkout/cancel). */ cancelToken: string; /** Beschriftung des Abbruch-Knopfs — je nach Ort "Zurueck" oder "Abbrechen". */ cancelLabel: string; /** Wird erst aufgerufen, nachdem der Server den Bestand freigegeben hat. */ onCancelled: () => void; } /** * Das Stripe-Zahlungsformular als iframe auf der eigenen Seite. * * Kartendaten, PayPal-Anmeldung und 3-D-Secure laufen vollstaendig innerhalb * von Stripes iframe bzw. bei PayPal — dieser Code sieht davon nichts. Ueber * "bezahlt" entscheidet er ebenfalls nicht: Stripe leitet nach jeder Zahlung * auf /checkout/success weiter, und erst dort wird serverseitig nachgefragt. */ export function EmbeddedPayment({ publishableKey, clientSecret, cancelToken, cancelLabel, onCancelled, }: EmbeddedPaymentProps) { const { t } = useI18n(); const [load, setLoad] = useState({ status: "loading" }); const [attempt, setAttempt] = useState(0); const [cancelling, setCancelling] = useState(false); const [cancelError, setCancelError] = useState(false); const headingRef = useRef(null); // Fokus auf die neue Ueberschrift: der Schritt ersetzt das Formular, auf // dem der Fokus eben noch lag. Ohne das stuende er fuer Screenreader im // Nichts, und auf dem Handy bliebe die Seite unten am alten Knopf stehen. useEffect(() => { headingRef.current?.focus(); headingRef.current?.scrollIntoView?.({ block: "start" }); }, []); useEffect(() => { let cancelled = false; getStripe(publishableKey) .then((stripe) => { if (cancelled) return; setLoad(stripe ? { status: "ready", stripe } : { status: "failed" }); }) .catch((error: unknown) => { if (cancelled) return; console.error("[checkout] Stripe.js konnte nicht geladen werden:", error); setLoad({ status: "failed" }); }); return () => { cancelled = true; }; }, [publishableKey, attempt]); async function handleCancel() { setCancelling(true); setCancelError(false); try { const response = await fetch("/api/checkout/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cancelToken }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); onCancelled(); } catch (error: unknown) { // Stehen bleiben statt trotzdem weiterzuschalten: sonst legte der // naechste Versuch eine zweite Bestellung an, waehrend die erste ihren // Bestand noch bis zum Sitzungsablauf festhaelt. console.error("[checkout] Abbruch fehlgeschlagen:", error); setCancelError(true); setCancelling(false); } } return (

{t.checkoutPage.paymentHeading}

{cancelError ? (

{t.checkoutPage.paymentCancelError}

) : null} {/* Mindesthoehe haelt den Platz frei, bis Stripe das iframe gezeichnet hat — sonst springt alles darunter beim Laden nach oben. */}
{load.status === "loading" ? (

{t.checkoutPage.paymentLoading}

) : null} {load.status === "failed" ? (

{t.checkoutPage.paymentLoadError}{" "}

) : null} {load.status === "ready" ? ( ) : null}
); }