feat(payment): confirm renewal only at checkout

This commit is contained in:
Codex
2026-07-29 11:26:42 +08:00
parent 196f9e39dc
commit 47c5f4098f
15 changed files with 492 additions and 240 deletions
@@ -4,11 +4,17 @@
*
* 包装 `SubscriptionCtaButton` —— 通过 payment 状态机创建后端订单并拉起支付。
*/
import { useState } from "react";
import { usePaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow";
import { PaymentLaunchDialogs } from "@/app/_components/payment/payment-launch-dialogs";
import { ExternalBrowserCheckoutButton } from "@/app/_components/payment/external-browser-checkout-button";
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
import {
hasAutomaticRenewalAcknowledgement,
rememberAutomaticRenewalAcknowledgement,
} from "@/lib/payment/automatic_renewal_acknowledgement";
import {
usePaymentDispatch,
usePaymentState,
@@ -16,15 +22,19 @@ import {
import { Logger } from "@/utils/logger";
import { SubscriptionCtaButton } from "./subscription-cta-button";
import { SubscriptionRenewalConfirmationDialog } from "./subscription-renewal-confirmation-dialog";
import type { VipOfferPlanView } from "./subscription-vip-offer-section";
const log = new Logger("SubscriptionCheckoutButton");
export interface SubscriptionCheckoutButtonProps {
/** 是否可用(未选套餐或 VIP 套餐尚未确认自动续费时为 false */
/** 是否可用(未选套餐、缺少角色来源或支付处理中为 false */
disabled?: boolean;
subscriptionType: "vip" | "topup";
returnTo?: SubscriptionReturnTo;
sourceCharacterSlug?: string;
renewalPlan?: VipOfferPlanView | null;
renewalConsentSubjectId?: string | null;
}
export function SubscriptionCheckoutButton({
@@ -32,7 +42,11 @@ export function SubscriptionCheckoutButton({
subscriptionType,
returnTo = null,
sourceCharacterSlug = DEFAULT_CHARACTER_SLUG,
renewalPlan = null,
renewalConsentSubjectId = null,
}: SubscriptionCheckoutButtonProps) {
const [showRenewalConfirmation, setShowRenewalConfirmation] =
useState(false);
const payment = usePaymentState();
const paymentDispatch = usePaymentDispatch();
const paymentLaunch = usePaymentLaunchFlow({
@@ -53,12 +67,32 @@ export function SubscriptionCheckoutButton({
? "Creating order..."
: readyLabel;
const handleClick = () => {
const startCheckout = () => {
if (disabled || isLoading) return;
paymentLaunch.resetLaunchState();
paymentDispatch({ type: "PaymentCreateOrderSubmitted" });
};
const handleClick = () => {
if (disabled || isLoading) return;
if (
payment.payChannel === "stripe" &&
payment.autoRenew &&
renewalPlan !== null &&
!hasAutomaticRenewalAcknowledgement(renewalConsentSubjectId)
) {
setShowRenewalConfirmation(true);
return;
}
startCheckout();
};
const handleRenewalConfirm = () => {
rememberAutomaticRenewalAcknowledgement(renewalConsentSubjectId);
setShowRenewalConfirmation(false);
startCheckout();
};
return (
<>
<SubscriptionCtaButton
@@ -105,6 +139,12 @@ export function SubscriptionCheckoutButton({
ezpayDescription="Your order has been created. Continue to the secure payment page to finish."
launch={paymentLaunch}
/>
<SubscriptionRenewalConfirmationDialog
open={showRenewalConfirmation}
plan={renewalPlan}
onCancel={() => setShowRenewalConfirmation(false)}
onConfirm={handleRenewalConfirm}
/>
</>
);
}