refactor(payment): share launch flow

This commit is contained in:
2026-07-09 12:38:53 +08:00
parent a92780373d
commit c67689f8a1
4 changed files with 290 additions and 258 deletions
@@ -4,19 +4,12 @@
*
* 包装 `SubscriptionCtaButton` —— 通过 payment 状态机创建后端订单并拉起支付。
*/
import { useEffect, useRef, useState } from "react";
import {
getPaymentUrl,
getStripeClientSecret,
isEzpayPayment,
launchEzpayRedirect,
} from "@/lib/payment/payment_launch";
import { usePaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow";
import {
usePaymentDispatch,
usePaymentState,
} from "@/stores/payment/payment-context";
import { AppEnvUtil, Logger } from "@/utils";
import { Logger } from "@/utils";
import { StripePaymentDialog } from "./stripe-payment-dialog";
import dialogStyles from "./stripe-payment-dialog.module.css";
@@ -39,65 +32,14 @@ export function SubscriptionCheckoutButton({
}: SubscriptionCheckoutButtonProps) {
const payment = usePaymentState();
const paymentDispatch = usePaymentDispatch();
const launchedNonceRef = useRef(0);
const [hiddenStripeClientSecret, setHiddenStripeClientSecret] = useState<
string | null
>(null);
const [isConfirmingEzpay, setIsConfirmingEzpay] = useState(false);
useEffect(() => {
if (!payment.payParams || payment.launchNonce <= launchedNonceRef.current) {
return;
}
launchedNonceRef.current = payment.launchNonce;
const clientSecret = getStripeClientSecret(payment.payParams);
if (clientSecret) {
return;
}
const paymentUrl = getPaymentUrl(payment.payParams);
if (paymentUrl) {
const isEzpay = isEzpayPayment(payment.payParams);
if (!AppEnvUtil.isProduction() && isEzpay) {
log.debug("[subscription-checkout] ezpay confirmation required", {
orderId: payment.currentOrderId,
paymentUrl,
subscriptionType,
});
return;
}
if (isEzpay) {
void launchEzpayRedirect({
orderId: payment.currentOrderId,
paymentUrl,
subscriptionType,
returnTo: returnTo ?? undefined,
onFailed: (errorMessage) =>
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }),
});
return;
}
window.location.href = paymentUrl;
return;
}
paymentDispatch({
type: "PaymentLaunchFailed",
errorMessage:
"Payment parameters did not include a supported URL or Stripe client secret.",
});
}, [
payment.currentOrderId,
payment.launchNonce,
payment.payParams,
const paymentLaunch = usePaymentLaunchFlow({
log,
logScope: "subscription-checkout",
payment,
paymentDispatch,
returnTo,
returnTo: returnTo ?? undefined,
subscriptionType,
]);
});
const isLoading =
payment.isCreatingOrder ||
@@ -118,69 +60,10 @@ export function SubscriptionCheckoutButton({
const handleClick = () => {
if (disabled || isLoading) return;
setIsConfirmingEzpay(false);
setHiddenStripeClientSecret(null);
paymentLaunch.resetLaunchState();
paymentDispatch({ type: "PaymentCreateOrderSubmitted" });
};
const handleStripeClose = () => {
if (stripeClientSecret) {
setHiddenStripeClientSecret(stripeClientSecret);
}
if (payment.orderStatus !== "paid") {
paymentDispatch({ type: "PaymentReset" });
}
};
const handleStripeConfirmed = () => {
if (stripeClientSecret) {
setHiddenStripeClientSecret(stripeClientSecret);
}
};
const stripeClientSecret = payment.payParams
? getStripeClientSecret(payment.payParams)
: null;
const shouldShowStripeDialog =
stripeClientSecret !== null &&
stripeClientSecret !== hiddenStripeClientSecret &&
!payment.isPaid;
const ezpayPaymentUrl = payment.payParams
? getPaymentUrl(payment.payParams)
: null;
const shouldShowEzpayConfirmDialog =
!AppEnvUtil.isProduction() &&
payment.payParams &&
isEzpayPayment(payment.payParams) &&
ezpayPaymentUrl &&
payment.currentOrderId
? true
: false;
const handleEzpayConfirm = () => {
if (!payment.currentOrderId || !ezpayPaymentUrl) return;
setIsConfirmingEzpay(true);
void launchEzpayRedirect({
orderId: payment.currentOrderId,
paymentUrl: ezpayPaymentUrl,
subscriptionType,
returnTo: returnTo ?? undefined,
onFailed: (errorMessage) => {
setIsConfirmingEzpay(false);
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage });
},
});
};
const handleEzpayCancel = () => {
log.debug("[subscription-checkout] ezpay confirmation cancelled", {
orderId: payment.currentOrderId,
subscriptionType,
});
setIsConfirmingEzpay(false);
paymentDispatch({ type: "PaymentReset" });
};
return (
<>
<SubscriptionCtaButton
@@ -205,19 +88,19 @@ export function SubscriptionCheckoutButton({
{payment.errorMessage}
</p>
) : null}
{shouldShowEzpayConfirmDialog ? (
{paymentLaunch.shouldShowEzpayConfirmDialog ? (
<EzpayRedirectConfirmDialog
orderId={payment.currentOrderId ?? ""}
isConfirming={isConfirmingEzpay}
onCancel={handleEzpayCancel}
onConfirm={handleEzpayConfirm}
isConfirming={paymentLaunch.isConfirmingEzpay}
onCancel={paymentLaunch.handleEzpayCancel}
onConfirm={paymentLaunch.handleEzpayConfirm}
/>
) : null}
{shouldShowStripeDialog ? (
{paymentLaunch.shouldShowStripeDialog && paymentLaunch.stripeClientSecret ? (
<StripePaymentDialog
clientSecret={stripeClientSecret}
onClose={handleStripeClose}
onConfirmed={handleStripeConfirmed}
clientSecret={paymentLaunch.stripeClientSecret}
onClose={paymentLaunch.handleStripeClose}
onConfirmed={paymentLaunch.handleStripeConfirmed}
/>
) : null}
</>