Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8abe88d6d |
@@ -1,104 +0,0 @@
|
|||||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import {
|
|
||||||
defaultCharacterChatUrl,
|
|
||||||
dismissChatInterruptions,
|
|
||||||
enterChatFromSplash,
|
|
||||||
} from "@e2e/fixtures/test-helpers";
|
|
||||||
|
|
||||||
const preReleaseHost = "frontend-test.banlv-ai.com";
|
|
||||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "";
|
|
||||||
|
|
||||||
test.describe("pre-release multi-message lock types", () => {
|
|
||||||
test.setTimeout(120_000);
|
|
||||||
test.skip(
|
|
||||||
!baseURL.includes(preReleaseHost),
|
|
||||||
`Run this test only with PLAYWRIGHT_BASE_URL pointing at ${preReleaseHost}.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
test("returns locked image, locked voice, then unlocked text", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await enterChatFromSplash(page, {
|
|
||||||
expectedUrl: defaultCharacterChatUrl,
|
|
||||||
timeout: 20_000,
|
|
||||||
waitUntil: "domcontentloaded",
|
|
||||||
});
|
|
||||||
await dismissChatInterruptions(page);
|
|
||||||
|
|
||||||
const messageInput = page.getByRole("textbox", { name: "Message" });
|
|
||||||
await expect(messageInput).toBeEnabled({ timeout: 30_000 });
|
|
||||||
|
|
||||||
let aiMessageCount = await page
|
|
||||||
.locator('[aria-label="AI message"]')
|
|
||||||
.count();
|
|
||||||
|
|
||||||
const imageReply = await sendMessageAndWaitForAiReply(
|
|
||||||
page,
|
|
||||||
messageInput,
|
|
||||||
"发个图片",
|
|
||||||
aiMessageCount,
|
|
||||||
);
|
|
||||||
aiMessageCount += 1;
|
|
||||||
await expect(
|
|
||||||
imageReply.getByRole("button", { name: "Open image in fullscreen" }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
imageReply
|
|
||||||
.getByRole("button", { name: "Open image in fullscreen" })
|
|
||||||
.locator("img"),
|
|
||||||
).toHaveClass(/blur-sm/);
|
|
||||||
|
|
||||||
const voiceReply = await sendMessageAndWaitForAiReply(
|
|
||||||
page,
|
|
||||||
messageInput,
|
|
||||||
"发送语音",
|
|
||||||
aiMessageCount,
|
|
||||||
);
|
|
||||||
aiMessageCount += 1;
|
|
||||||
await expect(
|
|
||||||
voiceReply.getByRole("button", { name: "Unlock voice message" }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
voiceReply.getByRole("button", { name: "Play voice message" }),
|
|
||||||
).toBeDisabled();
|
|
||||||
|
|
||||||
const textReply = await sendMessageAndWaitForAiReply(
|
|
||||||
page,
|
|
||||||
messageInput,
|
|
||||||
"你好",
|
|
||||||
aiMessageCount,
|
|
||||||
);
|
|
||||||
await expect(
|
|
||||||
textReply.getByRole("button", { name: "Unlock voice message" }),
|
|
||||||
).toHaveCount(0);
|
|
||||||
await expect(
|
|
||||||
textReply.getByRole("group", { name: "Locked private image" }),
|
|
||||||
).toHaveCount(0);
|
|
||||||
await expect(textReply.getByRole("button", { name: /Unlock/ })).toHaveCount(0);
|
|
||||||
await expect(textReply).toContainText(/\S/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function sendMessageAndWaitForAiReply(
|
|
||||||
page: Page,
|
|
||||||
messageInput: Locator,
|
|
||||||
message: string,
|
|
||||||
previousAiMessageCount: number,
|
|
||||||
): Promise<Locator> {
|
|
||||||
const aiMessages = page.locator('[aria-label="AI message"]');
|
|
||||||
const sendRequestPromise = page.waitForRequest("**/api/chat/send");
|
|
||||||
|
|
||||||
await messageInput.fill(message);
|
|
||||||
await messageInput.press("Enter");
|
|
||||||
|
|
||||||
const sendRequest = await sendRequestPromise;
|
|
||||||
expect(sendRequest.postDataJSON()).toMatchObject({ message });
|
|
||||||
|
|
||||||
await expect
|
|
||||||
.poll(() => aiMessages.count(), { timeout: 60_000 })
|
|
||||||
.toBeGreaterThan(previousAiMessageCount);
|
|
||||||
const latestReply = aiMessages.last();
|
|
||||||
await expect(latestReply).toBeVisible();
|
|
||||||
return latestReply;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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(
|
|
||||||
<PaymentMethodSelector
|
|
||||||
value="ezpay"
|
|
||||||
defaultChannel="ezpay"
|
|
||||||
caption="GCash by default"
|
|
||||||
analyticsKey="tip.payment_method"
|
|
||||||
onChange={() => 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(
|
|
||||||
/<button[^>]*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(
|
|
||||||
<PaymentMethodSelector
|
|
||||||
value="stripe"
|
|
||||||
disabled
|
|
||||||
analyticsKey="subscription.payment_method"
|
|
||||||
onChange={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(html.match(/ disabled=""/g)).toHaveLength(2);
|
|
||||||
expect(html).toContain(
|
|
||||||
'data-analytics-key="subscription.payment_method"',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { act } from "react";
|
|
||||||
import { createRoot, type Root } from "react-dom/client";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import type { PayChannel } from "@/data/schemas/payment";
|
|
||||||
import type { PaymentMethodConfig } from "@/lib/payment/payment_method";
|
|
||||||
|
|
||||||
import { usePaymentMethodSelection } from "../use-payment-method-selection";
|
|
||||||
|
|
||||||
describe("usePaymentMethodSelection", () => {
|
|
||||||
let container: HTMLDivElement;
|
|
||||||
let root: Root;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
|
||||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
|
||||||
container = document.createElement("div");
|
|
||||||
document.body.appendChild(container);
|
|
||||||
root = createRoot(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
act(() => root.unmount());
|
|
||||||
container.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("forces Stripe when payment method selection is unavailable", () => {
|
|
||||||
const onChange = vi.fn();
|
|
||||||
renderHarness(root, {
|
|
||||||
config: { canChoosePaymentMethod: false, initialPayChannel: "stripe" },
|
|
||||||
currentPayChannel: "ezpay",
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(onChange).toHaveBeenCalledWith("stripe");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies the default channel once when no return channel was requested", () => {
|
|
||||||
const onChange = vi.fn();
|
|
||||||
const config = {
|
|
||||||
canChoosePaymentMethod: true,
|
|
||||||
initialPayChannel: "ezpay",
|
|
||||||
} as const;
|
|
||||||
renderHarness(root, {
|
|
||||||
config,
|
|
||||||
currentPayChannel: "stripe",
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
renderHarness(root, {
|
|
||||||
config,
|
|
||||||
currentPayChannel: "ezpay",
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(onChange).toHaveBeenCalledTimes(1);
|
|
||||||
expect(onChange).toHaveBeenCalledWith("ezpay");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves an explicit return channel and pauses while payment is busy", () => {
|
|
||||||
const onChange = vi.fn();
|
|
||||||
renderHarness(root, {
|
|
||||||
config: { canChoosePaymentMethod: true, initialPayChannel: "ezpay" },
|
|
||||||
currentPayChannel: "stripe",
|
|
||||||
requestedPayChannel: "stripe",
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
renderHarness(root, {
|
|
||||||
config: { canChoosePaymentMethod: false, initialPayChannel: "stripe" },
|
|
||||||
currentPayChannel: "ezpay",
|
|
||||||
isPaymentBusy: true,
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(onChange).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
interface HarnessProps {
|
|
||||||
config: PaymentMethodConfig;
|
|
||||||
currentPayChannel: PayChannel;
|
|
||||||
requestedPayChannel?: PayChannel | null;
|
|
||||||
isPaymentBusy?: boolean;
|
|
||||||
onChange: (payChannel: PayChannel) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHarness(root: Root, props: HarnessProps): void {
|
|
||||||
act(() => {
|
|
||||||
root.render(<Harness {...props} />);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function Harness({
|
|
||||||
config,
|
|
||||||
currentPayChannel,
|
|
||||||
requestedPayChannel = null,
|
|
||||||
isPaymentBusy = false,
|
|
||||||
onChange,
|
|
||||||
}: HarnessProps) {
|
|
||||||
usePaymentMethodSelection({
|
|
||||||
config,
|
|
||||||
currentPayChannel,
|
|
||||||
requestedPayChannel,
|
|
||||||
isPaymentBusy,
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
import type { PayChannel } from "@/data/schemas/payment";
|
|
||||||
import type { PaymentMethodConfig } from "@/lib/payment/payment_method";
|
|
||||||
|
|
||||||
export interface UsePaymentMethodSelectionInput {
|
|
||||||
config: PaymentMethodConfig;
|
|
||||||
currentPayChannel: PayChannel;
|
|
||||||
isPaymentBusy: boolean;
|
|
||||||
requestedPayChannel: PayChannel | null;
|
|
||||||
onChange: (payChannel: PayChannel) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePaymentMethodSelection({
|
|
||||||
config,
|
|
||||||
currentPayChannel,
|
|
||||||
isPaymentBusy,
|
|
||||||
requestedPayChannel,
|
|
||||||
onChange,
|
|
||||||
}: UsePaymentMethodSelectionInput): void {
|
|
||||||
const appliedDefaultChannelRef = useRef<PayChannel | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPaymentBusy) return;
|
|
||||||
|
|
||||||
if (!config.canChoosePaymentMethod) {
|
|
||||||
appliedDefaultChannelRef.current = null;
|
|
||||||
if (currentPayChannel !== "stripe") onChange("stripe");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestedPayChannel !== null) return;
|
|
||||||
if (appliedDefaultChannelRef.current === config.initialPayChannel) return;
|
|
||||||
appliedDefaultChannelRef.current = config.initialPayChannel;
|
|
||||||
|
|
||||||
if (currentPayChannel !== config.initialPayChannel) {
|
|
||||||
onChange(config.initialPayChannel);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
config.canChoosePaymentMethod,
|
|
||||||
config.initialPayChannel,
|
|
||||||
currentPayChannel,
|
|
||||||
isPaymentBusy,
|
|
||||||
onChange,
|
|
||||||
requestedPayChannel,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -187,7 +187,6 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(guestHtml).toContain('href="/characters/elio/splash"');
|
expect(guestHtml).toContain('href="/characters/elio/splash"');
|
||||||
expect(guestHtml).toContain('aria-label="Back to home"');
|
expect(guestHtml).toContain('aria-label="Back to home"');
|
||||||
expect(guestHtml).toContain('data-analytics-key="chat.back_to_home"');
|
expect(guestHtml).toContain('data-analytics-key="chat.back_to_home"');
|
||||||
expect(guestHtml).toContain("px-(--chat-inline-padding,16px)");
|
|
||||||
expect(guestHtml).not.toContain('aria-label="Menu"');
|
expect(guestHtml).not.toContain('aria-label="Menu"');
|
||||||
expect(memberHtml).toContain("Offer");
|
expect(memberHtml).toContain("Offer");
|
||||||
expect(memberHtml).toContain('href="/characters/elio/splash"');
|
expect(memberHtml).toContain('href="/characters/elio/splash"');
|
||||||
@@ -195,8 +194,6 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(memberHtml).toContain('aria-label="Menu"');
|
expect(memberHtml).toContain('aria-label="Menu"');
|
||||||
expect(memberHtml).toContain('data-analytics-key="chat.open_menu"');
|
expect(memberHtml).toContain('data-analytics-key="chat.open_menu"');
|
||||||
expect(memberHtml).toContain('data-analytics-key="chat.back_to_home"');
|
expect(memberHtml).toContain('data-analytics-key="chat.back_to_home"');
|
||||||
expect(memberHtml).toContain("px-(--chat-inline-padding,16px)");
|
|
||||||
expect(memberHtml).not.toContain("px-(--spacing-md,12px)");
|
|
||||||
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
|
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
|
||||||
expect(memberHtml).toContain("size-(--responsive-icon-button-size,42px)");
|
expect(memberHtml).toContain("size-(--responsive-icon-button-size,42px)");
|
||||||
expect(memberWithHintHtml).toContain(
|
expect(memberWithHintHtml).toContain(
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export function ChatHeader({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isGuest
|
isGuest
|
||||||
? "flex items-center px-(--chat-inline-padding,16px) py-(--spacing-sm,8px)"
|
? "flex items-center px-(--spacing-md,12px) py-(--spacing-sm,8px)"
|
||||||
: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-(--spacing-sm,8px) px-(--chat-inline-padding,16px) py-(--spacing-sm,8px)"
|
: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-(--spacing-sm,8px) px-(--spacing-md,12px) py-(--spacing-sm,8px)"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<BackButton
|
<BackButton
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PaymentPlanSchema,
|
PaymentPlanSchema,
|
||||||
@@ -7,9 +7,12 @@ import {
|
|||||||
} from "@/data/schemas/payment";
|
} from "@/data/schemas/payment";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
canChooseSubscriptionPayChannel,
|
||||||
findSelectedSubscriptionPlan,
|
findSelectedSubscriptionPlan,
|
||||||
|
getDefaultPayChannelForCountryCode,
|
||||||
getDefaultSubscriptionPlanId,
|
getDefaultSubscriptionPlanId,
|
||||||
getFirstRechargeOfferView,
|
getFirstRechargeOfferView,
|
||||||
|
resolveSubscriptionPayChannel,
|
||||||
toCoinsOfferPlanViews,
|
toCoinsOfferPlanViews,
|
||||||
toVipOfferPlanViews,
|
toVipOfferPlanViews,
|
||||||
} from "../subscription-screen.helpers";
|
} from "../subscription-screen.helpers";
|
||||||
@@ -31,6 +34,68 @@ function makePlan(overrides: Partial<PaymentPlanInput>): PaymentPlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("subscription screen helpers", () => {
|
describe("subscription screen helpers", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults Philippines users to GCash and other countries to Stripe", () => {
|
||||||
|
expect(getDefaultPayChannelForCountryCode("PH")).toBe("ezpay");
|
||||||
|
expect(getDefaultPayChannelForCountryCode("ph")).toBe("ezpay");
|
||||||
|
expect(getDefaultPayChannelForCountryCode("HK")).toBe("stripe");
|
||||||
|
expect(getDefaultPayChannelForCountryCode("")).toBe("stripe");
|
||||||
|
expect(getDefaultPayChannelForCountryCode(null)).toBe("stripe");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only allows payment method switching for Philippines users in production", () => {
|
||||||
|
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
|
||||||
|
|
||||||
|
expect(canChooseSubscriptionPayChannel("PH")).toBe(true);
|
||||||
|
expect(canChooseSubscriptionPayChannel("ph")).toBe(true);
|
||||||
|
expect(canChooseSubscriptionPayChannel("HK")).toBe(false);
|
||||||
|
expect(canChooseSubscriptionPayChannel(null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows payment method switching in non-production", () => {
|
||||||
|
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "test");
|
||||||
|
|
||||||
|
expect(canChooseSubscriptionPayChannel("HK")).toBe(true);
|
||||||
|
expect(canChooseSubscriptionPayChannel(null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves subscription pay channel by country in production", () => {
|
||||||
|
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveSubscriptionPayChannel({
|
||||||
|
countryCode: "PH",
|
||||||
|
requestedPayChannel: null,
|
||||||
|
}),
|
||||||
|
).toBe("ezpay");
|
||||||
|
expect(
|
||||||
|
resolveSubscriptionPayChannel({
|
||||||
|
countryCode: "PH",
|
||||||
|
requestedPayChannel: "stripe",
|
||||||
|
}),
|
||||||
|
).toBe("stripe");
|
||||||
|
expect(
|
||||||
|
resolveSubscriptionPayChannel({
|
||||||
|
countryCode: "HK",
|
||||||
|
requestedPayChannel: "ezpay",
|
||||||
|
}),
|
||||||
|
).toBe("stripe");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps requested pay channel in non-production", () => {
|
||||||
|
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "test");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveSubscriptionPayChannel({
|
||||||
|
countryCode: "HK",
|
||||||
|
requestedPayChannel: "ezpay",
|
||||||
|
}),
|
||||||
|
).toBe("ezpay");
|
||||||
|
});
|
||||||
|
|
||||||
it("maps at most three VIP plans and all coin plans", () => {
|
it("maps at most three VIP plans and all coin plans", () => {
|
||||||
const plans = [
|
const plans = [
|
||||||
makePlan({ planId: "vip_monthly", planName: "Monthly" }),
|
makePlan({ planId: "vip_monthly", planName: "Monthly" }),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { StripePaymentDialog } from "@/app/_components/payment/stripe-payment-dialog";
|
import { StripePaymentDialog } from "@/app/_components/payment/stripe-payment-dialog";
|
||||||
|
|
||||||
import { SubscriptionCtaButton } from "../subscription-cta-button";
|
import { SubscriptionCtaButton } from "../subscription-cta-button";
|
||||||
|
import { SubscriptionPaymentMethod } from "../subscription-payment-method";
|
||||||
import { SubscriptionPaymentSuccessDialog } from "../subscription-payment-success-dialog";
|
import { SubscriptionPaymentSuccessDialog } from "../subscription-payment-success-dialog";
|
||||||
import { SubscriptionCoinsOfferSection } from "../subscription-coins-offer-section";
|
import { SubscriptionCoinsOfferSection } from "../subscription-coins-offer-section";
|
||||||
import { SubscriptionVipOfferSection } from "../subscription-vip-offer-section";
|
import { SubscriptionVipOfferSection } from "../subscription-vip-offer-section";
|
||||||
@@ -48,6 +49,38 @@ describe("subscription Tailwind components", () => {
|
|||||||
expect(html).toContain("OK");
|
expect(html).toContain("OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders SubscriptionPaymentMethod with default ordering and selection", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<SubscriptionPaymentMethod
|
||||||
|
value="ezpay"
|
||||||
|
defaultChannel="ezpay"
|
||||||
|
caption="GCash by default"
|
||||||
|
onChange={() => 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(
|
||||||
|
/<button[^>]*aria-pressed="true"[^>]*>/,
|
||||||
|
)?.[0];
|
||||||
|
expect(selectedButton).toBeDefined();
|
||||||
|
expect(selectedButton).toContain(
|
||||||
|
'data-analytics-key="subscription.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("renders StripePaymentDialog fallback with Tailwind dialog utilities", () => {
|
it("renders StripePaymentDialog fallback with Tailwind dialog utilities", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<StripePaymentDialog
|
<StripePaymentDialog
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
export * from "./subscription-checkout-button";
|
export * from "./subscription-checkout-button";
|
||||||
export * from "./subscription-coins-offer-section";
|
export * from "./subscription-coins-offer-section";
|
||||||
export * from "./subscription-cta-button";
|
export * from "./subscription-cta-button";
|
||||||
|
export * from "./subscription-payment-method";
|
||||||
export * from "./subscription-payment-success-dialog";
|
export * from "./subscription-payment-success-dialog";
|
||||||
export * from "./subscription-vip-offer-section";
|
export * from "./subscription-vip-offer-section";
|
||||||
|
|||||||
+5
-7
@@ -4,7 +4,7 @@ import Image from "next/image";
|
|||||||
|
|
||||||
import type { PayChannel } from "@/data/schemas/payment";
|
import type { PayChannel } from "@/data/schemas/payment";
|
||||||
|
|
||||||
const PAYMENT_METHODS: readonly {
|
const PAYMENT_METHODS: {
|
||||||
channel: PayChannel;
|
channel: PayChannel;
|
||||||
title: string;
|
title: string;
|
||||||
logoSrc?: string;
|
logoSrc?: string;
|
||||||
@@ -20,23 +20,21 @@ const PAYMENT_METHODS: readonly {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface PaymentMethodSelectorProps {
|
export interface SubscriptionPaymentMethodProps {
|
||||||
value: PayChannel;
|
value: PayChannel;
|
||||||
defaultChannel?: PayChannel;
|
defaultChannel?: PayChannel;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
analyticsKey: string;
|
|
||||||
onChange: (channel: PayChannel) => void;
|
onChange: (channel: PayChannel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaymentMethodSelector({
|
export function SubscriptionPaymentMethod({
|
||||||
value,
|
value,
|
||||||
defaultChannel = "stripe",
|
defaultChannel = "stripe",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
caption = "Stripe by default",
|
caption = "Stripe by default",
|
||||||
analyticsKey,
|
|
||||||
onChange,
|
onChange,
|
||||||
}: PaymentMethodSelectorProps) {
|
}: SubscriptionPaymentMethodProps) {
|
||||||
const paymentMethods =
|
const paymentMethods =
|
||||||
defaultChannel === "ezpay"
|
defaultChannel === "ezpay"
|
||||||
? [...PAYMENT_METHODS].sort((a, b) =>
|
? [...PAYMENT_METHODS].sort((a, b) =>
|
||||||
@@ -77,7 +75,7 @@ export function PaymentMethodSelector({
|
|||||||
<button
|
<button
|
||||||
key={method.channel}
|
key={method.channel}
|
||||||
type="button"
|
type="button"
|
||||||
data-analytics-key={analyticsKey}
|
data-analytics-key="subscription.payment_method"
|
||||||
data-analytics-label="Select payment method"
|
data-analytics-label="Select payment method"
|
||||||
className={classes}
|
className={classes}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { PaymentPlan } from "@/data/schemas/payment";
|
import type { PayChannel, PaymentPlan } from "@/data/schemas/payment";
|
||||||
|
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||||
|
import { AppEnvUtil } from "@/utils/app-env";
|
||||||
|
export { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||||
|
|
||||||
import type { CoinsOfferPlanView } from "./components/subscription-coins-offer-section";
|
import type { CoinsOfferPlanView } from "./components/subscription-coins-offer-section";
|
||||||
import type { VipOfferPlanView } from "./components/subscription-vip-offer-section";
|
import type { VipOfferPlanView } from "./components/subscription-vip-offer-section";
|
||||||
@@ -12,6 +15,24 @@ export interface FirstRechargeOfferView {
|
|||||||
renewalNotice: string | null;
|
renewalNotice: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canChooseSubscriptionPayChannel(
|
||||||
|
countryCode: string | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!AppEnvUtil.isProduction()) return true;
|
||||||
|
return countryCode?.trim().toUpperCase() === "PH";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSubscriptionPayChannel(input: {
|
||||||
|
countryCode: string | null | undefined;
|
||||||
|
requestedPayChannel: PayChannel | null | undefined;
|
||||||
|
}): PayChannel {
|
||||||
|
if (!canChooseSubscriptionPayChannel(input.countryCode)) return "stripe";
|
||||||
|
return (
|
||||||
|
input.requestedPayChannel ??
|
||||||
|
getDefaultPayChannelForCountryCode(input.countryCode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isVipPlan(plan: PaymentPlan): boolean {
|
export function isVipPlan(plan: PaymentPlan): boolean {
|
||||||
return plan.vipDays !== null;
|
return plan.vipDays !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { BackButton } from "@/app/_components";
|
import { BackButton } from "@/app/_components";
|
||||||
import { Checkbox, MobileShell } from "@/app/_components/core";
|
import { Checkbox, MobileShell } from "@/app/_components/core";
|
||||||
import { PaymentMethodSelector } from "@/app/_components/payment/payment-method-selector";
|
|
||||||
import { usePaymentMethodSelection } from "@/app/_hooks/use-payment-method-selection";
|
|
||||||
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
|
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
|
||||||
import { AppConstants } from "@/core/app_constants";
|
import { AppConstants } from "@/core/app_constants";
|
||||||
import type { PayChannel } from "@/data/schemas/payment";
|
import type { PayChannel } from "@/data/schemas/payment";
|
||||||
@@ -15,20 +13,22 @@ import {
|
|||||||
getDefaultPaymentAnalyticsContext,
|
getDefaultPaymentAnalyticsContext,
|
||||||
type PaymentAnalyticsContext,
|
type PaymentAnalyticsContext,
|
||||||
} from "@/lib/analytics";
|
} from "@/lib/analytics";
|
||||||
import { getPaymentMethodConfig } from "@/lib/payment/payment_method";
|
|
||||||
import { useUserState } from "@/stores/user/user-context";
|
import { useUserState } from "@/stores/user/user-context";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SubscriptionCheckoutButton,
|
SubscriptionCheckoutButton,
|
||||||
SubscriptionCoinsOfferSection,
|
SubscriptionCoinsOfferSection,
|
||||||
|
SubscriptionPaymentMethod,
|
||||||
SubscriptionPaymentSuccessDialog,
|
SubscriptionPaymentSuccessDialog,
|
||||||
SubscriptionVipOfferSection,
|
SubscriptionVipOfferSection,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import styles from "./components/subscription-screen.module.css";
|
import styles from "./components/subscription-screen.module.css";
|
||||||
import {
|
import {
|
||||||
findSelectedSubscriptionPlan,
|
findSelectedSubscriptionPlan,
|
||||||
|
canChooseSubscriptionPayChannel,
|
||||||
getFirstRechargeOfferView,
|
getFirstRechargeOfferView,
|
||||||
getDefaultSubscriptionPlanId,
|
getDefaultSubscriptionPlanId,
|
||||||
|
resolveSubscriptionPayChannel,
|
||||||
toCoinsOfferPlanViews,
|
toCoinsOfferPlanViews,
|
||||||
toVipOfferPlanViews,
|
toVipOfferPlanViews,
|
||||||
type SubscriptionType,
|
type SubscriptionType,
|
||||||
@@ -56,10 +56,13 @@ export function SubscriptionScreen({
|
|||||||
}: SubscriptionScreenProps) {
|
}: SubscriptionScreenProps) {
|
||||||
const userState = useUserState();
|
const userState = useUserState();
|
||||||
const countryCode = userState.currentUser?.countryCode;
|
const countryCode = userState.currentUser?.countryCode;
|
||||||
const paymentMethodConfig = getPaymentMethodConfig({
|
const canChoosePaymentMethod =
|
||||||
|
canChooseSubscriptionPayChannel(countryCode);
|
||||||
|
const resolvedInitialPayChannel = resolveSubscriptionPayChannel({
|
||||||
countryCode,
|
countryCode,
|
||||||
requestedPayChannel: initialPayChannel,
|
requestedPayChannel: initialPayChannel,
|
||||||
});
|
});
|
||||||
|
const phDefaultPayChannelAppliedRef = useRef(false);
|
||||||
const {
|
const {
|
||||||
payment,
|
payment,
|
||||||
paymentDispatch,
|
paymentDispatch,
|
||||||
@@ -71,7 +74,7 @@ export function SubscriptionScreen({
|
|||||||
shouldResumePendingOrder,
|
shouldResumePendingOrder,
|
||||||
returnTo,
|
returnTo,
|
||||||
characterSlug,
|
characterSlug,
|
||||||
initialPayChannel: paymentMethodConfig.initialPayChannel,
|
initialPayChannel: resolvedInitialPayChannel,
|
||||||
});
|
});
|
||||||
const canSubscribeVip = subscriptionType === "vip";
|
const canSubscribeVip = subscriptionType === "vip";
|
||||||
const analyticsContext =
|
const analyticsContext =
|
||||||
@@ -131,20 +134,35 @@ export function SubscriptionScreen({
|
|||||||
paymentDispatch({ type: "PaymentPlanSelected", planId });
|
paymentDispatch({ type: "PaymentPlanSelected", planId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaymentMethodChange = (payChannel: PayChannel) => {
|
useEffect(() => {
|
||||||
|
if (isPaymentBusy) return;
|
||||||
|
|
||||||
|
if (!canChoosePaymentMethod) {
|
||||||
|
if (payment.payChannel === "stripe") return;
|
||||||
|
paymentDispatch({
|
||||||
|
type: "PaymentPayChannelChanged",
|
||||||
|
payChannel: "stripe",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialPayChannel !== null) return;
|
||||||
|
if (phDefaultPayChannelAppliedRef.current) return;
|
||||||
|
phDefaultPayChannelAppliedRef.current = true;
|
||||||
|
|
||||||
|
if (payment.payChannel === resolvedInitialPayChannel) return;
|
||||||
paymentDispatch({
|
paymentDispatch({
|
||||||
type: "PaymentPayChannelChanged",
|
type: "PaymentPayChannelChanged",
|
||||||
payChannel,
|
payChannel: resolvedInitialPayChannel,
|
||||||
});
|
});
|
||||||
};
|
}, [
|
||||||
|
canChoosePaymentMethod,
|
||||||
usePaymentMethodSelection({
|
initialPayChannel,
|
||||||
config: paymentMethodConfig,
|
|
||||||
currentPayChannel: payment.payChannel,
|
|
||||||
isPaymentBusy,
|
isPaymentBusy,
|
||||||
requestedPayChannel: initialPayChannel,
|
payment.payChannel,
|
||||||
onChange: handlePaymentMethodChange,
|
paymentDispatch,
|
||||||
});
|
resolvedInitialPayChannel,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultPlanId = getDefaultSubscriptionPlanId({
|
const defaultPlanId = getDefaultSubscriptionPlanId({
|
||||||
@@ -224,15 +242,19 @@ export function SubscriptionScreen({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paymentMethodConfig.canChoosePaymentMethod ? (
|
{canChoosePaymentMethod ? (
|
||||||
<section className={styles.paymentMethodSlot}>
|
<section className={styles.paymentMethodSlot}>
|
||||||
<PaymentMethodSelector
|
<SubscriptionPaymentMethod
|
||||||
value={payment.payChannel}
|
value={payment.payChannel}
|
||||||
defaultChannel={paymentMethodConfig.initialPayChannel}
|
defaultChannel={resolvedInitialPayChannel}
|
||||||
disabled={isPaymentBusy}
|
disabled={isPaymentBusy}
|
||||||
caption="GCash by default in the Philippines"
|
caption="GCash by default in the Philippines"
|
||||||
analyticsKey="subscription.payment_method"
|
onChange={(payChannel) => {
|
||||||
onChange={handlePaymentMethodChange}
|
paymentDispatch({
|
||||||
|
type: "PaymentPayChannelChanged",
|
||||||
|
payChannel,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
.header,
|
.header,
|
||||||
.hero,
|
.hero,
|
||||||
.productCard,
|
.productCard,
|
||||||
.paymentMethodSlot,
|
|
||||||
.statusMessage,
|
.statusMessage,
|
||||||
.successCard,
|
.successCard,
|
||||||
.checkoutSlot {
|
.checkoutSlot {
|
||||||
@@ -408,10 +407,6 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paymentMethodSlot {
|
|
||||||
margin-top: clamp(18px, 5.185vw, 28px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.successCard {
|
.successCard {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
border-color: rgba(255, 84, 135, 0.16);
|
border-color: rgba(255, 84, 135, 0.16);
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { ArrowLeft, Heart, Sparkles } from "lucide-react";
|
|||||||
|
|
||||||
import { CharacterAvatar } from "@/app/_components";
|
import { CharacterAvatar } from "@/app/_components";
|
||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
import { PaymentMethodSelector } from "@/app/_components/payment/payment-method-selector";
|
|
||||||
import { usePaymentMethodSelection } from "@/app/_hooks/use-payment-method-selection";
|
|
||||||
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
|
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
|
||||||
import { usePaymentRouteFlow } from "@/app/_hooks/use-payment-route-flow";
|
import { usePaymentRouteFlow } from "@/app/_hooks/use-payment-route-flow";
|
||||||
import type { PayChannel } from "@/data/schemas/payment";
|
import type { PayChannel } from "@/data/schemas/payment";
|
||||||
@@ -15,7 +13,6 @@ import {
|
|||||||
behaviorAnalytics,
|
behaviorAnalytics,
|
||||||
type PaymentAnalyticsContext,
|
type PaymentAnalyticsContext,
|
||||||
} from "@/lib/analytics";
|
} from "@/lib/analytics";
|
||||||
import { getPaymentMethodConfig } from "@/lib/payment/payment_method";
|
|
||||||
import {
|
import {
|
||||||
buildTipCoffeePath,
|
buildTipCoffeePath,
|
||||||
DEFAULT_TIP_COFFEE_TYPE,
|
DEFAULT_TIP_COFFEE_TYPE,
|
||||||
@@ -29,7 +26,6 @@ import {
|
|||||||
useActiveCharacterRoutes,
|
useActiveCharacterRoutes,
|
||||||
} from "@/providers/character-provider";
|
} from "@/providers/character-provider";
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useUserState } from "@/stores/user/user-context";
|
|
||||||
|
|
||||||
import { TipCheckoutButton } from "./tip-checkout-button";
|
import { TipCheckoutButton } from "./tip-checkout-button";
|
||||||
import {
|
import {
|
||||||
@@ -63,11 +59,6 @@ export function TipScreen({
|
|||||||
const character = useActiveCharacter();
|
const character = useActiveCharacter();
|
||||||
const characterRoutes = useActiveCharacterRoutes();
|
const characterRoutes = useActiveCharacterRoutes();
|
||||||
const authState = useAuthState();
|
const authState = useAuthState();
|
||||||
const userState = useUserState();
|
|
||||||
const paymentMethodConfig = getPaymentMethodConfig({
|
|
||||||
countryCode: userState.currentUser?.countryCode,
|
|
||||||
requestedPayChannel: initialPayChannel,
|
|
||||||
});
|
|
||||||
const [selectedCoffeeType, setSelectedCoffeeType] =
|
const [selectedCoffeeType, setSelectedCoffeeType] =
|
||||||
useState<TipCoffeeType>(coffeeType);
|
useState<TipCoffeeType>(coffeeType);
|
||||||
const coffeeOption = getTipCoffeeOption(selectedCoffeeType);
|
const coffeeOption = getTipCoffeeOption(selectedCoffeeType);
|
||||||
@@ -75,9 +66,11 @@ export function TipScreen({
|
|||||||
selectedCoffeeType,
|
selectedCoffeeType,
|
||||||
characterRoutes.tip,
|
characterRoutes.tip,
|
||||||
);
|
);
|
||||||
|
const resolvedInitialPayChannel =
|
||||||
|
initialPayChannel ?? navigator.getDefaultPayChannel();
|
||||||
const { payment, paymentDispatch } = usePaymentRouteFlow({
|
const { payment, paymentDispatch } = usePaymentRouteFlow({
|
||||||
catalog: "tip",
|
catalog: "tip",
|
||||||
initialPayChannel: paymentMethodConfig.initialPayChannel,
|
initialPayChannel: resolvedInitialPayChannel,
|
||||||
paymentType: "tip",
|
paymentType: "tip",
|
||||||
shouldResumePendingOrder,
|
shouldResumePendingOrder,
|
||||||
});
|
});
|
||||||
@@ -127,21 +120,6 @@ export function TipScreen({
|
|||||||
payment.status !== "ready" || payment.isLoadingPlans || isPaymentBusy;
|
payment.status !== "ready" || payment.isLoadingPlans || isPaymentBusy;
|
||||||
const isAuthLoading = !authState.hasInitialized || authState.isLoading;
|
const isAuthLoading = !authState.hasInitialized || authState.isLoading;
|
||||||
|
|
||||||
const handlePaymentMethodChange = (payChannel: PayChannel) => {
|
|
||||||
paymentDispatch({
|
|
||||||
type: "PaymentPayChannelChanged",
|
|
||||||
payChannel,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
usePaymentMethodSelection({
|
|
||||||
config: paymentMethodConfig,
|
|
||||||
currentPayChannel: payment.payChannel,
|
|
||||||
isPaymentBusy,
|
|
||||||
requestedPayChannel: initialPayChannel,
|
|
||||||
onChange: handlePaymentMethodChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (payment.isLoadingPlans || payment.isCreatingOrder || payment.isPollingOrder) {
|
if (payment.isLoadingPlans || payment.isCreatingOrder || payment.isPollingOrder) {
|
||||||
return;
|
return;
|
||||||
@@ -292,19 +270,6 @@ export function TipScreen({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{paymentMethodConfig.canChoosePaymentMethod ? (
|
|
||||||
<section className={styles.paymentMethodSlot}>
|
|
||||||
<PaymentMethodSelector
|
|
||||||
value={payment.payChannel}
|
|
||||||
defaultChannel={paymentMethodConfig.initialPayChannel}
|
|
||||||
disabled={isPaymentBusy}
|
|
||||||
caption="GCash by default in the Philippines"
|
|
||||||
analyticsKey="tip.payment_method"
|
|
||||||
onChange={handlePaymentMethodChange}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showMissingPlan ? (
|
{showMissingPlan ? (
|
||||||
<p className={styles.statusMessage} role="alert">
|
<p className={styles.statusMessage} role="alert">
|
||||||
Coffee tip is not available yet. Please try again later.
|
Coffee tip is not available yet. Please try again later.
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import { getDefaultPayChannelForCountryCode } from "../default_pay_channel";
|
|
||||||
import {
|
|
||||||
canChoosePayChannel,
|
|
||||||
getPaymentMethodConfig,
|
|
||||||
resolvePayChannel,
|
|
||||||
} from "../payment_method";
|
|
||||||
|
|
||||||
describe("payment method rules", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllEnvs();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("defaults Philippines users to GCash and other countries to Stripe", () => {
|
|
||||||
expect(getDefaultPayChannelForCountryCode("PH")).toBe("ezpay");
|
|
||||||
expect(getDefaultPayChannelForCountryCode("ph")).toBe("ezpay");
|
|
||||||
expect(getDefaultPayChannelForCountryCode("HK")).toBe("stripe");
|
|
||||||
expect(getDefaultPayChannelForCountryCode(null)).toBe("stripe");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("only allows Philippines users to choose in production", () => {
|
|
||||||
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
|
|
||||||
|
|
||||||
expect(canChoosePayChannel("PH")).toBe(true);
|
|
||||||
expect(canChoosePayChannel("ph")).toBe(true);
|
|
||||||
expect(canChoosePayChannel("HK")).toBe(false);
|
|
||||||
expect(canChoosePayChannel(null)).toBe(false);
|
|
||||||
expect(
|
|
||||||
getPaymentMethodConfig({
|
|
||||||
countryCode: null,
|
|
||||||
requestedPayChannel: "ezpay",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
canChoosePaymentMethod: false,
|
|
||||||
initialPayChannel: "stripe",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves requested and default channels for Philippines", () => {
|
|
||||||
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
resolvePayChannel({ countryCode: "PH", requestedPayChannel: null }),
|
|
||||||
).toBe("ezpay");
|
|
||||||
expect(
|
|
||||||
resolvePayChannel({
|
|
||||||
countryCode: "PH",
|
|
||||||
requestedPayChannel: "stripe",
|
|
||||||
}),
|
|
||||||
).toBe("stripe");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows every user and keeps the requested channel outside production", () => {
|
|
||||||
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "test");
|
|
||||||
|
|
||||||
expect(canChoosePayChannel(null)).toBe(true);
|
|
||||||
expect(
|
|
||||||
resolvePayChannel({
|
|
||||||
countryCode: "HK",
|
|
||||||
requestedPayChannel: "ezpay",
|
|
||||||
}),
|
|
||||||
).toBe("ezpay");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import type { PayChannel } from "@/data/schemas/payment";
|
|
||||||
import { AppEnvUtil } from "@/utils/app-env";
|
|
||||||
|
|
||||||
import { getDefaultPayChannelForCountryCode } from "./default_pay_channel";
|
|
||||||
|
|
||||||
export interface PaymentMethodConfig {
|
|
||||||
canChoosePaymentMethod: boolean;
|
|
||||||
initialPayChannel: PayChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canChoosePayChannel(
|
|
||||||
countryCode: string | null | undefined,
|
|
||||||
): boolean {
|
|
||||||
if (!AppEnvUtil.isProduction()) return true;
|
|
||||||
return countryCode?.trim().toUpperCase() === "PH";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolvePayChannel(input: {
|
|
||||||
countryCode: string | null | undefined;
|
|
||||||
requestedPayChannel: PayChannel | null | undefined;
|
|
||||||
}): PayChannel {
|
|
||||||
if (!canChoosePayChannel(input.countryCode)) return "stripe";
|
|
||||||
return (
|
|
||||||
input.requestedPayChannel ??
|
|
||||||
getDefaultPayChannelForCountryCode(input.countryCode)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPaymentMethodConfig(input: {
|
|
||||||
countryCode: string | null | undefined;
|
|
||||||
requestedPayChannel: PayChannel | null | undefined;
|
|
||||||
}): PaymentMethodConfig {
|
|
||||||
return {
|
|
||||||
canChoosePaymentMethod: canChoosePayChannel(input.countryCode),
|
|
||||||
initialPayChannel: resolvePayChannel(input),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user