Consolidate Kreditkarte/PayPal into one "Online-Zahlung" checkout option

Both already route through the same Stripe PaymentIntent
(automatic_payment_methods: enabled — Stripe's own recommended Payment
Element pattern, letting Stripe itself decide which eligible method to
show). Pre-selecting one of two identical-behind-the-scenes rows before
the payment step was redundant friction, not a real choice. Collapses
them into one option with a hint text explaining the actual instrument
is picked on the next screen; Überweisung is unaffected.

Also refines paymentMethodTitle from a neutral "Online-Zahlung"
placeholder (snapshotted at order-creation time, before the customer has
picked an instrument) to the real one Stripe reports, once payment
confirms — carried through to both the stored order and the
sessionStorage snapshot shown on /bestellbestaetigung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-25 12:17:38 +00:00
parent 4e22942031
commit bab2c916be
8 changed files with 152 additions and 27 deletions
+27 -18
View File
@@ -1,6 +1,6 @@
"use client";
import { forwardRef, useEffect, useRef, useState } from "react";
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
@@ -22,6 +22,7 @@ import { normalizeVatId, isValidVatId } from "../../lib/vatId";
import { computeExemptTotals, destinationCountry, isExemptionEligibleCountry } from "../../lib/vatExemption";
import { validateEmailFormat } from "../../lib/email";
import type { ShippingMethod, ShippingCountry, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
import { groupPaymentMethodsForCheckout } from "../../lib/payload";
import type { CustomerProfile } from "../../lib/customerAuth";
// Native HTML5 pattern validation (instant, no round-trip) mirroring the
@@ -141,7 +142,12 @@ export function CheckoutContent({
// validateZip() call site.
const plzDigitsMap: Record<string, number> = Object.fromEntries(shippingCountries.map((c) => [c.name, c.plzDigits]));
const [shippingMethodId, setShippingMethodId] = useState<number | null>(shippingMethods[0]?.id ?? null);
const [paymentMethodId, setPaymentMethodId] = useState<number | null>(paymentMethods[0]?.id ?? null);
// Kreditkarte/PayPal collapse into one "Online-Zahlung" option here —
// see groupPaymentMethodsForCheckout's own comment for why. The
// resulting id is still a real payment-methods row id, so everything
// downstream (submission, sessionStorage draft restore) is unaffected.
const paymentOptions = useMemo(() => groupPaymentMethodsForCheckout(paymentMethods), [paymentMethods]);
const [paymentMethodId, setPaymentMethodId] = useState<number | null>(paymentOptions[0]?.id ?? null);
const [versandOpen, setVersandOpen] = useState(false);
const [purchaseError, setPurchaseError] = useState<string | null>(null);
const [purchasing, setPurchasing] = useState(false);
@@ -1219,23 +1225,26 @@ export function CheckoutContent({
3. Zahlungsart
</p>
{paymentMethods.map((method) => (
<label key={method.id} className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="payment"
checked={paymentMethodId === method.id}
onChange={() => setPaymentMethodId(method.id)}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 text-body-sm text-text-primary">{method.title}</span>
<span className="flex items-center gap-2 shrink-0">
{method.icons.map((icon, i) => (
<div key={i} className="relative h-5 w-8 shrink-0">
<Image src={icon} alt="" fill sizes="32px" className="object-contain" />
</div>
))}
{paymentOptions.map((method) => (
<label key={method.id} className="flex flex-col gap-1 w-full cursor-pointer">
<span className="flex items-center gap-3 w-full">
<input
type="radio"
name="payment"
checked={paymentMethodId === method.id}
onChange={() => setPaymentMethodId(method.id)}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 text-body-sm text-text-primary">{method.title}</span>
<span className="flex items-center gap-2 shrink-0">
{method.icons.map((icon, i) => (
<div key={i} className="relative h-5 w-8 shrink-0">
<Image src={icon} alt="" fill sizes="32px" className="object-contain" />
</div>
))}
</span>
</span>
{method.hint && <span className="pl-8 text-label text-text-muted">{method.hint}</span>}
</label>
))}