feat(payment): expand Stripe methods and checkout handoff
Docker Image / Build and Push Docker Image (push) Successful in 2m10s

This commit is contained in:
Codex
2026-07-28 16:27:03 +08:00
parent 2e402de15b
commit 59e4eac736
42 changed files with 1151 additions and 92 deletions
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import {
persistExternalEntryPayload,
resolveCheckoutIntentDestination,
resolveExternalEntryDestination,
resolveExternalEntryPromotionType,
resolveExternalEntryTarget,
@@ -22,6 +23,7 @@ import {
import { Logger } from "@/utils/logger";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { consumeTopUpHandoff } from "@/lib/auth/top_up_handoff";
import { consumeCheckoutHandoff } from "@/lib/auth/checkout_handoff";
import { Result } from "@/utils/result";
import {
isFavoriteEntryRequest,
@@ -61,6 +63,9 @@ export default function ExternalEntryPersist({
const [hasPersisted, setHasPersisted] = useState(false);
const [handoffError, setHandoffError] = useState<string | null>(null);
const [handoffCompleted, setHandoffCompleted] = useState(false);
const [checkoutDestination, setCheckoutDestination] = useState<string | null>(
null,
);
const hasNavigatedRef = useRef(false);
const handoffStartedRef = useRef(false);
const targetRoute = resolveExternalEntryTarget({ target });
@@ -71,11 +76,14 @@ export default function ExternalEntryPersist({
mode,
promotionType,
});
const isTopUpHandoff = targetRoute === ROUTES.subscription;
const normalizedTarget = target?.trim().toLowerCase() ?? "";
const isTopUpHandoff = normalizedTarget === "topup";
const isCheckoutHandoff = normalizedTarget === "checkout";
const isLoginHandoff = isTopUpHandoff || isCheckoutHandoff;
const displayedHandoffError =
handoffError ??
(isTopUpHandoff && !hasValue(handoffToken)
? "This top-up link is invalid. Please request a new link in Messenger."
(isLoginHandoff && !hasValue(handoffToken)
? "This checkout link is invalid. Please return and request a new link."
: null);
useEffect(() => {
@@ -128,7 +136,7 @@ export default function ExternalEntryPersist({
useEffect(() => {
if (hasNavigatedRef.current || !hasPersisted) return;
if (isTopUpHandoff) {
if (isLoginHandoff) {
if (!authState.hasInitialized || authState.isLoading) return;
if (handoffCompleted) {
if (
@@ -138,25 +146,60 @@ export default function ExternalEntryPersist({
return;
}
hasNavigatedRef.current = true;
router.replace(destination);
router.replace(checkoutDestination ?? destination);
return;
}
if (handoffStartedRef.current) return;
if (!hasValue(handoffToken)) return;
handoffStartedRef.current = true;
void (async () => {
const result = await consumeTopUpHandoff(handoffToken);
if (Result.isErr(result)) {
log.warn("[ExternalEntryPersist] top-up handoff failed", result.error);
if (isCheckoutHandoff) {
const result = await consumeCheckoutHandoff(handoffToken);
if (Result.isErr(result)) {
log.warn(
"[ExternalEntryPersist] checkout handoff failed",
result.error,
);
window.history.replaceState(
window.history.state,
"",
"/external-entry?target=checkout",
);
setHandoffError(
"This checkout link is invalid, expired, or already used. Please return and request a new link.",
);
return;
}
window.history.replaceState(
window.history.state,
"",
"/external-entry?target=checkout",
);
setCheckoutDestination(
resolveCheckoutIntentDestination(result.data),
);
} else {
const result = await consumeTopUpHandoff(handoffToken);
if (Result.isErr(result)) {
log.warn(
"[ExternalEntryPersist] top-up handoff failed",
result.error,
);
window.history.replaceState(
window.history.state,
"",
"/external-entry?target=topup",
);
setHandoffError(
"This top-up link is invalid or has expired. Please request a new link in Messenger.",
);
return;
}
window.history.replaceState(
window.history.state,
"",
"/external-entry?target=topup",
);
setHandoffError(
"This top-up link is invalid or has expired. Please request a new link in Messenger.",
);
return;
}
setHandoffCompleted(true);
authDispatch({ type: "AuthInit" });
@@ -190,9 +233,12 @@ export default function ExternalEntryPersist({
authState.isLoading,
authState.loginStatus,
destination,
checkoutDestination,
hasPersisted,
handoffCompleted,
handoffToken,
isCheckoutHandoff,
isLoginHandoff,
isTopUpHandoff,
psid,
router,