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

161 lines
4.5 KiB
TypeScript

"use client";
/**
* 订阅 checkout 触发按钮
*
* 包装 `SubscriptionCtaButton` —— 通过 payment 状态机创建后端订单并拉起支付。
*/
import { usePaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow";
import {
usePaymentDispatch,
usePaymentState,
} from "@/stores/payment/payment-context";
import { Logger } from "@/utils";
import { StripePaymentDialog } from "./stripe-payment-dialog";
import dialogStyles from "./stripe-payment-dialog.module.css";
import { SubscriptionCtaButton } from "./subscription-cta-button";
const log = new Logger("SubscriptionCheckoutButton");
export interface SubscriptionCheckoutButtonProps {
/** 是否可用(未选套餐 / 未勾选协议 → false) */
disabled?: boolean;
subscriptionType: "vip" | "topup";
returnTo?: "chat" | null;
}
export function SubscriptionCheckoutButton({
disabled = false,
subscriptionType,
returnTo = null,
}: SubscriptionCheckoutButtonProps) {
const payment = usePaymentState();
const paymentDispatch = usePaymentDispatch();
const paymentLaunch = usePaymentLaunchFlow({
log,
logScope: "subscription-checkout",
payment,
paymentDispatch,
returnTo: returnTo ?? undefined,
subscriptionType,
});
const isLoading =
payment.isCreatingOrder ||
payment.isPollingOrder;
const readyLabel =
subscriptionType === "topup" ? "Pay and Top Up" : "Pay and Activiate";
const agreementLabel =
subscriptionType === "topup"
? "Confirm the agreement and Top Up"
: "Confirm the agreement and Activate";
const label = payment.isPollingOrder
? "Processing payment..."
: payment.isCreatingOrder
? "Creating order..."
: payment.agreed
? readyLabel
: agreementLabel;
const handleClick = () => {
if (disabled || isLoading) return;
paymentLaunch.resetLaunchState();
paymentDispatch({ type: "PaymentCreateOrderSubmitted" });
};
return (
<>
<SubscriptionCtaButton
type="button"
disabled={disabled}
isLoading={isLoading}
onClick={handleClick}
>
{label}
</SubscriptionCtaButton>
{payment.errorMessage ? (
<p
role="alert"
style={{
color: "#c0392b",
fontSize: "0.875rem",
marginTop: "0.5rem",
textAlign: "center",
}}
>
{payment.errorMessage}
</p>
) : null}
{paymentLaunch.shouldShowEzpayConfirmDialog ? (
<EzpayRedirectConfirmDialog
orderId={payment.currentOrderId ?? ""}
isConfirming={paymentLaunch.isConfirmingEzpay}
onCancel={paymentLaunch.handleEzpayCancel}
onConfirm={paymentLaunch.handleEzpayConfirm}
/>
) : null}
{paymentLaunch.shouldShowStripeDialog && paymentLaunch.stripeClientSecret ? (
<StripePaymentDialog
clientSecret={paymentLaunch.stripeClientSecret}
onClose={paymentLaunch.handleStripeClose}
onConfirmed={paymentLaunch.handleStripeConfirmed}
/>
) : null}
</>
);
}
interface EzpayRedirectConfirmDialogProps {
orderId: string;
isConfirming: boolean;
onCancel: () => void;
onConfirm: () => void;
}
function EzpayRedirectConfirmDialog({
orderId,
isConfirming,
onCancel,
onConfirm,
}: EzpayRedirectConfirmDialogProps) {
return (
<div
className={dialogStyles.overlay}
role="alertdialog"
aria-modal="true"
aria-labelledby="ezpay-redirect-title"
>
<div className={dialogStyles.dialog}>
<div className={dialogStyles.header}>
<h2 id="ezpay-redirect-title" className={dialogStyles.title}>
Continue to GCash?
</h2>
<p className={dialogStyles.content}>
Your order has been created. Continue to GCash to finish the
payment.
</p>
<p className={dialogStyles.content}>Order No. {orderId}</p>
</div>
<div className={dialogStyles.actions}>
<button
type="button"
className={`${dialogStyles.button} ${dialogStyles.secondary}`}
disabled={isConfirming}
onClick={onCancel}
>
Cancel
</button>
<button
type="button"
className={`${dialogStyles.button} ${dialogStyles.primary}`}
disabled={isConfirming}
onClick={onConfirm}
>
{isConfirming ? "Opening..." : "Continue"}
</button>
</div>
</div>
</div>
);
}