From 3536045794356f0f4440fed15fcf5766cb567c36 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 28 Jul 2026 19:33:59 +0800 Subject: [PATCH] fix(payment): inherit chat recipient without selector --- e2e/specs/mock/payment/indonesia-qris.spec.ts | 14 +++-- .../__tests__/subscription-page.test.tsx | 2 +- .../subscription-screen-flow.test.tsx | 61 ++++++++++++++----- .../components/subscription-screen.module.css | 46 -------------- src/app/subscription/subscription-screen.tsx | 44 +------------ .../use-subscription-payment-flow.ts | 3 +- .../__tests__/payment-order-flow.test.ts | 37 +++++------ 7 files changed, 78 insertions(+), 129 deletions(-) diff --git a/e2e/specs/mock/payment/indonesia-qris.spec.ts b/e2e/specs/mock/payment/indonesia-qris.spec.ts index efb1c06a..479ec104 100644 --- a/e2e/specs/mock/payment/indonesia-qris.spec.ts +++ b/e2e/specs/mock/payment/indonesia-qris.spec.ts @@ -210,11 +210,13 @@ test("Indonesia VIP defaults to QRIS and completes after status polling", async }) => { const payment = await registerIndonesiaPaymentMocks(page); await prepareIndonesiaUser(page); - await page.goto("/subscription?type=vip"); + await page.goto("/subscription?type=vip&character=elio"); - await expect(page.getByText("Choose who you want to support")).toBeVisible(); - await page.getByRole("button", { name: "Elio", exact: true }).click(); - await expect(page.getByText("Supporting Elio")).toBeVisible(); + await expect(page.getByText("Choose who you want to support")).toHaveCount(0); + await expect(page.getByText("Supporting Elio")).toHaveCount(0); + await expect( + page.getByRole("button", { name: "Elio", exact: true }), + ).toHaveCount(0); await expect(page.getByRole("button", { name: "QRIS" })).toHaveAttribute( "aria-pressed", @@ -255,8 +257,8 @@ test("Indonesia VIP defaults to QRIS and completes after status polling", async test("Indonesia credit top-up uses QRIS display cents", async ({ page }) => { const payment = await registerIndonesiaPaymentMocks(page); await prepareIndonesiaUser(page); - await page.goto("/subscription?type=topup"); - await page.getByRole("button", { name: "Elio", exact: true }).click(); + await page.goto("/subscription?type=topup&character=elio"); + await expect(page.getByText("Supporting Elio")).toHaveCount(0); await expectCheckoutButtonLayout(page); const orderId = await expectQrisOrder( diff --git a/src/app/subscription/__tests__/subscription-page.test.tsx b/src/app/subscription/__tests__/subscription-page.test.tsx index 5607ec99..5cc24563 100644 --- a/src/app/subscription/__tests__/subscription-page.test.tsx +++ b/src/app/subscription/__tests__/subscription-page.test.tsx @@ -34,7 +34,7 @@ describe("SubscriptionPage", () => { ); }); - it("requires an explicit selection when source navigation has no character", async () => { + it("keeps the recipient empty when source navigation has no character", async () => { const page = await SubscriptionPage({ searchParams: Promise.resolve({}), }); diff --git a/src/app/subscription/__tests__/subscription-screen-flow.test.tsx b/src/app/subscription/__tests__/subscription-screen-flow.test.tsx index 6e3e1302..0da25150 100644 --- a/src/app/subscription/__tests__/subscription-screen-flow.test.tsx +++ b/src/app/subscription/__tests__/subscription-screen-flow.test.tsx @@ -219,7 +219,7 @@ describe("SubscriptionScreen payment selection flow", () => { }); it("confirms VIP selection before enabling the separate checkout button", () => { - act(() => root.render()); + act(() => root.render()); const checkout = container.querySelector( '[data-testid="checkout"]', ); @@ -250,7 +250,7 @@ describe("SubscriptionScreen payment selection flow", () => { }); it("keeps the original selection when VIP confirmation is cancelled", () => { - act(() => root.render()); + act(() => root.render()); act(() => clickButton(container, "vip_quarterly")); expect(container.querySelector('[role="dialog"]')).not.toBeNull(); @@ -265,21 +265,21 @@ describe("SubscriptionScreen payment selection flow", () => { }); it("requires VIP confirmation again after the page is remounted", () => { - act(() => root.render()); + act(() => root.render()); act(() => clickButton(container, "vip_monthly")); act(() => clickButton(container, "Confirm")); expect(container.querySelector('[role="dialog"]')).toBeNull(); act(() => root.unmount()); root = createRoot(container); - act(() => root.render()); + act(() => root.render()); act(() => clickButton(container, "vip_monthly")); expect(container.querySelector('[role="dialog"]')).not.toBeNull(); }); it("selects a coin package without showing the renewal dialog", () => { - act(() => root.render()); + act(() => root.render()); act(() => clickButton(container, "coin_1000")); expect(container.querySelector('[role="dialog"]')).toBeNull(); @@ -319,7 +319,7 @@ describe("SubscriptionScreen payment selection flow", () => { promotionType: "first_recharge_half_price", }); - act(() => root.render()); + act(() => root.render()); expect(container.textContent).not.toContain("First Recharge Offer"); expect(container.textContent).not.toContain( @@ -327,7 +327,38 @@ describe("SubscriptionScreen payment selection flow", () => { ); }); - it("requires a support character selection when opened without a character", () => { + it.each(["elio", "maya", "nayeli"])( + "inherits the %s chat character without rendering a recipient selector", + (sourceCharacterSlug) => { + mocks.payment.selectedPlanId = "coin_1000"; + + act(() => + root.render( + , + ), + ); + + const checkout = container.querySelector( + '[data-testid="checkout"]', + ); + expect(container.textContent).not.toContain("Supporting "); + expect(container.textContent).not.toContain( + "Choose who you want to support", + ); + expect(container.textContent).not.toContain( + "Your VIP or credit purchase supports the character you choose", + ); + expect(checkout?.disabled).toBe(false); + expect(mocks.paymentFlowInput).toHaveBeenLastCalledWith( + expect.objectContaining({ sourceCharacterSlug }), + ); + }, + ); + + it("does not invent a recipient when opened without a source character", () => { mocks.payment.selectedPlanId = "coin_1000"; act(() => @@ -342,19 +373,17 @@ describe("SubscriptionScreen payment selection flow", () => { const checkout = container.querySelector( '[data-testid="checkout"]', ); - expect(container.textContent).toContain("Choose who you want to support"); + expect(container.textContent).not.toContain("Supporting "); + expect(container.textContent).not.toContain( + "Choose who you want to support", + ); + expect(container.textContent).not.toContain( + "Your VIP or credit purchase supports the character you choose", + ); expect(checkout?.disabled).toBe(true); expect(mocks.paymentFlowInput).toHaveBeenLastCalledWith( expect.objectContaining({ sourceCharacterSlug: null }), ); - - act(() => clickButton(container, "Maya")); - - expect(container.textContent).toContain("Supporting Maya"); - expect(checkout?.disabled).toBe(false); - expect(mocks.paymentFlowInput).toHaveBeenLastCalledWith( - expect.objectContaining({ sourceCharacterSlug: "maya" }), - ); }); }); diff --git a/src/app/subscription/components/subscription-screen.module.css b/src/app/subscription/components/subscription-screen.module.css index d0609881..82e04f0a 100644 --- a/src/app/subscription/components/subscription-screen.module.css +++ b/src/app/subscription/components/subscription-screen.module.css @@ -61,52 +61,6 @@ box-shadow: 0 12px 30px rgba(22, 101, 52, 0.12); } -.supportCharacterSelector { - margin-top: var(--page-section-gap, 14px); - padding: 14px 16px; - border: 1px solid rgba(246, 87, 160, 0.2); - border-radius: var(--responsive-card-radius-sm, 22px); - background: rgba(255, 255, 255, 0.9); - color: #4a3340; -} - -.supportCharacterSelector h2 { - margin: 0; - color: #24151d; - font-size: var(--responsive-card-title, 17px); -} - -.supportCharacterSelector p { - margin: 6px 0 10px; - font-size: var(--responsive-caption, 13px); - line-height: 1.4; -} - -.supportCharacterOptions { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.supportCharacterOption, -.supportCharacterOptionActive { - border: 1px solid rgba(246, 87, 160, 0.24); - border-radius: 999px; - padding: 7px 12px; - background: #fff; - color: #8e315e; - cursor: pointer; - font: inherit; - font-size: 13px; - font-weight: 800; -} - -.supportCharacterOptionActive { - border-color: #f657a0; - background: #f657a0; - color: #fff; -} - .characterSupportBanner { margin-top: var(--page-section-gap, 14px); padding: 14px 16px; diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index 9ef9861d..135a933e 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -8,10 +8,7 @@ import { usePaymentMethodSelection } from "@/app/_hooks/use-payment-method-selec import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics"; import type { PayChannel } from "@/data/schemas/payment"; import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit"; -import { - DEFAULT_CHARACTER_SLUG, - getCharacterBySlug, -} from "@/data/constants/character"; +import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character"; import { useCharacterCatalog } from "@/providers/character-catalog-provider"; import { behaviorAnalytics, @@ -65,7 +62,7 @@ export function SubscriptionScreen({ returnTo = null, initialPayChannel = null, analyticsContext: providedAnalyticsContext, - sourceCharacterSlug = DEFAULT_CHARACTER_SLUG, + sourceCharacterSlug = null, initialPlanId = null, initialAutoRenew = null, commercialOfferId = null, @@ -81,14 +78,8 @@ export function SubscriptionScreen({ null, ); const characterCatalog = useCharacterCatalog(); - const [selectedSupportCharacterSlug, setSelectedSupportCharacterSlug] = - useState(() => - getCharacterBySlug(sourceCharacterSlug)?.slug ?? null, - ); const userState = useUserState(); - const sourceCharacter = characterCatalog.getBySlug( - selectedSupportCharacterSlug, - ); + const sourceCharacter = characterCatalog.getBySlug(sourceCharacterSlug); const hasHydrated = useHasHydrated(); const countryCode = userState.currentUser?.countryCode; const paymentMethodConfig = getPaymentMethodConfig({ @@ -278,35 +269,6 @@ export function SubscriptionScreen({

) : null} -
-

- {sourceCharacter - ? `Supporting ${sourceCharacter.shortName}` - : "Choose who you want to support"} -

-

- Your VIP or credit purchase supports the character you choose, and - they will thank you after the payment and benefits are confirmed. -

-
- {characterCatalog.characters.map((character) => ( - - ))} -
-
- {chatActionId && sourceCharacter ? (
{ actor.stop(); }); - it("binds a default VIP or credit order to the selected support character", async () => { - const createOrderSpy = vi.fn(); - const actor = createActor( - createTestPaymentMachine({ createOrderSpy }), - ).start(); - actor.send({ type: "PaymentInit", characterId: "maya-tan" }); - await waitFor(actor, (snapshot) => snapshot.matches("ready")); + it.each(["elio", "maya-tan", "nayeli-cervantes"])( + "binds a default VIP or credit order to the source chat character %s", + async (characterId) => { + const createOrderSpy = vi.fn(); + const actor = createActor( + createTestPaymentMachine({ createOrderSpy }), + ).start(); + actor.send({ type: "PaymentInit", characterId }); + await waitFor(actor, (snapshot) => snapshot.matches("ready")); - actor.send({ type: "PaymentCreateOrderSubmitted" }); - await waitFor(actor, (snapshot) => snapshot.matches("paid")); + actor.send({ type: "PaymentCreateOrderSubmitted" }); + await waitFor(actor, (snapshot) => snapshot.matches("paid")); - expect(createOrderSpy).toHaveBeenCalledWith({ - planId: "vip_monthly", - payChannel: "stripe", - autoRenew: true, - recipientCharacterId: "maya-tan", - }); - actor.stop(); - }); + expect(createOrderSpy).toHaveBeenCalledWith({ + planId: "vip_monthly", + payChannel: "stripe", + autoRenew: true, + recipientCharacterId: characterId, + }); + actor.stop(); + }, + ); it("carries the originating chat action into order creation", async () => { const createOrderSpy = vi.fn();