Files
cozsweet-frontend-nextjs/src/app/subscription/components/subscription-checkout-button.tsx
T

159 lines
5.0 KiB
TypeScript

"use client";
/**
* 订阅 checkout 触发按钮
*
* 包装 `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,
} from "@/stores/payment/payment-context";
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 {
/** 是否可用(未选套餐、缺少角色来源或支付处理中为 false) */
disabled?: boolean;
subscriptionType: "vip" | "topup";
returnTo?: SubscriptionReturnTo;
sourceCharacterSlug?: string;
renewalPlan?: VipOfferPlanView | null;
renewalConsentSubjectId?: string | null;
}
export function SubscriptionCheckoutButton({
disabled = false,
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({
log,
logScope: "subscription-checkout",
payment,
paymentDispatch,
returnTo: returnTo ?? undefined,
characterSlug: sourceCharacterSlug,
subscriptionType,
});
const isResumingQris = paymentLaunch.hasHiddenQrisPayment;
const isLoading =
payment.isCreatingOrder || (payment.isPollingOrder && !isResumingQris);
const readyLabel = "Pay and Top Up";
const label = isResumingQris
? "Resume QRIS payment"
: payment.isPollingOrder
? "Processing payment..."
: payment.isCreatingOrder
? "Creating order..."
: readyLabel;
const startCheckout = () => {
if (disabled || isLoading) return;
paymentLaunch.resetLaunchState();
paymentDispatch({ type: "PaymentCreateOrderSubmitted" });
};
const handleClick = () => {
if (isResumingQris) {
paymentLaunch.handleQrisResume();
return;
}
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
type="button"
data-analytics-key="subscription.checkout"
data-analytics-label="Start subscription checkout"
disabled={disabled && !isResumingQris}
isLoading={isLoading}
onClick={handleClick}
>
{label}
</SubscriptionCtaButton>
{payment.payChannel === "stripe" && payment.selectedPlanId ? (
<ExternalBrowserCheckoutButton
disabled={disabled || isLoading}
checkoutIntent={{
planId: payment.selectedPlanId,
autoRenew: payment.autoRenew,
...(payment.commercialOfferId
? { commercialOfferId: payment.commercialOfferId }
: {}),
...(payment.chatActionId
? { chatActionId: payment.chatActionId }
: {}),
}}
/>
) : null}
{payment.errorMessage ? (
<p
role="alert"
style={{
color: "#c0392b",
fontSize: "0.875rem",
marginTop: "0.5rem",
textAlign: "center",
}}
>
{payment.errorMessage}
</p>
) : null}
<PaymentLaunchDialogs
currentOrderId={payment.currentOrderId}
externalCheckoutAnalyticsKey="subscription.external_checkout"
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}
/>
</>
);
}