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();