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
@@ -1401,15 +1401,23 @@ export const en: Dictionary = {
save: "Save password",
},
checkoutSuccessPage: {
pendingEyebrow: "Order Received",
pendingEyebrow: "Payment Processing",
successEyebrow: "Thank You",
pendingTitle: "Your order has been received.",
successTitle: "Your order has been confirmed.",
orderNumberLabel: "Order number",
pendingText: "Payment processing was briefly unreachable. We'll email you to complete the payment.",
pendingText: "Your payment is still being confirmed (this can take a few business days for SEPA direct debit). We'll email you as soon as it arrives.",
successText: "We're carefully preparing your jar of Wabenhain honey. As soon as your payment arrives, we'll send you a confirmation by email (this can take a few business days for SEPA direct debit).",
continueShopping: "Keep browsing",
trackOrder: "Track order",
openEyebrow: "Payment Not Completed",
openTitle: "That didn't work, unfortunately.",
openText: "The payment was cancelled or declined and nothing was charged. Your order is still reserved for you — just try again, with another payment method if you like.",
cancelPayment: "Cancel order",
notCompletedEyebrow: "Not Completed",
notCompletedTitle: "This order was not paid.",
notCompletedText: "The payment expired or was cancelled and nothing was charged. Your cart is still filled, so you can order again at any time.",
backToCart: "Back to cart",
},
checkoutCancelPage: {
eyebrow: "Cancelled",
@@ -1419,6 +1427,13 @@ export const en: Dictionary = {
backToCart: "Back to cart",
},
checkoutPage: {
paymentHeading: "Payment",
paymentBack: "Back to your details",
paymentCancelling: "One moment…",
paymentCancelError: "That didn't work just now. Please try again.",
paymentLoading: "Loading secure payment…",
paymentLoadError: "The payment form could not be loaded. An ad blocker can prevent this.",
paymentUnavailable: "Payment is currently unavailable. Please try again later or get in touch with us.",
eyebrow: "Checkout",
emptyCart: "Your cart is empty",
yourDetails: "Your Details",
@@ -1460,7 +1475,7 @@ export const en: Dictionary = {
submitting: "Processing…",
placeOrder: "Place order",
paymentMethodsNote:
"Payment methods: PayPal, credit card, Apple Pay, Google Pay, SEPA direct debit, Klarna, provided via Stripe.",
"In the next step you pay securely right here on this page: PayPal, credit card, SEPA direct debit or Klarna, processed by Stripe.",
yourOrderHeading: "Your Order",
shippingWithCount: (count) => `Shipping (${count} item${count === 1 ? "" : "s"})`,
shippingPickup: "Shipping (pickup)",