diff --git a/src/app/subscription/components/stripe-payment-dialog.module.css b/src/app/subscription/components/stripe-payment-dialog.module.css index 2cd64a18..25160cca 100644 --- a/src/app/subscription/components/stripe-payment-dialog.module.css +++ b/src/app/subscription/components/stripe-payment-dialog.module.css @@ -46,6 +46,10 @@ gap: 18px; } +.expressCheckout { + min-height: 48px; +} + .error { margin: 0; color: #c0392b; diff --git a/src/app/subscription/components/stripe-payment-dialog.tsx b/src/app/subscription/components/stripe-payment-dialog.tsx index f0fc66ec..dcef8cb5 100644 --- a/src/app/subscription/components/stripe-payment-dialog.tsx +++ b/src/app/subscription/components/stripe-payment-dialog.tsx @@ -7,6 +7,7 @@ import { useState, type FormEvent } from "react"; import { Elements, + ExpressCheckoutElement, PaymentElement, useElements, useStripe, @@ -106,6 +107,57 @@ function StripePaymentForm({ const [errorMessage, setErrorMessage] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const handleExpressReady = (event: Parameters< + NonNullable["onReady"]> + >[0]) => { + log.important( + "info", + "[stripe-payment-dialog] ExpressCheckoutElement ready", + { + availablePaymentMethods: event.availablePaymentMethods ?? null, + userAgent: navigator.userAgent, + }, + ); + }; + + const handleExpressAvailablePaymentMethodsChange = (event: Parameters< + NonNullable< + React.ComponentProps< + typeof ExpressCheckoutElement + >["onAvailablePaymentMethodsChange"] + > + >[0]) => { + log.important( + "info", + "[stripe-payment-dialog] ExpressCheckoutElement available payment methods changed", + { + paymentMethods: event.paymentMethods ?? null, + userAgent: navigator.userAgent, + }, + ); + }; + + const handleExpressLoadError = (event: Parameters< + NonNullable< + React.ComponentProps["onLoadError"] + > + >[0]) => { + log.warn("[stripe-payment-dialog] ExpressCheckoutElement load failed", { + type: event.error.type, + code: event.error.code, + message: event.error.message, + declineCode: event.error.decline_code, + }); + }; + + const handleExpressCancel = (event: Parameters< + NonNullable["onCancel"]> + >[0]) => { + log.debug("[stripe-payment-dialog] Express checkout cancelled", { + elementType: event.elementType, + }); + }; + const handleLoadError = (event: Parameters< NonNullable["onLoadError"]> >[0]) => { @@ -121,16 +173,37 @@ function StripePaymentForm({ ); }; - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - if (!stripe || !elements || isSubmitting) return; + const confirmStripePayment = async ( + source: "payment_element" | "express_checkout", + expressEvent?: Parameters< + NonNullable< + React.ComponentProps["onConfirm"] + > + >[0], + ) => { + if (!stripe || !elements || isSubmitting) { + expressEvent?.paymentFailed({ + reason: "fail", + message: "Payment is not ready yet. Please try again.", + }); + return; + } + log.debug("[stripe-payment-dialog] confirming payment", { + source, + expressPaymentType: expressEvent?.expressPaymentType, + }); setIsSubmitting(true); setErrorMessage(null); const { error: submitError } = await elements.submit(); if (submitError) { - setErrorMessage(submitError.message ?? "Please check your payment info."); + const message = submitError.message ?? "Please check your payment info."; + setErrorMessage(message); + expressEvent?.paymentFailed({ + reason: "invalid_payment_data", + message, + }); setIsSubmitting(false); return; } @@ -148,16 +221,59 @@ function StripePaymentForm({ }); if (error) { - setErrorMessage(error.message ?? "Payment confirmation failed."); + const message = error.message ?? "Payment confirmation failed."; + setErrorMessage(message); + expressEvent?.paymentFailed({ + reason: "fail", + message, + }); setIsSubmitting(false); return; } + log.info("[stripe-payment-dialog] payment confirmed", { + source, + expressPaymentType: expressEvent?.expressPaymentType, + }); onConfirmed?.(); }; + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + void confirmStripePayment("payment_element"); + }; + + const handleExpressConfirm = (event: Parameters< + NonNullable["onConfirm"]> + >[0]) => { + void confirmStripePayment("express_checkout", event); + }; + return (
+
+ +
{errorMessage ? (