Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f44db8d8b2 | |||
| 9920f97029 | |||
| 9b90f5c83d | |||
| 1f8bbe9022 | |||
| bc6373f4c0 |
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
|
|||||||
|
|
||||||
import { registerAuthMocks } from "./api/auth";
|
import { registerAuthMocks } from "./api/auth";
|
||||||
import { registerChatMocks } from "./api/chat";
|
import { registerChatMocks } from "./api/chat";
|
||||||
|
import { registerCharacterMocks } from "./api/character";
|
||||||
import { registerPaymentMocks } from "./api/payment";
|
import { registerPaymentMocks } from "./api/payment";
|
||||||
import { registerUserMocks } from "./api/user";
|
import { registerUserMocks } from "./api/user";
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export async function mockCoreApis(page: Page, options: MockCoreApisOptions = {}
|
|||||||
isChatSendTokenExpired: () => chatState.hasExpiredChatSend,
|
isChatSendTokenExpired: () => chatState.hasExpiredChatSend,
|
||||||
psidLoginFlow: chatOptions.psidLoginFlow,
|
psidLoginFlow: chatOptions.psidLoginFlow,
|
||||||
});
|
});
|
||||||
|
await registerCharacterMocks(page);
|
||||||
await registerUserMocks(page);
|
await registerUserMocks(page);
|
||||||
await registerChatMocks(page, chatOptions, chatState);
|
await registerChatMocks(page, chatOptions, chatState);
|
||||||
await registerPaymentMocks(page);
|
await registerPaymentMocks(page);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { apiEnvelope } from "../data/common";
|
||||||
|
import { chatCharactersResponse } from "../data/character";
|
||||||
|
|
||||||
|
export async function registerCharacterMocks(page: Page) {
|
||||||
|
await page.route("**/api/characters**", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(chatCharactersResponse) });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
import { apiEnvelope } from "../data/common";
|
import { apiEnvelope } from "../data/common";
|
||||||
import { chatSendResponse, createChatSendResponse, createPaidImageHistoryResponse, createWeeklyLimitChatSendResponse, emptyChatHistoryResponse, insufficientCreditsUnlockImageResponse, insufficientCreditsUnlockVoiceResponse, paidImageChatSendResponse, paidImageUnlockHistoryResponse, paidVoiceHistoryResponse } from "../data/chat";
|
import { chatSendResponse, createChatSendResponse, createPaidImageHistoryResponse, createWeeklyLimitChatSendResponse, emptyChatHistoryResponse, emptyChatPreviewsResponse, insufficientCreditsUnlockImageResponse, insufficientCreditsUnlockVoiceResponse, paidImageChatSendResponse, paidImageUnlockHistoryResponse, paidVoiceHistoryResponse } from "../data/chat";
|
||||||
|
|
||||||
export interface ChatMockOptions {
|
export interface ChatMockOptions {
|
||||||
chatLimitTriggerAt?: number;
|
chatLimitTriggerAt?: number;
|
||||||
@@ -31,6 +31,10 @@ export async function registerChatMocks(page: Page, options: ChatMockOptions, st
|
|||||||
await route.fulfill({ json: apiEnvelope(response) });
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/chat/previews", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(emptyChatPreviewsResponse) });
|
||||||
|
});
|
||||||
|
|
||||||
await page.route("**/api/chat/send", async (route) => {
|
await page.route("**/api/chat/send", async (route) => {
|
||||||
sendCount += 1;
|
sendCount += 1;
|
||||||
const requestBody = route.request().postDataJSON() as { message?: unknown } | undefined;
|
const requestBody = route.request().postDataJSON() as { message?: unknown } | undefined;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export const chatCharactersResponse = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "elio",
|
||||||
|
displayName: "Elio Silvestri",
|
||||||
|
isActive: true,
|
||||||
|
capabilities: { chat: true, privateContent: true },
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "maya-tan",
|
||||||
|
displayName: "Maya Tan",
|
||||||
|
isActive: true,
|
||||||
|
capabilities: { chat: true, privateContent: true },
|
||||||
|
sortOrder: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nayeli-cervantes",
|
||||||
|
displayName: "Nayeli Cervantes",
|
||||||
|
isActive: true,
|
||||||
|
capabilities: { chat: true, privateContent: true },
|
||||||
|
sortOrder: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultCharacterId: "elio",
|
||||||
|
};
|
||||||
@@ -9,6 +9,8 @@ export const emptyChatHistoryResponse = {
|
|||||||
privateCanViewFree: false,
|
privateCanViewFree: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const emptyChatPreviewsResponse = { items: [] };
|
||||||
|
|
||||||
export const chatSendResponse = {
|
export const chatSendResponse = {
|
||||||
reply: "Hello from the E2E boyfriend.",
|
reply: "Hello from the E2E boyfriend.",
|
||||||
audioUrl: "",
|
audioUrl: "",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const splashStartChatButtonName = "Start Chatting";
|
|||||||
export const defaultCharacterSlug = "elio";
|
export const defaultCharacterSlug = "elio";
|
||||||
export const defaultCharacterSplashPath = `/characters/${defaultCharacterSlug}/splash`;
|
export const defaultCharacterSplashPath = `/characters/${defaultCharacterSlug}/splash`;
|
||||||
export const defaultCharacterChatPath = `/characters/${defaultCharacterSlug}/chat`;
|
export const defaultCharacterChatPath = `/characters/${defaultCharacterSlug}/chat`;
|
||||||
export const defaultCharacterSidebarPath = `/characters/${defaultCharacterSlug}/sidebar`;
|
export const defaultCharacterSidebarPath = `/sidebar?returnTo=%2Fcharacters%2F${defaultCharacterSlug}%2Fchat`;
|
||||||
export const defaultCharacterChatUrl = new RegExp(
|
export const defaultCharacterChatUrl = new RegExp(
|
||||||
`/characters/${defaultCharacterSlug}/chat(?:\\?.*)?$`,
|
`/characters/${defaultCharacterSlug}/chat(?:\\?.*)?$`,
|
||||||
);
|
);
|
||||||
@@ -34,6 +34,8 @@ export async function seedEmailSession(page: Page) {
|
|||||||
await page.goto(defaultCharacterSplashPath);
|
await page.goto(defaultCharacterSplashPath);
|
||||||
await setEmailSessionStorage(page);
|
await setEmailSessionStorage(page);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
// Let the root auth provider hydrate the seeded session before navigation.
|
||||||
|
await page.waitForTimeout(750);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function switchToEmailSignIn(page: Page) {
|
export async function switchToEmailSignIn(page: Page) {
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import {
|
|||||||
clearBrowserState,
|
clearBrowserState,
|
||||||
defaultCharacterSidebarPath,
|
defaultCharacterSidebarPath,
|
||||||
defaultCharacterSplashPath,
|
defaultCharacterSplashPath,
|
||||||
|
defaultCharacterChatUrl,
|
||||||
|
enterChatFromSplash,
|
||||||
getSplashStartChatButton,
|
getSplashStartChatButton,
|
||||||
signInWithEmailAndOpenChat,
|
submitEmailLogin,
|
||||||
|
switchToEmailSignIn,
|
||||||
} from "@e2e/fixtures/test-helpers";
|
} from "@e2e/fixtures/test-helpers";
|
||||||
|
|
||||||
test.beforeEach(async ({ baseURL, context, page }) => {
|
test.beforeEach(async ({ baseURL, context, page }) => {
|
||||||
@@ -17,11 +20,19 @@ test.beforeEach(async ({ baseURL, context, page }) => {
|
|||||||
test("user can log out from the sidebar after email login", async ({
|
test("user can log out from the sidebar after email login", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await signInWithEmailAndOpenChat(page);
|
await enterChatFromSplash(page);
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "Sign up to unlock more features" })
|
||||||
|
.click();
|
||||||
|
await expect(page).toHaveURL(/\/auth(?:\?.*)?$/);
|
||||||
|
await switchToEmailSignIn(page);
|
||||||
|
await submitEmailLogin(page, { expectedUrl: defaultCharacterChatUrl });
|
||||||
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Menu" }).click();
|
await page.getByRole("button", { name: "Menu" }).click();
|
||||||
await expect(page).toHaveURL(new RegExp(`${defaultCharacterSidebarPath}$`));
|
await expect(page).toHaveURL(
|
||||||
|
new RegExp(defaultCharacterSidebarPath.replace("?", "\\?") + "$"),
|
||||||
|
);
|
||||||
|
|
||||||
const logoutRequestPromise = page.waitForRequest("**/api/auth/logout");
|
const logoutRequestPromise = page.waitForRequest("**/api/auth/logout");
|
||||||
await page.getByRole("button", { name: /Log out/i }).click();
|
await page.getByRole("button", { name: /Log out/i }).click();
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { expect, test, type Request } from "@playwright/test";
|
|||||||
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
|
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
|
||||||
import {
|
import {
|
||||||
clearBrowserState,
|
clearBrowserState,
|
||||||
signInWithEmailAndOpenChat,
|
enterChatFromSplash,
|
||||||
|
setEmailSessionStorage,
|
||||||
} from "@e2e/fixtures/test-helpers";
|
} from "@e2e/fixtures/test-helpers";
|
||||||
|
|
||||||
test.beforeEach(async ({ baseURL, context, page }) => {
|
test.beforeEach(async ({ baseURL, context, page }) => {
|
||||||
@@ -23,7 +24,8 @@ test("chat send refreshes an expired token, retries the request, and renders the
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await signInWithEmailAndOpenChat(page);
|
await enterChatFromSplash(page);
|
||||||
|
await setEmailSessionStorage(page);
|
||||||
|
|
||||||
const message = "token refresh retry test";
|
const message = "token refresh retry test";
|
||||||
const messageInput = page.getByRole("textbox", { name: "Message" });
|
const messageInput = page.getByRole("textbox", { name: "Message" });
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ describe("PaymentMethodSelector", () => {
|
|||||||
it("orders the default channel first and renders page analytics", () => {
|
it("orders the default channel first and renders page analytics", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<PaymentMethodSelector
|
<PaymentMethodSelector
|
||||||
|
config={{
|
||||||
|
canChoosePaymentMethod: true,
|
||||||
|
initialPayChannel: "ezpay",
|
||||||
|
showPaymentMethodSelector: true,
|
||||||
|
}}
|
||||||
value="ezpay"
|
value="ezpay"
|
||||||
defaultChannel="ezpay"
|
|
||||||
caption="GCash by default"
|
caption="GCash by default"
|
||||||
analyticsKey="tip.payment_method"
|
analyticsKey="tip.payment_method"
|
||||||
onChange={() => undefined}
|
onChange={() => undefined}
|
||||||
@@ -40,6 +44,11 @@ describe("PaymentMethodSelector", () => {
|
|||||||
it("disables both payment methods while payment is busy", () => {
|
it("disables both payment methods while payment is busy", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<PaymentMethodSelector
|
<PaymentMethodSelector
|
||||||
|
config={{
|
||||||
|
canChoosePaymentMethod: true,
|
||||||
|
initialPayChannel: "stripe",
|
||||||
|
showPaymentMethodSelector: true,
|
||||||
|
}}
|
||||||
value="stripe"
|
value="stripe"
|
||||||
disabled
|
disabled
|
||||||
analyticsKey="subscription.payment_method"
|
analyticsKey="subscription.payment_method"
|
||||||
@@ -52,4 +61,21 @@ describe("PaymentMethodSelector", () => {
|
|||||||
'data-analytics-key="subscription.payment_method"',
|
'data-analytics-key="subscription.payment_method"',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render when payment method selection is unavailable", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<PaymentMethodSelector
|
||||||
|
config={{
|
||||||
|
canChoosePaymentMethod: true,
|
||||||
|
initialPayChannel: "stripe",
|
||||||
|
showPaymentMethodSelector: false,
|
||||||
|
}}
|
||||||
|
value="stripe"
|
||||||
|
analyticsKey="tip.payment_method"
|
||||||
|
onChange={() => undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import type { PayChannel } from "@/data/schemas/payment";
|
import type { PayChannel } from "@/data/schemas/payment";
|
||||||
|
import type { PaymentMethodConfig } from "@/lib/payment/payment_method";
|
||||||
|
|
||||||
const PAYMENT_METHODS: readonly {
|
const PAYMENT_METHODS: readonly {
|
||||||
channel: PayChannel;
|
channel: PayChannel;
|
||||||
@@ -21,24 +22,28 @@ const PAYMENT_METHODS: readonly {
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface PaymentMethodSelectorProps {
|
export interface PaymentMethodSelectorProps {
|
||||||
|
config: PaymentMethodConfig;
|
||||||
value: PayChannel;
|
value: PayChannel;
|
||||||
defaultChannel?: PayChannel;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
|
className?: string;
|
||||||
analyticsKey: string;
|
analyticsKey: string;
|
||||||
onChange: (channel: PayChannel) => void;
|
onChange: (channel: PayChannel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaymentMethodSelector({
|
export function PaymentMethodSelector({
|
||||||
|
config,
|
||||||
value,
|
value,
|
||||||
defaultChannel = "stripe",
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
caption = "Stripe by default",
|
caption = "Stripe by default",
|
||||||
|
className,
|
||||||
analyticsKey,
|
analyticsKey,
|
||||||
onChange,
|
onChange,
|
||||||
}: PaymentMethodSelectorProps) {
|
}: PaymentMethodSelectorProps) {
|
||||||
|
if (!config.showPaymentMethodSelector) return null;
|
||||||
|
|
||||||
const paymentMethods =
|
const paymentMethods =
|
||||||
defaultChannel === "ezpay"
|
config.initialPayChannel === "ezpay"
|
||||||
? [...PAYMENT_METHODS].sort((a, b) =>
|
? [...PAYMENT_METHODS].sort((a, b) =>
|
||||||
a.channel === "ezpay" ? -1 : b.channel === "ezpay" ? 1 : 0,
|
a.channel === "ezpay" ? -1 : b.channel === "ezpay" ? 1 : 0,
|
||||||
)
|
)
|
||||||
@@ -46,7 +51,7 @@ export function PaymentMethodSelector({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="flex flex-col gap-[clamp(8px,1.852vw,10px)]"
|
className={`flex flex-col gap-[clamp(8px,1.852vw,10px)] ${className ?? ""}`.trim()}
|
||||||
aria-labelledby="payment-method-title"
|
aria-labelledby="payment-method-title"
|
||||||
>
|
>
|
||||||
<div className="flex items-baseline justify-between gap-sm px-xs">
|
<div className="flex items-baseline justify-between gap-sm px-xs">
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ describe("usePaymentMethodSelection", () => {
|
|||||||
it("forces Stripe when payment method selection is unavailable", () => {
|
it("forces Stripe when payment method selection is unavailable", () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
renderHarness(root, {
|
renderHarness(root, {
|
||||||
config: { canChoosePaymentMethod: false, initialPayChannel: "stripe" },
|
config: {
|
||||||
|
canChoosePaymentMethod: false,
|
||||||
|
initialPayChannel: "stripe",
|
||||||
|
showPaymentMethodSelector: false,
|
||||||
|
},
|
||||||
currentPayChannel: "ezpay",
|
currentPayChannel: "ezpay",
|
||||||
onChange,
|
onChange,
|
||||||
});
|
});
|
||||||
@@ -40,6 +44,7 @@ describe("usePaymentMethodSelection", () => {
|
|||||||
const config = {
|
const config = {
|
||||||
canChoosePaymentMethod: true,
|
canChoosePaymentMethod: true,
|
||||||
initialPayChannel: "ezpay",
|
initialPayChannel: "ezpay",
|
||||||
|
showPaymentMethodSelector: true,
|
||||||
} as const;
|
} as const;
|
||||||
renderHarness(root, {
|
renderHarness(root, {
|
||||||
config,
|
config,
|
||||||
@@ -59,13 +64,21 @@ describe("usePaymentMethodSelection", () => {
|
|||||||
it("preserves an explicit return channel and pauses while payment is busy", () => {
|
it("preserves an explicit return channel and pauses while payment is busy", () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
renderHarness(root, {
|
renderHarness(root, {
|
||||||
config: { canChoosePaymentMethod: true, initialPayChannel: "ezpay" },
|
config: {
|
||||||
|
canChoosePaymentMethod: true,
|
||||||
|
initialPayChannel: "ezpay",
|
||||||
|
showPaymentMethodSelector: true,
|
||||||
|
},
|
||||||
currentPayChannel: "stripe",
|
currentPayChannel: "stripe",
|
||||||
requestedPayChannel: "stripe",
|
requestedPayChannel: "stripe",
|
||||||
onChange,
|
onChange,
|
||||||
});
|
});
|
||||||
renderHarness(root, {
|
renderHarness(root, {
|
||||||
config: { canChoosePaymentMethod: false, initialPayChannel: "stripe" },
|
config: {
|
||||||
|
canChoosePaymentMethod: false,
|
||||||
|
initialPayChannel: "stripe",
|
||||||
|
showPaymentMethodSelector: false,
|
||||||
|
},
|
||||||
currentPayChannel: "ezpay",
|
currentPayChannel: "ezpay",
|
||||||
isPaymentBusy: true,
|
isPaymentBusy: true,
|
||||||
onChange,
|
onChange,
|
||||||
|
|||||||
@@ -224,18 +224,15 @@ export function SubscriptionScreen({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paymentMethodConfig.canChoosePaymentMethod ? (
|
|
||||||
<section className={styles.paymentMethodSlot}>
|
|
||||||
<PaymentMethodSelector
|
<PaymentMethodSelector
|
||||||
|
config={paymentMethodConfig}
|
||||||
value={payment.payChannel}
|
value={payment.payChannel}
|
||||||
defaultChannel={paymentMethodConfig.initialPayChannel}
|
|
||||||
disabled={isPaymentBusy}
|
disabled={isPaymentBusy}
|
||||||
caption="GCash by default in the Philippines"
|
caption="GCash by default in the Philippines"
|
||||||
|
className={styles.paymentMethodSlot}
|
||||||
analyticsKey="subscription.payment_method"
|
analyticsKey="subscription.payment_method"
|
||||||
onChange={handlePaymentMethodChange}
|
onChange={handlePaymentMethodChange}
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={styles.ctaSlot}>
|
<div className={styles.ctaSlot}>
|
||||||
<SubscriptionCheckoutButton
|
<SubscriptionCheckoutButton
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ const items: readonly TipCoffeeTierItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "medium",
|
type: "medium",
|
||||||
displayName: "Golden Reserve",
|
displayName: "Gilded Heart",
|
||||||
priceLabel: "US$ 9.99",
|
priceLabel: "US$ 9.99",
|
||||||
unavailable: false,
|
unavailable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "large",
|
type: "large",
|
||||||
displayName: "Imperial Grand Cru",
|
displayName: "Crown Blossom",
|
||||||
priceLabel: "US$ 19.99",
|
priceLabel: "US$ 19.99",
|
||||||
unavailable: false,
|
unavailable: false,
|
||||||
},
|
},
|
||||||
@@ -67,8 +67,8 @@ describe("TipCoffeeTierSelector", () => {
|
|||||||
act(() => root.render(<Harness />));
|
act(() => root.render(<Harness />));
|
||||||
|
|
||||||
expect(container.textContent).toContain("Velvet Espresso");
|
expect(container.textContent).toContain("Velvet Espresso");
|
||||||
expect(container.textContent).toContain("Golden Reserve");
|
expect(container.textContent).toContain("Gilded Heart");
|
||||||
expect(container.textContent).toContain("Imperial Grand Cru");
|
expect(container.textContent).toContain("Crown Blossom");
|
||||||
expect(
|
expect(
|
||||||
container.querySelector<HTMLInputElement>('input[value="medium"]')
|
container.querySelector<HTMLInputElement>('input[value="medium"]')
|
||||||
?.checked,
|
?.checked,
|
||||||
|
|||||||
@@ -284,18 +284,15 @@ export function TipScreen({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{paymentMethodConfig.canChoosePaymentMethod ? (
|
|
||||||
<section className={styles.paymentMethodSlot}>
|
|
||||||
<PaymentMethodSelector
|
<PaymentMethodSelector
|
||||||
|
config={paymentMethodConfig}
|
||||||
value={payment.payChannel}
|
value={payment.payChannel}
|
||||||
defaultChannel={paymentMethodConfig.initialPayChannel}
|
|
||||||
disabled={isPaymentBusy}
|
disabled={isPaymentBusy}
|
||||||
caption="GCash by default in the Philippines"
|
caption="GCash by default in the Philippines"
|
||||||
|
className={styles.paymentMethodSlot}
|
||||||
analyticsKey="tip.payment_method"
|
analyticsKey="tip.payment_method"
|
||||||
onChange={handlePaymentMethodChange}
|
onChange={handlePaymentMethodChange}
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showMissingPlan ? (
|
{showMissingPlan ? (
|
||||||
<p className={styles.statusMessage} role="alert">
|
<p className={styles.statusMessage} role="alert">
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ describe("payment method rules", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
canChoosePaymentMethod: false,
|
canChoosePaymentMethod: false,
|
||||||
initialPayChannel: "stripe",
|
initialPayChannel: "stripe",
|
||||||
|
showPaymentMethodSelector: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ describe("payment method rules", () => {
|
|||||||
).toBe("stripe");
|
).toBe("stripe");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows every user and keeps the requested channel outside production", () => {
|
it("keeps test channels but shows the selector only for Philippines", () => {
|
||||||
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "test");
|
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "test");
|
||||||
|
|
||||||
expect(canChoosePayChannel(null)).toBe(true);
|
expect(canChoosePayChannel(null)).toBe(true);
|
||||||
@@ -61,5 +62,27 @@ describe("payment method rules", () => {
|
|||||||
requestedPayChannel: "ezpay",
|
requestedPayChannel: "ezpay",
|
||||||
}),
|
}),
|
||||||
).toBe("ezpay");
|
).toBe("ezpay");
|
||||||
|
expect(
|
||||||
|
getPaymentMethodConfig({
|
||||||
|
countryCode: "HK",
|
||||||
|
requestedPayChannel: "ezpay",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
canChoosePaymentMethod: true,
|
||||||
|
initialPayChannel: "ezpay",
|
||||||
|
showPaymentMethodSelector: false,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getPaymentMethodConfig({
|
||||||
|
countryCode: " ph ",
|
||||||
|
requestedPayChannel: null,
|
||||||
|
}).showPaymentMethodSelector,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
getPaymentMethodConfig({
|
||||||
|
countryCode: undefined,
|
||||||
|
requestedPayChannel: null,
|
||||||
|
}).showPaymentMethodSelector,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import { getDefaultPayChannelForCountryCode } from "./default_pay_channel";
|
|||||||
export interface PaymentMethodConfig {
|
export interface PaymentMethodConfig {
|
||||||
canChoosePaymentMethod: boolean;
|
canChoosePaymentMethod: boolean;
|
||||||
initialPayChannel: PayChannel;
|
initialPayChannel: PayChannel;
|
||||||
|
showPaymentMethodSelector: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canChoosePayChannel(
|
export function canChoosePayChannel(
|
||||||
countryCode: string | null | undefined,
|
countryCode: string | null | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!AppEnvUtil.isProduction()) return true;
|
if (!AppEnvUtil.isProduction()) return true;
|
||||||
return countryCode?.trim().toUpperCase() === "PH";
|
return isPhilippinesCountryCode(countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePayChannel(input: {
|
export function resolvePayChannel(input: {
|
||||||
@@ -33,5 +34,12 @@ export function getPaymentMethodConfig(input: {
|
|||||||
return {
|
return {
|
||||||
canChoosePaymentMethod: canChoosePayChannel(input.countryCode),
|
canChoosePaymentMethod: canChoosePayChannel(input.countryCode),
|
||||||
initialPayChannel: resolvePayChannel(input),
|
initialPayChannel: resolvePayChannel(input),
|
||||||
|
showPaymentMethodSelector: isPhilippinesCountryCode(input.countryCode),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPhilippinesCountryCode(
|
||||||
|
countryCode: string | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return countryCode?.trim().toUpperCase() === "PH";
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ describe("tip coffee configuration", () => {
|
|||||||
});
|
});
|
||||||
expect(getTipCoffeeOption("medium")).toMatchObject({
|
expect(getTipCoffeeOption("medium")).toMatchObject({
|
||||||
amountCents: 999,
|
amountCents: 999,
|
||||||
displayName: "Golden Reserve",
|
displayName: "Gilded Heart",
|
||||||
image: { src: "/images/tip/medium.png", width: 1024, height: 1024 },
|
image: { src: "/images/tip/medium.png", width: 1024, height: 1024 },
|
||||||
planId: "tip_coffee_usd_9_99",
|
planId: "tip_coffee_usd_9_99",
|
||||||
});
|
});
|
||||||
expect(getTipCoffeeOption("large")).toMatchObject({
|
expect(getTipCoffeeOption("large")).toMatchObject({
|
||||||
amountCents: 1999,
|
amountCents: 1999,
|
||||||
displayName: "Imperial Grand Cru",
|
displayName: "Crown Blossom",
|
||||||
image: { src: "/images/tip/large.png", width: 1024, height: 1024 },
|
image: { src: "/images/tip/large.png", width: 1024, height: 1024 },
|
||||||
planId: "tip_coffee_usd_19_99",
|
planId: "tip_coffee_usd_19_99",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const TIP_COFFEE_OPTION_BY_TYPE: Record<TipCoffeeType, TipCoffeeOption> = {
|
|||||||
medium: {
|
medium: {
|
||||||
type: "medium",
|
type: "medium",
|
||||||
amountCents: 999,
|
amountCents: 999,
|
||||||
displayName: "Golden Reserve",
|
displayName: "Gilded Heart",
|
||||||
image: {
|
image: {
|
||||||
src: "/images/tip/medium.png",
|
src: "/images/tip/medium.png",
|
||||||
width: 1024,
|
width: 1024,
|
||||||
@@ -43,7 +43,7 @@ const TIP_COFFEE_OPTION_BY_TYPE: Record<TipCoffeeType, TipCoffeeOption> = {
|
|||||||
large: {
|
large: {
|
||||||
type: "large",
|
type: "large",
|
||||||
amountCents: 1999,
|
amountCents: 1999,
|
||||||
displayName: "Imperial Grand Cru",
|
displayName: "Crown Blossom",
|
||||||
image: {
|
image: {
|
||||||
src: "/images/tip/large.png",
|
src: "/images/tip/large.png",
|
||||||
width: 1024,
|
width: 1024,
|
||||||
|
|||||||
Reference in New Issue
Block a user