feat(subscription): add Stripe express checkout logging

This commit is contained in:
2026-07-02 16:32:53 +08:00
parent b5ca2d301f
commit 2878cfa558
2 changed files with 125 additions and 5 deletions
@@ -46,6 +46,10 @@
gap: 18px; gap: 18px;
} }
.expressCheckout {
min-height: 48px;
}
.error { .error {
margin: 0; margin: 0;
color: #c0392b; color: #c0392b;
@@ -7,6 +7,7 @@
import { useState, type FormEvent } from "react"; import { useState, type FormEvent } from "react";
import { import {
Elements, Elements,
ExpressCheckoutElement,
PaymentElement, PaymentElement,
useElements, useElements,
useStripe, useStripe,
@@ -106,6 +107,57 @@ function StripePaymentForm({
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const handleExpressReady = (event: Parameters<
NonNullable<React.ComponentProps<typeof ExpressCheckoutElement>["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<typeof ExpressCheckoutElement>["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<React.ComponentProps<typeof ExpressCheckoutElement>["onCancel"]>
>[0]) => {
log.debug("[stripe-payment-dialog] Express checkout cancelled", {
elementType: event.elementType,
});
};
const handleLoadError = (event: Parameters< const handleLoadError = (event: Parameters<
NonNullable<React.ComponentProps<typeof PaymentElement>["onLoadError"]> NonNullable<React.ComponentProps<typeof PaymentElement>["onLoadError"]>
>[0]) => { >[0]) => {
@@ -121,16 +173,37 @@ function StripePaymentForm({
); );
}; };
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { const confirmStripePayment = async (
event.preventDefault(); source: "payment_element" | "express_checkout",
if (!stripe || !elements || isSubmitting) return; expressEvent?: Parameters<
NonNullable<
React.ComponentProps<typeof ExpressCheckoutElement>["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); setIsSubmitting(true);
setErrorMessage(null); setErrorMessage(null);
const { error: submitError } = await elements.submit(); const { error: submitError } = await elements.submit();
if (submitError) { 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); setIsSubmitting(false);
return; return;
} }
@@ -148,16 +221,59 @@ function StripePaymentForm({
}); });
if (error) { if (error) {
setErrorMessage(error.message ?? "Payment confirmation failed."); const message = error.message ?? "Payment confirmation failed.";
setErrorMessage(message);
expressEvent?.paymentFailed({
reason: "fail",
message,
});
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
log.info("[stripe-payment-dialog] payment confirmed", {
source,
expressPaymentType: expressEvent?.expressPaymentType,
});
onConfirmed?.(); onConfirmed?.();
}; };
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
void confirmStripePayment("payment_element");
};
const handleExpressConfirm = (event: Parameters<
NonNullable<React.ComponentProps<typeof ExpressCheckoutElement>["onConfirm"]>
>[0]) => {
void confirmStripePayment("express_checkout", event);
};
return ( return (
<form className={styles.form} onSubmit={handleSubmit}> <form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.expressCheckout}>
<ExpressCheckoutElement
options={{
buttonHeight: 48,
layout: {
maxColumns: 1,
maxRows: 3,
},
paymentMethods: {
applePay: "auto",
googlePay: "auto",
link: "auto",
},
}}
onReady={handleExpressReady}
onLoadError={handleExpressLoadError}
onAvailablePaymentMethodsChange={
handleExpressAvailablePaymentMethodsChange
}
onCancel={handleExpressCancel}
onConfirm={handleExpressConfirm}
/>
</div>
<PaymentElement onLoadError={handleLoadError} /> <PaymentElement onLoadError={handleLoadError} />
{errorMessage ? ( {errorMessage ? (
<p role="alert" className={styles.error}> <p role="alert" className={styles.error}>