fix: Zahlungsmethoden-Text vereinheitlichen, Retry-Button in Zahlungsfehler-Mail
- Apple Pay/Google Pay geprueft (Stripe-Doku via `stripe docs`): bei der hier verwendeten vollstaendig gehosteten Checkout-Seite (kein payment_method_types, kein ui_mode: "embedded") bereits automatisch enthalten, sobald Kartenzahlung aktiv ist - keine Codeaenderung noetig, keine Domain-Registrierung noetig (die betrifft laut Stripe nur Elements/eingebettete Checkout-Seite). Klaerenden Kommentar in api/checkout/route.ts und README-Abschnitt "Stripe" ergaenzt. - versand-zahlung-Seite nannte nur 4 von 6 tatsaechlich akzeptierten Zahlungsmethoden (SEPA-Lastschrift und Klarna fehlten, obwohl sie laut README aktiv unterstuetzt werden und auf der Checkout-Seite selbst bereits korrekt gelistet sind) - Text in beiden Sprachen an die vollstaendige Liste angeglichen. - Transaktions-E-Mails durchgesehen: Layout/Branding/Track-Order-Button bei Bestellbestaetigung und Versand-Mail bereits solide, keine Aenderung noetig. Die Zahlungsfehler-Mail forderte im Fliesstext zur erneuten Bestellung auf, hatte dafuer aber keinen anklickbaren Link - jetzt ein echter Button zum Shop. Verifiziert: tsc --noEmit sauber, pnpm test 151/151 gruen, pnpm build erfolgreich.
This commit is contained in:
1 parent
a579ebabeb
commit
6143e5704d
6 files changed
+21
-3
No files matched your search
@@ -150,6 +150,8 @@ Nur `db:push`/`db:migrate`/`db:deploy`/`db:studio` (Befehle, die tatsächlich ge
|
|||||||
|
|
||||||
Die Checkout Session gibt bewusst kein `payment_method_types` vor — Stripe zeigt Kundinnen und Kunden dadurch automatisch alle im Dashboard aktivierten, für Betrag/Währung/Land berechtigten Zahlungsmethoden an ("dynamische Zahlungsmethoden"). Um SEPA-Lastschrift und Klarna neben Kartenzahlung anzubieten, reicht es, sie unter [Dashboard → Zahlungsmethoden](https://dashboard.stripe.com/settings/payment_methods) zu aktivieren — dafür ist keine Codeänderung nötig.
|
Die Checkout Session gibt bewusst kein `payment_method_types` vor — Stripe zeigt Kundinnen und Kunden dadurch automatisch alle im Dashboard aktivierten, für Betrag/Währung/Land berechtigten Zahlungsmethoden an ("dynamische Zahlungsmethoden"). Um SEPA-Lastschrift und Klarna neben Kartenzahlung anzubieten, reicht es, sie unter [Dashboard → Zahlungsmethoden](https://dashboard.stripe.com/settings/payment_methods) zu aktivieren — dafür ist keine Codeänderung nötig.
|
||||||
|
|
||||||
|
**Apple Pay / Google Pay**: Diese Wallets sind bei der von Stripe gehosteten Checkout-Seite (der hier verwendete Modus, `stripe.checkout.sessions.create(...)` ohne `ui_mode: "embedded"`) laut Stripe-Dokumentation "ohne Änderungen an der Integration" automatisch enthalten, solange Kartenzahlung aktiviert ist (Standardeinstellung) — sie erscheinen als zusätzliche Option innerhalb des Kartenformulars auf unterstützten Geräten/Browsern (Safari mit hinterlegter Karte in Apple Wallet, Chrome/Android mit hinterlegter Karte in Google Pay). Die separate Domain-Registrierung unter [Dashboard → Zahlungsmethoden-Domains](https://dashboard.stripe.com/settings/payment_method_domains) ist laut Stripe nur für Elements bzw. die *eingebettete* Checkout-Zahlungsseite nötig, **nicht** für die vollständig gehostete Checkout-Seite (eigene, von Stripe bereits verifizierte Domain `checkout.stripe.com`) — hier also kein weiterer Schritt nötig. Falls die Wallets bei einem Testkauf trotzdem nicht erscheinen, liegt es fast immer am Testgerät/-browser (kein Apple Pay/Google Pay eingerichtet) oder daran, dass Kartenzahlung im Dashboard deaktiviert wurde, nicht am Code.
|
||||||
|
|
||||||
SEPA-Lastschrift und teils Klarna bestätigen die Zahlung **verzögert** (bei SEPA i. d. R. einige Werktage), nicht sofort wie eine Kartenzahlung. Der Webhook-Handler (`/api/checkout/webhook`) berücksichtigt das:
|
SEPA-Lastschrift und teils Klarna bestätigen die Zahlung **verzögert** (bei SEPA i. d. R. einige Werktage), nicht sofort wie eine Kartenzahlung. Der Webhook-Handler (`/api/checkout/webhook`) berücksichtigt das:
|
||||||
|
|
||||||
- `checkout.session.completed` markiert die Bestellung nur dann als bezahlt, wenn `payment_status` nicht `unpaid` ist (bei Kartenzahlung sofort der Fall).
|
- `checkout.session.completed` markiert die Bestellung nur dann als bezahlt, wenn `payment_status` nicht `unpaid` ist (bei Kartenzahlung sofort der Fall).
|
||||||
|
|||||||
@@ -298,6 +298,14 @@ export async function POST(request: Request) {
|
|||||||
discounts = [{ coupon: coupon.id }];
|
discounts = [{ coupon: coupon.id }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kein payment_method_types (s. README "Zahlungsmethoden"): Stripe zeigt
|
||||||
|
// dadurch dynamisch alle im Dashboard aktivierten, berechtigten
|
||||||
|
// Zahlungsmethoden. Apple Pay/Google Pay sind bei dieser vollstaendig
|
||||||
|
// gehosteten Checkout-Seite (kein ui_mode: "embedded") laut Stripe-Doku
|
||||||
|
// automatisch enthalten, sobald Kartenzahlung aktiv ist — keine
|
||||||
|
// Codeaenderung und (anders als bei Elements/eingebetteter Checkout-Seite)
|
||||||
|
// keine Domain-Registrierung noetig, da checkout.stripe.com bereits
|
||||||
|
// Stripes eigene, verifizierte Domain ist.
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
mode: "payment",
|
mode: "payment",
|
||||||
customer_email: input.email,
|
customer_email: input.email,
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ export const de: Dictionary = {
|
|||||||
tierFallback: "Die aktuellen Versandkosten werden dir vor Abschluss der Bestellung transparent angezeigt.",
|
tierFallback: "Die aktuellen Versandkosten werden dir vor Abschluss der Bestellung transparent angezeigt.",
|
||||||
pickupNote: "Alternativ kannst du deine Bestellung nach Absprache direkt bei uns in Altenmünster abholen — dabei fallen keine Versandkosten an.",
|
pickupNote: "Alternativ kannst du deine Bestellung nach Absprache direkt bei uns in Altenmünster abholen — dabei fallen keine Versandkosten an.",
|
||||||
paymentHeading: "Zahlungsmethoden",
|
paymentHeading: "Zahlungsmethoden",
|
||||||
paymentText: "Wir bieten PayPal, Kreditkarte, Apple Pay und Google Pay über unseren Zahlungsdienstleister Stripe an. Eine Zahlung per Überweisung ist auf Anfrage möglich.",
|
paymentText: "Wir bieten Kreditkarte, SEPA-Lastschrift, Klarna, PayPal, Apple Pay und Google Pay über unseren Zahlungsdienstleister Stripe an. Eine Zahlung per Überweisung ist auf Anfrage möglich.",
|
||||||
deliveryTimeHeading: "Lieferzeit",
|
deliveryTimeHeading: "Lieferzeit",
|
||||||
deliveryTimeText: "Die übliche Lieferzeit innerhalb Deutschlands beträgt 2–4 Werktage nach Zahlungseingang.",
|
deliveryTimeText: "Die übliche Lieferzeit innerhalb Deutschlands beträgt 2–4 Werktage nach Zahlungseingang.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ export const en: Dictionary = {
|
|||||||
tierFallback: "The current shipping costs will be shown to you transparently before you complete your order.",
|
tierFallback: "The current shipping costs will be shown to you transparently before you complete your order.",
|
||||||
pickupNote: "Alternatively, you can pick up your order directly from us in Altenmünster by arrangement — no shipping costs apply in that case.",
|
pickupNote: "Alternatively, you can pick up your order directly from us in Altenmünster by arrangement — no shipping costs apply in that case.",
|
||||||
paymentHeading: "Payment Methods",
|
paymentHeading: "Payment Methods",
|
||||||
paymentText: "We offer PayPal, credit card, Apple Pay, and Google Pay via our payment provider Stripe. Payment by bank transfer is available on request.",
|
paymentText: "We offer credit card, SEPA direct debit, Klarna, PayPal, Apple Pay, and Google Pay via our payment provider Stripe. Payment by bank transfer is available on request.",
|
||||||
deliveryTimeHeading: "Delivery Time",
|
deliveryTimeHeading: "Delivery Time",
|
||||||
deliveryTimeText: "Typical delivery time within Germany is 2–4 business days after payment is received.",
|
deliveryTimeText: "Typical delivery time within Germany is 2–4 business days after payment is received.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ interface EmailStrings {
|
|||||||
heading: (firstName: string) => string;
|
heading: (firstName: string) => string;
|
||||||
intro: (orderNumber: string) => string;
|
intro: (orderNumber: string) => string;
|
||||||
retryNote: string;
|
retryNote: string;
|
||||||
|
button: string;
|
||||||
subject: (orderNumber: string) => string;
|
subject: (orderNumber: string) => string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -198,6 +199,7 @@ const de: EmailStrings = {
|
|||||||
intro: (orderNumber) =>
|
intro: (orderNumber) =>
|
||||||
`Der Zahlungseinzug für deine Bestellung <strong>${orderNumber}</strong> konnte nicht abgeschlossen werden. Wir haben die Bestellung daher storniert, es sind dir keine Kosten entstanden.`,
|
`Der Zahlungseinzug für deine Bestellung <strong>${orderNumber}</strong> konnte nicht abgeschlossen werden. Wir haben die Bestellung daher storniert, es sind dir keine Kosten entstanden.`,
|
||||||
retryNote: "Du kannst die Bestellung gerne erneut aufgeben, zum Beispiel mit einer anderen Zahlungsmethode.",
|
retryNote: "Du kannst die Bestellung gerne erneut aufgeben, zum Beispiel mit einer anderen Zahlungsmethode.",
|
||||||
|
button: "ERNEUT BESTELLEN",
|
||||||
subject: (orderNumber) => `Zahlung zu Bestellung ${orderNumber} fehlgeschlagen — Wabenhain`,
|
subject: (orderNumber) => `Zahlung zu Bestellung ${orderNumber} fehlgeschlagen — Wabenhain`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -298,6 +300,7 @@ const en: EmailStrings = {
|
|||||||
intro: (orderNumber) =>
|
intro: (orderNumber) =>
|
||||||
`The payment for your order <strong>${orderNumber}</strong> could not be completed. We've cancelled the order, so no costs were incurred.`,
|
`The payment for your order <strong>${orderNumber}</strong> could not be completed. We've cancelled the order, so no costs were incurred.`,
|
||||||
retryNote: "Feel free to place the order again, for example with a different payment method.",
|
retryNote: "Feel free to place the order again, for example with a different payment method.",
|
||||||
|
button: "ORDER AGAIN",
|
||||||
subject: (orderNumber) => `Payment for order ${orderNumber} failed — Wabenhain`,
|
subject: (orderNumber) => `Payment for order ${orderNumber} failed — Wabenhain`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -354,6 +354,10 @@ export async function returnRefundedEmail(order: OrderStatusUpdateInput): Promis
|
|||||||
|
|
||||||
export async function paymentFailedEmail(order: OrderStatusUpdateInput): Promise<EmailContent> {
|
export async function paymentFailedEmail(order: OrderStatusUpdateInput): Promise<EmailContent> {
|
||||||
const s = getEmailStrings(order.locale).paymentFailed;
|
const s = getEmailStrings(order.locale).paymentFailed;
|
||||||
|
// Der Text (retryNote) fordert bereits zur erneuten Bestellung auf — ohne
|
||||||
|
// einen tatsaechlich anklickbaren Button dazu bliebe das ein reiner
|
||||||
|
// Lauftext-Hinweis ohne direkten Weg zurueck in den Shop.
|
||||||
|
const shopUrl = `https://${siteConfig.domain}/shop`;
|
||||||
const html = await emailLayout(
|
const html = await emailLayout(
|
||||||
order.locale,
|
order.locale,
|
||||||
s.preheader(order.orderNumber),
|
s.preheader(order.orderNumber),
|
||||||
@@ -361,11 +365,12 @@ export async function paymentFailedEmail(order: OrderStatusUpdateInput): Promise
|
|||||||
<h1 style="font-size:20px;color:#24352B;margin:0 0 12px;">${s.heading(order.firstName)}</h1>
|
<h1 style="font-size:20px;color:#24352B;margin:0 0 12px;">${s.heading(order.firstName)}</h1>
|
||||||
<p style="margin:0 0 8px;line-height:1.5;">${s.intro(order.orderNumber)}</p>
|
<p style="margin:0 0 8px;line-height:1.5;">${s.intro(order.orderNumber)}</p>
|
||||||
<p style="margin:0;line-height:1.5;">${s.retryNote}</p>
|
<p style="margin:0;line-height:1.5;">${s.retryNote}</p>
|
||||||
|
<div style="margin-top:20px;">${button(shopUrl, s.button)}</div>
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
subject: s.subject(order.orderNumber),
|
subject: s.subject(order.orderNumber),
|
||||||
html,
|
html,
|
||||||
text: `${s.intro(order.orderNumber).replace(/<\/?strong>/g, "")} ${s.retryNote}`,
|
text: `${s.intro(order.orderNumber).replace(/<\/?strong>/g, "")} ${s.retryNote}\n\n${s.button}: ${shopUrl}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user