fix(subscription): render stripe dialog at screen root
This commit is contained in:
@@ -4,8 +4,7 @@
|
|||||||
*
|
*
|
||||||
* 后端当前返回 PaymentIntent clientSecret,因此这里直接用它完成前端确认。
|
* 后端当前返回 PaymentIntent clientSecret,因此这里直接用它完成前端确认。
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import {
|
import {
|
||||||
Elements,
|
Elements,
|
||||||
PaymentElement,
|
PaymentElement,
|
||||||
@@ -36,24 +35,8 @@ export function StripePaymentDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
onConfirmed,
|
onConfirmed,
|
||||||
}: StripePaymentDialogProps) {
|
}: StripePaymentDialogProps) {
|
||||||
useEffect(() => {
|
if (!stripePromise) {
|
||||||
if (typeof document === "undefined") return;
|
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 ? (
|
|
||||||
<div className={styles.overlay} role="alertdialog" aria-modal="true">
|
<div className={styles.overlay} role="alertdialog" aria-modal="true">
|
||||||
<div className={styles.dialog}>
|
<div className={styles.dialog}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -73,7 +56,10 @@ export function StripePaymentDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.overlay}
|
className={styles.overlay}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -109,10 +95,6 @@ export function StripePaymentDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof document === "undefined") return null;
|
|
||||||
|
|
||||||
return createPortal(dialog, document.body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StripePaymentForm({
|
function StripePaymentForm({
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
} from "@/stores/payment/payment-context";
|
} from "@/stores/payment/payment-context";
|
||||||
import { AppEnvUtil, Logger, Result } from "@/utils";
|
import { AppEnvUtil, Logger, Result } from "@/utils";
|
||||||
|
|
||||||
import { StripePaymentDialog } from "./stripe-payment-dialog";
|
|
||||||
import dialogStyles from "./stripe-payment-dialog.module.css";
|
import dialogStyles from "./stripe-payment-dialog.module.css";
|
||||||
import { SubscriptionCtaButton } from "./subscription-cta-button";
|
import { SubscriptionCtaButton } from "./subscription-cta-button";
|
||||||
import styles from "./subscription-cta-button.module.css";
|
import styles from "./subscription-cta-button.module.css";
|
||||||
@@ -35,9 +34,6 @@ export function SubscriptionCheckoutButton({
|
|||||||
const payment = usePaymentState();
|
const payment = usePaymentState();
|
||||||
const paymentDispatch = usePaymentDispatch();
|
const paymentDispatch = usePaymentDispatch();
|
||||||
const launchedNonceRef = useRef(0);
|
const launchedNonceRef = useRef(0);
|
||||||
const [hiddenStripeClientSecret, setHiddenStripeClientSecret] = useState<
|
|
||||||
string | null
|
|
||||||
>(null);
|
|
||||||
const [isConfirmingEzpay, setIsConfirmingEzpay] = useState(false);
|
const [isConfirmingEzpay, setIsConfirmingEzpay] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,7 +94,7 @@ export function SubscriptionCheckoutButton({
|
|||||||
payment.isCreatingOrder ||
|
payment.isCreatingOrder ||
|
||||||
payment.isPollingOrder;
|
payment.isPollingOrder;
|
||||||
const readyLabel =
|
const readyLabel =
|
||||||
subscriptionType === "topup" ? "Pay and Top Up" : "Pay and Activiate";
|
subscriptionType === "topup" ? "Pay and Top Up" : "Pay and Activate";
|
||||||
const agreementLabel =
|
const agreementLabel =
|
||||||
subscriptionType === "topup"
|
subscriptionType === "topup"
|
||||||
? "Confirm the agreement and Top Up"
|
? "Confirm the agreement and Top Up"
|
||||||
@@ -114,32 +110,9 @@ export function SubscriptionCheckoutButton({
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (disabled || isLoading) return;
|
if (disabled || isLoading) return;
|
||||||
setIsConfirmingEzpay(false);
|
setIsConfirmingEzpay(false);
|
||||||
setHiddenStripeClientSecret(null);
|
|
||||||
paymentDispatch({ type: "PaymentCreateOrderSubmitted" });
|
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
|
const ezpayPaymentUrl = payment.payParams
|
||||||
? getPaymentUrl(payment.payParams)
|
? getPaymentUrl(payment.payParams)
|
||||||
: null;
|
: null;
|
||||||
@@ -208,13 +181,6 @@ export function SubscriptionCheckoutButton({
|
|||||||
onConfirm={handleEzpayConfirm}
|
onConfirm={handleEzpayConfirm}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{shouldShowStripeDialog ? (
|
|
||||||
<StripePaymentDialog
|
|
||||||
clientSecret={stripeClientSecret}
|
|
||||||
onClose={handleStripeClose}
|
|
||||||
onConfirmed={handleStripeConfirmed}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -366,7 +332,7 @@ function EzpayRedirectConfirmDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStripeClientSecret(
|
export function getStripeClientSecret(
|
||||||
payParams: Record<string, unknown>,
|
payParams: Record<string, unknown>,
|
||||||
): string | null {
|
): string | null {
|
||||||
const provider = payParams.provider;
|
const provider = payParams.provider;
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import { useUserDispatch } from "@/stores/user/user-context";
|
|||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getStripeClientSecret,
|
||||||
SubscriptionBackLink,
|
SubscriptionBackLink,
|
||||||
SubscriptionCheckoutButton,
|
SubscriptionCheckoutButton,
|
||||||
SubscriptionCoinsOfferSection,
|
SubscriptionCoinsOfferSection,
|
||||||
SubscriptionPaymentMethod,
|
SubscriptionPaymentMethod,
|
||||||
SubscriptionPaymentSuccessDialog,
|
SubscriptionPaymentSuccessDialog,
|
||||||
SubscriptionVipOfferSection,
|
SubscriptionVipOfferSection,
|
||||||
|
StripePaymentDialog,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import styles from "./components/subscription-screen.module.css";
|
import styles from "./components/subscription-screen.module.css";
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +51,9 @@ export function SubscriptionScreen({
|
|||||||
const refreshedPaidOrderRef = useRef<string | null>(null);
|
const refreshedPaidOrderRef = useRef<string | null>(null);
|
||||||
const resumedPendingOrderRef = useRef<string | null>(null);
|
const resumedPendingOrderRef = useRef<string | null>(null);
|
||||||
const successDialogShownOrderRef = useRef<string | null>(null);
|
const successDialogShownOrderRef = useRef<string | null>(null);
|
||||||
|
const [hiddenStripeDialogKey, setHiddenStripeDialogKey] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
|
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const canSubscribeVip = subscriptionType === "vip";
|
const canSubscribeVip = subscriptionType === "vip";
|
||||||
@@ -156,6 +161,16 @@ export function SubscriptionScreen({
|
|||||||
payment.isPollingOrder;
|
payment.isPollingOrder;
|
||||||
const canActivate =
|
const canActivate =
|
||||||
selectedPlan !== null && payment.agreed && !isPaymentBusy;
|
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 = () => {
|
const finishPaymentSuccessClose = () => {
|
||||||
setShowPaymentSuccessDialog(false);
|
setShowPaymentSuccessDialog(false);
|
||||||
@@ -169,6 +184,21 @@ export function SubscriptionScreen({
|
|||||||
finishPaymentSuccessClose();
|
finishPaymentSuccessClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStripeClose = () => {
|
||||||
|
if (stripeDialogKey) {
|
||||||
|
setHiddenStripeDialogKey(stripeDialogKey);
|
||||||
|
}
|
||||||
|
if (payment.orderStatus !== "paid") {
|
||||||
|
paymentDispatch({ type: "PaymentReset" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStripeConfirmed = () => {
|
||||||
|
if (stripeDialogKey) {
|
||||||
|
setHiddenStripeDialogKey(stripeDialogKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canSubscribeVip) {
|
if (canSubscribeVip) {
|
||||||
const firstVipPlanId = vipOfferPlans[0]?.id ?? "";
|
const firstVipPlanId = vipOfferPlans[0]?.id ?? "";
|
||||||
@@ -212,6 +242,7 @@ export function SubscriptionScreen({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
<div className={styles.shell}>
|
<div className={styles.shell}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
@@ -305,5 +336,14 @@ export function SubscriptionScreen({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
|
|
||||||
|
{shouldShowStripeDialog ? (
|
||||||
|
<StripePaymentDialog
|
||||||
|
clientSecret={stripeClientSecret}
|
||||||
|
onClose={handleStripeClose}
|
||||||
|
onConfirmed={handleStripeConfirmed}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user