"use client"; import { type Dispatch, useEffect, useRef, useState } from "react"; import { getPaymentUrl, getStripeClientSecret, isEzpayPayment, launchEzpayRedirect, } from "@/lib/payment/payment_launch"; import { behaviorAnalytics } from "@/lib/analytics"; import type { PendingPaymentReturnTo, PendingPaymentSubscriptionType, } from "@/lib/payment/pending_payment_order"; import type { PaymentContextState, } from "@/stores/payment/payment-context"; import type { PaymentEvent } from "@/stores/payment/payment-events"; import { AppEnvUtil } from "@/utils/app-env"; import { Logger } from "@/utils/logger"; const UNSUPPORTED_PAYMENT_PARAMS_MESSAGE = "Payment parameters did not include a supported URL or Stripe client secret."; export interface ShouldShowEzpayConfirmationInput { currentOrderId: string | null; isProduction: boolean; payParams: Record | null; paymentUrl: string | null; } export interface UsePaymentLaunchFlowInput { log: Logger; logScope: string; payment: PaymentContextState; paymentDispatch: Dispatch; returnTo?: PendingPaymentReturnTo; characterSlug?: string; subscriptionType: PendingPaymentSubscriptionType; giftCategory?: string | null; giftPlanId?: string | null; } export interface PaymentLaunchFlow { ezpayPaymentUrl: string | null; handleEzpayCancel: () => void; handleEzpayConfirm: () => void; handleStripeClose: () => void; handleStripeConfirmed: () => void; isConfirmingEzpay: boolean; resetLaunchState: () => void; shouldShowEzpayConfirmDialog: boolean; shouldShowStripeDialog: boolean; stripeClientSecret: string | null; } function trackPaymentCheckoutOpened( payment: PaymentContextState, checkoutUrl: string, ): void { const plan = payment.plans.find( (item) => item.planId === payment.selectedPlanId, ); if (!payment.currentOrderId || !plan) return; behaviorAnalytics.checkoutOpened({ orderId: payment.currentOrderId, plan, payChannel: payment.payChannel, checkoutUrl, }); } function trackPaymentCheckoutFailed( payment: PaymentContextState, reason: "missing_checkout_url" | "payment_redirect_failed", ): void { const plan = payment.plans.find( (item) => item.planId === payment.selectedPlanId, ); if (!payment.currentOrderId || !plan) return; behaviorAnalytics.checkoutFailed({ orderId: payment.currentOrderId, plan, payChannel: payment.payChannel, reason, }); } export function shouldShowEzpayConfirmation({ currentOrderId, isProduction, payParams, paymentUrl, }: ShouldShowEzpayConfirmationInput): boolean { return Boolean( !isProduction && payParams && isEzpayPayment(payParams) && paymentUrl && currentOrderId, ); } export function usePaymentLaunchFlow({ log, logScope, payment, paymentDispatch, returnTo, characterSlug, subscriptionType, giftCategory, giftPlanId, }: UsePaymentLaunchFlowInput): PaymentLaunchFlow { const launchedNonceRef = useRef(0); const [hiddenStripeClientSecret, setHiddenStripeClientSecret] = useState< string | null >(null); const [isConfirmingEzpay, setIsConfirmingEzpay] = useState(false); const stripeClientSecret = payment.payParams ? getStripeClientSecret(payment.payParams) : null; const ezpayPaymentUrl = payment.payParams ? getPaymentUrl(payment.payParams) : null; useEffect(() => { if (!payment.payParams || payment.launchNonce <= launchedNonceRef.current) { return; } launchedNonceRef.current = payment.launchNonce; const clientSecret = getStripeClientSecret(payment.payParams); if (clientSecret) { trackPaymentCheckoutOpened(payment, "stripe_embedded"); return; } const paymentUrl = getPaymentUrl(payment.payParams); if (paymentUrl) { const isEzpay = isEzpayPayment(payment.payParams); if (!AppEnvUtil.isProduction() && isEzpay) { log.debug(`[${logScope}] ezpay confirmation required`, { orderId: payment.currentOrderId, paymentUrl, subscriptionType, }); return; } if (isEzpay) { void launchEzpayRedirect({ orderId: payment.currentOrderId, paymentUrl, subscriptionType, giftCategory, giftPlanId, ...(returnTo ? { returnTo } : {}), ...(characterSlug ? { characterSlug } : {}), onOpened: () => trackPaymentCheckoutOpened(payment, paymentUrl), onFailed: (errorMessage) => { trackPaymentCheckoutFailed(payment, "payment_redirect_failed"); paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }); }, }); return; } try { window.location.href = paymentUrl; trackPaymentCheckoutOpened(payment, paymentUrl); } catch { trackPaymentCheckoutFailed(payment, "payment_redirect_failed"); paymentDispatch({ type: "PaymentLaunchFailed", errorMessage: "Could not open the payment page. Please try again.", }); } return; } trackPaymentCheckoutFailed(payment, "missing_checkout_url"); paymentDispatch({ type: "PaymentLaunchFailed", errorMessage: UNSUPPORTED_PAYMENT_PARAMS_MESSAGE, }); }, [ log, logScope, characterSlug, payment.currentOrderId, payment, payment.launchNonce, payment.payChannel, payment.payParams, payment.plans, payment.selectedPlanId, paymentDispatch, returnTo, subscriptionType, giftCategory, giftPlanId, ]); const shouldShowStripeDialog = stripeClientSecret !== null && stripeClientSecret !== hiddenStripeClientSecret && !payment.isPaid; const shouldShowEzpayConfirmDialog = shouldShowEzpayConfirmation({ currentOrderId: payment.currentOrderId, isProduction: AppEnvUtil.isProduction(), payParams: payment.payParams, paymentUrl: ezpayPaymentUrl, }); function resetLaunchState(): void { setIsConfirmingEzpay(false); setHiddenStripeClientSecret(null); } function handleStripeClose(): void { if (stripeClientSecret) { setHiddenStripeClientSecret(stripeClientSecret); } if (payment.orderStatus !== "paid") { paymentDispatch({ type: "PaymentReset" }); } } function handleStripeConfirmed(): void { if (stripeClientSecret) { setHiddenStripeClientSecret(stripeClientSecret); } } function handleEzpayConfirm(): void { if (!payment.currentOrderId || !ezpayPaymentUrl) return; setIsConfirmingEzpay(true); void launchEzpayRedirect({ orderId: payment.currentOrderId, paymentUrl: ezpayPaymentUrl, subscriptionType, giftCategory, giftPlanId, ...(returnTo ? { returnTo } : {}), ...(characterSlug ? { characterSlug } : {}), onOpened: () => trackPaymentCheckoutOpened(payment, ezpayPaymentUrl), onFailed: (errorMessage) => { trackPaymentCheckoutFailed(payment, "payment_redirect_failed"); setIsConfirmingEzpay(false); paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }); }, }); } function handleEzpayCancel(): void { log.debug(`[${logScope}] ezpay confirmation cancelled`, { orderId: payment.currentOrderId, subscriptionType, }); setIsConfirmingEzpay(false); paymentDispatch({ type: "PaymentReset" }); } return { ezpayPaymentUrl, handleEzpayCancel, handleEzpayConfirm, handleStripeClose, handleStripeConfirmed, isConfirmingEzpay, resetLaunchState, shouldShowEzpayConfirmDialog, shouldShowStripeDialog, stripeClientSecret, }; }