From b22db6d147df4ff5dd6c692c52691495847aabdc Mon Sep 17 00:00:00 2001 From: chenhang Date: Fri, 17 Jul 2026 18:27:24 +0800 Subject: [PATCH] feat(payment): share payment method selector --- .../payment-method-selector.test.tsx | 55 +++++++++ .../payment/payment-method-selector.tsx} | 12 +- .../use-payment-method-selection.test.tsx | 107 ++++++++++++++++++ .../_hooks/use-payment-method-selection.ts | 49 ++++++++ .../subscription-screen.helpers.test.ts | 67 +---------- .../__tests__/tailwind-components.test.tsx | 33 ------ src/app/subscription/components/index.ts | 1 - .../subscription-screen.helpers.ts | 23 +--- src/app/subscription/subscription-screen.tsx | 64 ++++------- src/app/tip/tip-screen.module.css | 5 + src/app/tip/tip-screen.tsx | 41 ++++++- .../payment/__tests__/payment_method.test.ts | 65 +++++++++++ src/lib/payment/payment_method.ts | 37 ++++++ 13 files changed, 386 insertions(+), 173 deletions(-) create mode 100644 src/app/_components/payment/__tests__/payment-method-selector.test.tsx rename src/app/{subscription/components/subscription-payment-method.tsx => _components/payment/payment-method-selector.tsx} (93%) create mode 100644 src/app/_hooks/__tests__/use-payment-method-selection.test.tsx create mode 100644 src/app/_hooks/use-payment-method-selection.ts create mode 100644 src/lib/payment/__tests__/payment_method.test.ts create mode 100644 src/lib/payment/payment_method.ts diff --git a/src/app/_components/payment/__tests__/payment-method-selector.test.tsx b/src/app/_components/payment/__tests__/payment-method-selector.test.tsx new file mode 100644 index 00000000..b8218137 --- /dev/null +++ b/src/app/_components/payment/__tests__/payment-method-selector.test.tsx @@ -0,0 +1,55 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; + +import { PaymentMethodSelector } from "../payment-method-selector"; + +describe("PaymentMethodSelector", () => { + it("orders the default channel first and renders page analytics", () => { + const html = renderToStaticMarkup( + undefined} + />, + ); + + expect(html).toContain('aria-labelledby="payment-method-title"'); + expect(html).toContain("Payment Method"); + expect(html).toContain("GCash by default"); + expect(html.indexOf('aria-label="GCash"')).toBeLessThan( + html.indexOf('aria-label="Stripe"'), + ); + const selectedButton = html.match( + /]*aria-pressed="true"[^>]*>/, + )?.[0]; + expect(selectedButton).toBeDefined(); + expect(selectedButton).toContain( + 'data-analytics-key="tip.payment_method"', + ); + expect(selectedButton).toContain("border-[#f657a0]"); + expect(selectedButton).not.toContain("border-[rgba(246,87,160,0.18)]"); + expect(selectedButton).toContain("bg-[#fff4f9]"); + expect(selectedButton).not.toContain("bg-white"); + expect(selectedButton).toContain("0_0_0_3px_rgba(217,47,127,0.18)"); + expect(selectedButton).toContain("-translate-y-0.5"); + expect(html).toContain("/images/subscription/gcash-logo.svg"); + }); + + it("disables both payment methods while payment is busy", () => { + const html = renderToStaticMarkup( + undefined} + />, + ); + + expect(html.match(/ disabled=""/g)).toHaveLength(2); + expect(html).toContain( + 'data-analytics-key="subscription.payment_method"', + ); + }); +}); diff --git a/src/app/subscription/components/subscription-payment-method.tsx b/src/app/_components/payment/payment-method-selector.tsx similarity index 93% rename from src/app/subscription/components/subscription-payment-method.tsx rename to src/app/_components/payment/payment-method-selector.tsx index 0d4c51ee..5064446c 100644 --- a/src/app/subscription/components/subscription-payment-method.tsx +++ b/src/app/_components/payment/payment-method-selector.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import type { PayChannel } from "@/data/schemas/payment"; -const PAYMENT_METHODS: { +const PAYMENT_METHODS: readonly { channel: PayChannel; title: string; logoSrc?: string; @@ -20,21 +20,23 @@ const PAYMENT_METHODS: { }, ]; -export interface SubscriptionPaymentMethodProps { +export interface PaymentMethodSelectorProps { value: PayChannel; defaultChannel?: PayChannel; disabled?: boolean; caption?: string; + analyticsKey: string; onChange: (channel: PayChannel) => void; } -export function SubscriptionPaymentMethod({ +export function PaymentMethodSelector({ value, defaultChannel = "stripe", disabled = false, caption = "Stripe by default", + analyticsKey, onChange, -}: SubscriptionPaymentMethodProps) { +}: PaymentMethodSelectorProps) { const paymentMethods = defaultChannel === "ezpay" ? [...PAYMENT_METHODS].sort((a, b) => @@ -75,7 +77,7 @@ export function SubscriptionPaymentMethod({