refactor(payment): share launch flow
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { shouldShowEzpayConfirmation } from "../use-payment-launch-flow";
|
||||
|
||||
describe("shouldShowEzpayConfirmation", () => {
|
||||
it("shows confirmation for Ezpay in non-production builds", () => {
|
||||
expect(
|
||||
shouldShowEzpayConfirmation({
|
||||
currentOrderId: "order-1",
|
||||
isProduction: false,
|
||||
payParams: { provider: "ezpay" },
|
||||
paymentUrl: "https://pay.example/checkout",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show confirmation in production", () => {
|
||||
expect(
|
||||
shouldShowEzpayConfirmation({
|
||||
currentOrderId: "order-1",
|
||||
isProduction: true,
|
||||
payParams: { provider: "ezpay" },
|
||||
paymentUrl: "https://pay.example/checkout",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show confirmation without an order id", () => {
|
||||
expect(
|
||||
shouldShowEzpayConfirmation({
|
||||
currentOrderId: null,
|
||||
isProduction: false,
|
||||
payParams: { provider: "ezpay" },
|
||||
paymentUrl: "https://pay.example/checkout",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show confirmation for non-Ezpay providers", () => {
|
||||
expect(
|
||||
shouldShowEzpayConfirmation({
|
||||
currentOrderId: "order-1",
|
||||
isProduction: false,
|
||||
payParams: { provider: "stripe" },
|
||||
paymentUrl: "https://pay.example/checkout",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import { type Dispatch, useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
getPaymentUrl,
|
||||
getStripeClientSecret,
|
||||
isEzpayPayment,
|
||||
launchEzpayRedirect,
|
||||
} from "@/lib/payment/payment_launch";
|
||||
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, Logger } from "@/utils";
|
||||
|
||||
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;
|
||||
subscriptionType: PendingPaymentSubscriptionType;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
subscriptionType,
|
||||
}: 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) 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,
|
||||
...(returnTo ? { returnTo } : {}),
|
||||
onFailed: (errorMessage) =>
|
||||
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = paymentUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
paymentDispatch({
|
||||
type: "PaymentLaunchFailed",
|
||||
errorMessage: UNSUPPORTED_PAYMENT_PARAMS_MESSAGE,
|
||||
});
|
||||
}, [
|
||||
log,
|
||||
logScope,
|
||||
payment.currentOrderId,
|
||||
payment.launchNonce,
|
||||
payment.payParams,
|
||||
paymentDispatch,
|
||||
returnTo,
|
||||
subscriptionType,
|
||||
]);
|
||||
|
||||
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,
|
||||
...(returnTo ? { returnTo } : {}),
|
||||
onFailed: (errorMessage) => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user