diff --git a/src/app/subscription/components/stripe-payment-dialog.tsx b/src/app/subscription/components/stripe-payment-dialog.tsx index 12c093aa..f0fc66ec 100644 --- a/src/app/subscription/components/stripe-payment-dialog.tsx +++ b/src/app/subscription/components/stripe-payment-dialog.tsx @@ -4,8 +4,7 @@ * * 后端当前返回 PaymentIntent clientSecret,因此这里直接用它完成前端确认。 */ -import { useEffect, useState, type FormEvent } from "react"; -import { createPortal } from "react-dom"; +import { useState, type FormEvent } from "react"; import { Elements, PaymentElement, @@ -36,44 +35,31 @@ export function StripePaymentDialog({ onClose, onConfirmed, }: StripePaymentDialogProps) { - useEffect(() => { - if (typeof document === "undefined") return; - - const prevOverflow = document.body.style.overflow; - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") onClose(); - }; - - document.body.style.overflow = "hidden"; - document.addEventListener("keydown", handleKeyDown); - - return () => { - document.body.style.overflow = prevOverflow; - document.removeEventListener("keydown", handleKeyDown); - }; - }, [onClose]); - - const dialog = !stripePromise ? ( -
-
-
-

Payment unavailable

-

- Stripe publishable key is not configured for this build. -

-
-
- + if (!stripePromise) { + return ( +
+
+
+

Payment unavailable

+

+ Stripe publishable key is not configured for this build. +

+
+
+ +
-
- ) : ( + ); + } + + return (
); - - if (typeof document === "undefined") return null; - - return createPortal(dialog, document.body); } function StripePaymentForm({ diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index b71d3c27..9995260c 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -13,7 +13,6 @@ import { } from "@/stores/payment/payment-context"; import { AppEnvUtil, Logger, Result } from "@/utils"; -import { StripePaymentDialog } from "./stripe-payment-dialog"; import dialogStyles from "./stripe-payment-dialog.module.css"; import { SubscriptionCtaButton } from "./subscription-cta-button"; import styles from "./subscription-cta-button.module.css"; @@ -35,9 +34,6 @@ export function SubscriptionCheckoutButton({ const payment = usePaymentState(); const paymentDispatch = usePaymentDispatch(); const launchedNonceRef = useRef(0); - const [hiddenStripeClientSecret, setHiddenStripeClientSecret] = useState< - string | null - >(null); const [isConfirmingEzpay, setIsConfirmingEzpay] = useState(false); useEffect(() => { @@ -98,7 +94,7 @@ export function SubscriptionCheckoutButton({ payment.isCreatingOrder || payment.isPollingOrder; const readyLabel = - subscriptionType === "topup" ? "Pay and Top Up" : "Pay and Activiate"; + subscriptionType === "topup" ? "Pay and Top Up" : "Pay and Activate"; const agreementLabel = subscriptionType === "topup" ? "Confirm the agreement and Top Up" @@ -114,32 +110,9 @@ export function SubscriptionCheckoutButton({ const handleClick = () => { if (disabled || isLoading) return; setIsConfirmingEzpay(false); - setHiddenStripeClientSecret(null); 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; @@ -208,13 +181,6 @@ export function SubscriptionCheckoutButton({ onConfirm={handleEzpayConfirm} /> ) : null} - {shouldShowStripeDialog ? ( - - ) : null} ); } @@ -366,7 +332,7 @@ function EzpayRedirectConfirmDialog({ ); } -function getStripeClientSecret( +export function getStripeClientSecret( payParams: Record, ): string | null { const provider = payParams.provider; diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index ca9ef45f..ef77f69a 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -13,12 +13,14 @@ import { useUserDispatch } from "@/stores/user/user-context"; import { ROUTES } from "@/router/routes"; import { + getStripeClientSecret, SubscriptionBackLink, SubscriptionCheckoutButton, SubscriptionCoinsOfferSection, SubscriptionPaymentMethod, SubscriptionPaymentSuccessDialog, SubscriptionVipOfferSection, + StripePaymentDialog, } from "./components"; import styles from "./components/subscription-screen.module.css"; import { @@ -49,6 +51,9 @@ export function SubscriptionScreen({ const refreshedPaidOrderRef = useRef(null); const resumedPendingOrderRef = useRef(null); const successDialogShownOrderRef = useRef(null); + const [hiddenStripeDialogKey, setHiddenStripeDialogKey] = useState< + string | null + >(null); const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] = useState(false); const canSubscribeVip = subscriptionType === "vip"; @@ -156,6 +161,16 @@ export function SubscriptionScreen({ payment.isPollingOrder; const canActivate = selectedPlan !== null && payment.agreed && !isPaymentBusy; + const stripeClientSecret = payment.payParams + ? getStripeClientSecret(payment.payParams) + : null; + const stripeDialogKey = stripeClientSecret + ? `${payment.launchNonce}:${stripeClientSecret}` + : null; + const shouldShowStripeDialog = + stripeClientSecret !== null && + stripeDialogKey !== hiddenStripeDialogKey && + !payment.isPaid; const finishPaymentSuccessClose = () => { setShowPaymentSuccessDialog(false); @@ -169,6 +184,21 @@ export function SubscriptionScreen({ finishPaymentSuccessClose(); }; + const handleStripeClose = () => { + if (stripeDialogKey) { + setHiddenStripeDialogKey(stripeDialogKey); + } + if (payment.orderStatus !== "paid") { + paymentDispatch({ type: "PaymentReset" }); + } + }; + + const handleStripeConfirmed = () => { + if (stripeDialogKey) { + setHiddenStripeDialogKey(stripeDialogKey); + } + }; + useEffect(() => { if (canSubscribeVip) { const firstVipPlanId = vipOfferPlans[0]?.id ?? ""; @@ -212,16 +242,28 @@ export function SubscriptionScreen({ ]); return ( - -
-
- -
+ <> + +
+
+ +
-
- {canSubscribeVip ? ( - + {canSubscribeVip ? ( + + paymentDispatch({ + type: "PaymentPlanSelected", + planId, + }) + } + /> + ) : null} + paymentDispatch({ @@ -230,80 +272,78 @@ export function SubscriptionScreen({ }) } /> - ) : null} - - paymentDispatch({ - type: "PaymentPlanSelected", - planId, - }) - } - /> -
+
-
- - paymentDispatch({ - type: "PaymentPayChannelChanged", - payChannel, - }) - } - /> -
+
+ + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel, + }) + } + /> +
-
- + +
+ +
+ + paymentDispatch({ + type: "PaymentAgreementChanged", + agreed, + }) + } + label={ + + I agree to the{" "} + + VIP Membership Benefits Agreement + {" "} + and{" "} + + Automatic renewal Agreement + + + } + /> +
+ +
+
-
- - paymentDispatch({ - type: "PaymentAgreementChanged", - agreed, - }) - } - label={ - - I agree to the{" "} - - VIP Membership Benefits Agreement - {" "} - and{" "} - - Automatic renewal Agreement - - - } - /> -
- - -
- + ) : null} + ); }