feat: Stripe Embedded Checkout statt Weiterleitung zu checkout.stripe.com

Bezahlt wird jetzt direkt auf /checkout: die Checkout-Session laeuft mit
ui_mode "embedded_page", das Zahlungsformular ist ein Stripe-iframe
(PCI SAQ A bleibt). PayPal, Karte, SEPA und Klarna kommen weiterhin
dynamisch aus dem Dashboard.

- Zahlungsschritt ersetzt das Formular; "Zurueck zu deinen Angaben" gibt
  Sitzung und Bestand sofort frei (POST /api/checkout/cancel) und behaelt
  alle Eingaben, Turnstile wird neu geladen
- Rueckkehrseite prueft serverseitig bei Stripe und gleicht die Sitzung mit
  der eigenen Bestellung ab; bei abgebrochener PayPal-Zahlung wird dieselbe
  Sitzung erneut eingebettet
- Erneuter Versuch und Abbruch sind per HttpOnly-Cookie an den bestellenden
  Browser gebunden, die session_id in der URL allein reicht nicht
- Alte /checkout/cancel?ref= Links aendern per GET nichts mehr
- CSP um die Stripe-Hosts fuer Embedded Checkout und 3-D-Secure erweitert
- STRIPE_PUBLISHABLE_KEY zur Laufzeit gelesen (kein NEXT_PUBLIC_), mit
  Preflight-Pruefung auf pk_live und Schutz gegen versehentliche sk_/rk_

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-25 08:01:18 +02:00
1 parent 6ac7cc5d1f
commit 4ee5bafe20
28 files changed
+1272 -179

No files matched your search

+18 -3
View File
@@ -1415,15 +1415,23 @@ export const de: Dictionary = {
save: "Passwort speichern",
},
checkoutSuccessPage: {
pendingEyebrow: "Bestellung vorgemerkt",
pendingEyebrow: "Zahlung in Bearbeitung",
successEyebrow: "Vielen Dank",
pendingTitle: "Deine Bestellung ist eingegangen.",
successTitle: "Deine Bestellung wurde bestätigt.",
orderNumberLabel: "Bestellnummer",
pendingText: "Die Zahlungsabwicklung war kurzfristig nicht erreichbar. Wir melden uns per E-Mail bei dir, um die Zahlung abzuschließen.",
pendingText: "Deine Zahlung wird noch bestätigt (bei SEPA-Lastschrift kann das einige Werktage dauern). Sobald sie eingegangen ist, bekommst du eine Bestätigung per E-Mail.",
successText: "Wir bereiten dein Glas Wabenhain-Honig mit Sorgfalt vor. Sobald deine Zahlung eingegangen ist, senden wir dir eine Bestätigung per E-Mail (bei SEPA-Lastschrift kann das einige Werktage dauern).",
continueShopping: "Weiter stöbern",
trackOrder: "Bestellung verfolgen",
openEyebrow: "Zahlung nicht abgeschlossen",
openTitle: "Das hat leider nicht geklappt.",
openText: "Die Zahlung wurde abgebrochen oder abgelehnt, es wurde nichts abgebucht. Deine Bestellung ist noch für dich reserviert — versuche es einfach erneut, gern auch mit einer anderen Zahlungsart.",
cancelPayment: "Bestellung abbrechen",
notCompletedEyebrow: "Nicht abgeschlossen",
notCompletedTitle: "Diese Bestellung wurde nicht bezahlt.",
notCompletedText: "Die Zahlung ist abgelaufen oder wurde abgebrochen, es wurde nichts abgebucht. Dein Warenkorb ist noch gefüllt, du kannst jederzeit neu bestellen.",
backToCart: "Zurück zum Warenkorb",
},
checkoutCancelPage: {
eyebrow: "Abgebrochen",
@@ -1433,6 +1441,13 @@ export const de: Dictionary = {
backToCart: "Zurück zum Warenkorb",
},
checkoutPage: {
paymentHeading: "Zahlung",
paymentBack: "Zurück zu deinen Angaben",
paymentCancelling: "Einen Moment…",
paymentCancelError: "Das hat gerade nicht geklappt. Bitte versuche es noch einmal.",
paymentLoading: "Sichere Zahlung wird geladen…",
paymentLoadError: "Das Zahlungsformular konnte nicht geladen werden. Ein Werbeblocker kann das verhindern.",
paymentUnavailable: "Die Zahlung ist gerade nicht verfügbar. Bitte versuche es später erneut oder melde dich bei uns.",
eyebrow: "Checkout",
emptyCart: "Dein Warenkorb ist leer",
yourDetails: "Deine Daten",
@@ -1474,7 +1489,7 @@ export const de: Dictionary = {
submitting: "Wird verarbeitet…",
placeOrder: "Kostenpflichtig bestellen",
paymentMethodsNote:
"Zahlungsmethoden: PayPal, Kreditkarte, Apple Pay, Google Pay, SEPA-Lastschrift, Klarna, bereitgestellt über Stripe.",
"Im nächsten Schritt bezahlst du sicher direkt hier auf der Seite: PayPal, Kreditkarte, SEPA-Lastschrift oder Klarna, abgewickelt über Stripe.",
yourOrderHeading: "Deine Bestellung",
shippingWithCount: (count) => `Versand (${count} Stück)`,
shippingPickup: "Versand (Abholung)",