diff --git a/src/app/_components/payment/__tests__/payment-launch-dialogs.test.tsx b/src/app/_components/payment/__tests__/payment-launch-dialogs.test.tsx new file mode 100644 index 00000000..55be9ac1 --- /dev/null +++ b/src/app/_components/payment/__tests__/payment-launch-dialogs.test.tsx @@ -0,0 +1,52 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vitest"; + +import { PaymentLaunchDialogs } from "../payment-launch-dialogs"; + +const hiddenLaunch = { + handleEzpayCancel: vi.fn(), + handleEzpayConfirm: vi.fn(), + handleStripeClose: vi.fn(), + handleStripeConfirmed: vi.fn(), + isConfirmingEzpay: false, + shouldShowEzpayConfirmDialog: false, + shouldShowStripeDialog: false, + stripeClientSecret: null, +}; + +describe("PaymentLaunchDialogs", () => { + it("renders nothing when neither payment dialog is active", () => { + expect( + renderToStaticMarkup( + , + ), + ).toBe(""); + }); + + it("renders the shared Ezpay confirmation content", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain('role="alertdialog"'); + expect(html).toContain("Continue to GCash?"); + expect(html).toContain("Your coffee order is ready."); + expect(html).toContain("Order No. order-123"); + expect(html).toContain('data-analytics-key="tip.external_checkout"'); + expect(html).toContain("Opening..."); + }); +}); diff --git a/src/app/subscription/components/lazy-stripe-payment-dialog.tsx b/src/app/_components/payment/lazy-stripe-payment-dialog.tsx similarity index 100% rename from src/app/subscription/components/lazy-stripe-payment-dialog.tsx rename to src/app/_components/payment/lazy-stripe-payment-dialog.tsx diff --git a/src/app/_components/payment/payment-launch-dialogs.tsx b/src/app/_components/payment/payment-launch-dialogs.tsx new file mode 100644 index 00000000..086f3fa8 --- /dev/null +++ b/src/app/_components/payment/payment-launch-dialogs.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useId, type ReactNode } from "react"; + +import type { PaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow"; + +import { LazyStripePaymentDialog } from "./lazy-stripe-payment-dialog"; +import { stripePaymentDialogStyles as styles } from "./stripe-payment-dialog.styles"; + +type PaymentLaunchDialogFlow = Pick< + PaymentLaunchFlow, + | "handleEzpayCancel" + | "handleEzpayConfirm" + | "handleStripeClose" + | "handleStripeConfirmed" + | "isConfirmingEzpay" + | "shouldShowEzpayConfirmDialog" + | "shouldShowStripeDialog" + | "stripeClientSecret" +>; + +export interface PaymentLaunchDialogsProps { + currentOrderId: string | null; + externalCheckoutAnalyticsKey: string; + ezpayDescription: ReactNode; + launch: PaymentLaunchDialogFlow; + stripeReturnPath?: string; +} + +export function PaymentLaunchDialogs({ + currentOrderId, + externalCheckoutAnalyticsKey, + ezpayDescription, + launch, + stripeReturnPath, +}: PaymentLaunchDialogsProps) { + return ( + <> + {launch.shouldShowEzpayConfirmDialog ? ( + + ) : null} + {launch.shouldShowStripeDialog && launch.stripeClientSecret ? ( + + ) : null} + + ); +} + +interface EzpayRedirectConfirmDialogProps { + description: ReactNode; + externalCheckoutAnalyticsKey: string; + isConfirming: boolean; + onCancel: () => void; + onConfirm: () => void; + orderId: string; +} + +function EzpayRedirectConfirmDialog({ + description, + externalCheckoutAnalyticsKey, + isConfirming, + onCancel, + onConfirm, + orderId, +}: EzpayRedirectConfirmDialogProps) { + const titleId = useId(); + + return ( +
+
+
+

+ Continue to GCash? +

+

{description}

+

Order No. {orderId}

+
+
+ + +
+
+
+ ); +} diff --git a/src/app/subscription/components/stripe-payment-dialog.styles.ts b/src/app/_components/payment/stripe-payment-dialog.styles.ts similarity index 100% rename from src/app/subscription/components/stripe-payment-dialog.styles.ts rename to src/app/_components/payment/stripe-payment-dialog.styles.ts diff --git a/src/app/subscription/components/stripe-payment-dialog.tsx b/src/app/_components/payment/stripe-payment-dialog.tsx similarity index 100% rename from src/app/subscription/components/stripe-payment-dialog.tsx rename to src/app/_components/payment/stripe-payment-dialog.tsx diff --git a/src/app/subscription/components/__tests__/tailwind-components.test.tsx b/src/app/subscription/components/__tests__/tailwind-components.test.tsx index 6cf27cd5..3bcbcbb3 100644 --- a/src/app/subscription/components/__tests__/tailwind-components.test.tsx +++ b/src/app/subscription/components/__tests__/tailwind-components.test.tsx @@ -1,10 +1,11 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; +import { StripePaymentDialog } from "@/app/_components/payment/stripe-payment-dialog"; + import { SubscriptionCtaButton } from "../subscription-cta-button"; import { SubscriptionPaymentMethod } from "../subscription-payment-method"; import { SubscriptionPaymentSuccessDialog } from "../subscription-payment-success-dialog"; -import { StripePaymentDialog } from "../stripe-payment-dialog"; describe("subscription Tailwind components", () => { it("renders SubscriptionCtaButton with loading state", () => { diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index 12a57419..7e2cff5f 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -5,14 +5,13 @@ * 包装 `SubscriptionCtaButton` —— 通过 payment 状态机创建后端订单并拉起支付。 */ import { usePaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow"; +import { PaymentLaunchDialogs } from "@/app/_components/payment/payment-launch-dialogs"; import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; import { Logger } from "@/utils/logger"; -import { LazyStripePaymentDialog } from "./lazy-stripe-payment-dialog"; -import { stripePaymentDialogStyles as dialogStyles } from "./stripe-payment-dialog.styles"; import { SubscriptionCtaButton } from "./subscription-cta-button"; const log = new Logger("SubscriptionCheckoutButton"); @@ -88,77 +87,12 @@ export function SubscriptionCheckoutButton({ {payment.errorMessage}

) : null} - {paymentLaunch.shouldShowEzpayConfirmDialog ? ( - - ) : null} - {paymentLaunch.shouldShowStripeDialog && paymentLaunch.stripeClientSecret ? ( - - ) : null} + ); } - -interface EzpayRedirectConfirmDialogProps { - orderId: string; - isConfirming: boolean; - onCancel: () => void; - onConfirm: () => void; -} - -function EzpayRedirectConfirmDialog({ - orderId, - isConfirming, - onCancel, - onConfirm, -}: EzpayRedirectConfirmDialogProps) { - return ( -
-
-
-

- Continue to GCash? -

-

- Your order has been created. Continue to GCash to finish the - payment. -

-

Order No. {orderId}

-
-
- - -
-
-
- ); -} diff --git a/src/app/tip/tip-checkout-button.tsx b/src/app/tip/tip-checkout-button.tsx index 3d9c12e5..391a4410 100644 --- a/src/app/tip/tip-checkout-button.tsx +++ b/src/app/tip/tip-checkout-button.tsx @@ -1,6 +1,7 @@ "use client"; import { usePaymentLaunchFlow } from "@/app/_hooks/use-payment-launch-flow"; +import { PaymentLaunchDialogs } from "@/app/_components/payment/payment-launch-dialogs"; import type { TipCoffeeType } from "@/lib/tip/tip_coffee"; import { usePaymentDispatch, @@ -8,8 +9,6 @@ import { } from "@/stores/payment/payment-context"; import { Logger } from "@/utils/logger"; -import { LazyStripePaymentDialog } from "../subscription/components/lazy-stripe-payment-dialog"; -import { stripePaymentDialogStyles as dialogStyles } from "../subscription/components/stripe-payment-dialog.styles"; import styles from "./tip-screen.module.css"; const log = new Logger("TipCheckoutButton"); @@ -67,78 +66,13 @@ export function TipCheckoutButton({ {payment.errorMessage}

) : null} - {paymentLaunch.shouldShowEzpayConfirmDialog ? ( - - ) : null} - {paymentLaunch.shouldShowStripeDialog && paymentLaunch.stripeClientSecret ? ( - - ) : null} + ); } - -interface EzpayRedirectConfirmDialogProps { - orderId: string; - isConfirming: boolean; - onCancel: () => void; - onConfirm: () => void; -} - -function EzpayRedirectConfirmDialog({ - orderId, - isConfirming, - onCancel, - onConfirm, -}: EzpayRedirectConfirmDialogProps) { - return ( -
-
-
-

- Continue to GCash? -

-

- Your coffee order is ready. Continue to GCash to finish the - payment. -

-

Order No. {orderId}

-
-
- - -
-
-
- ); -}