From 5bec04a9706a07062e2e66b2183535a8d9d9bfd4 Mon Sep 17 00:00:00 2001
From: chenhang
Date: Tue, 14 Jul 2026 18:23:08 +0800
Subject: [PATCH] refactor(payment): share checkout dialogs
---
.../__tests__/payment-launch-dialogs.test.tsx | 52 ++++++++
.../payment}/lazy-stripe-payment-dialog.tsx | 0
.../payment/payment-launch-dialogs.tsx | 118 ++++++++++++++++++
.../payment}/stripe-payment-dialog.styles.ts | 0
.../payment}/stripe-payment-dialog.tsx | 0
.../__tests__/tailwind-components.test.tsx | 3 +-
.../subscription-checkout-button.tsx | 80 ++----------
src/app/tip/tip-checkout-button.tsx | 82 ++----------
8 files changed, 187 insertions(+), 148 deletions(-)
create mode 100644 src/app/_components/payment/__tests__/payment-launch-dialogs.test.tsx
rename src/app/{subscription/components => _components/payment}/lazy-stripe-payment-dialog.tsx (100%)
create mode 100644 src/app/_components/payment/payment-launch-dialogs.tsx
rename src/app/{subscription/components => _components/payment}/stripe-payment-dialog.styles.ts (100%)
rename src/app/{subscription/components => _components/payment}/stripe-payment-dialog.tsx (100%)
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}
-
-
-
-
-
-
-
- );
-}