Files
cozsweet-frontend-nextjs/src/app/_hooks/use-payment-launch-flow.ts
T

279 lines
7.5 KiB
TypeScript

"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<string, unknown> | null;
paymentUrl: string | null;
}
export interface UsePaymentLaunchFlowInput {
log: Logger;
logScope: string;
payment: PaymentContextState;
paymentDispatch: Dispatch<PaymentEvent>;
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,
};
}