refactor(routes): move shared pages to global scope
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
import { CoinsRulesScreen } from "@/app/coins-rules/coins-rules-screen";
|
||||
|
||||
export default function CharacterCoinsRulesPage() {
|
||||
return <CoinsRulesScreen />;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { FeedbackScreen } from "@/app/feedback/feedback-screen";
|
||||
|
||||
export default function CharacterFeedbackPage() {
|
||||
return <FeedbackScreen />;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { SidebarScreen } from "@/app/sidebar/sidebar-screen";
|
||||
|
||||
export default function CharacterSidebarPage() {
|
||||
return <SidebarScreen />;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { Lock, Menu } from "lucide-react";
|
||||
|
||||
import { BackButton } from "@/app/_components";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import { buildGlobalPageUrl } from "@/router/global-route-context";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
|
||||
import { BrowserHintOverlay } from "./browser-hint-overlay";
|
||||
@@ -75,7 +77,11 @@ export function ChatHeader({
|
||||
data-analytics-key="chat.open_menu"
|
||||
data-analytics-label="Open chat menu"
|
||||
className="flex size-(--responsive-icon-button-size,42px) cursor-pointer items-center justify-center rounded-full border border-[rgba(255,255,255,0.12)] bg-[rgba(13,11,20,0.7)] text-(--color-text-primary,#fff) shadow-[0_10px_24px_rgba(0,0,0,0.2)] backdrop-blur-md transition-[background,transform] duration-150 hover:bg-[rgba(28,24,39,0.82)] active:scale-96 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
||||
onClick={() => navigator.push(characterRoutes.sidebar)}
|
||||
onClick={() =>
|
||||
navigator.push(
|
||||
buildGlobalPageUrl(ROUTES.sidebar, characterRoutes.chat),
|
||||
)
|
||||
}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<Menu size={24} aria-hidden="true" />
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { CoinsRulesScreen } from "../coins-rules-screen";
|
||||
|
||||
vi.mock("@/stores/auth/auth-context", () => ({
|
||||
useAuthState: () => ({
|
||||
hasInitialized: true,
|
||||
isLoading: false,
|
||||
loginStatus: "email",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/user/user-context", () => ({
|
||||
useUserDispatch: () => vi.fn(),
|
||||
useUserState: () => ({
|
||||
currentUser: {
|
||||
dailyFreeChatLimit: 30,
|
||||
dailyFreePrivateLimit: 2,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("CoinsRulesScreen", () => {
|
||||
it("renders outside CharacterProvider and returns to global sidebar", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CoinsRulesScreen returnTo="/characters/maya/chat" />,
|
||||
);
|
||||
|
||||
expect(html).toContain("Coin Usage Rules");
|
||||
expect(html).toContain(
|
||||
'href="/sidebar?returnTo=%2Fcharacters%2Fmaya%2Fchat"',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,10 @@ import { FaCoins } from "react-icons/fa6";
|
||||
|
||||
import { BackButton } from "@/app/_components";
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import {
|
||||
resolveGlobalRouteContext,
|
||||
type GlobalReturnToValue,
|
||||
} from "@/router/global-route-context";
|
||||
import { useAuthState } from "@/stores/auth/auth-context";
|
||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||
|
||||
@@ -75,8 +78,12 @@ const createCoinRules = (
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function CoinsRulesScreen() {
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
export interface CoinsRulesScreenProps {
|
||||
returnTo?: GlobalReturnToValue;
|
||||
}
|
||||
|
||||
export function CoinsRulesScreen({ returnTo }: CoinsRulesScreenProps) {
|
||||
const navigation = resolveGlobalRouteContext(returnTo);
|
||||
const authState = useAuthState();
|
||||
const userState = useUserState();
|
||||
const userDispatch = useUserDispatch();
|
||||
@@ -111,7 +118,7 @@ export function CoinsRulesScreen() {
|
||||
<main className={styles.screen}>
|
||||
<header className={styles.header}>
|
||||
<BackButton
|
||||
href={characterRoutes.sidebar}
|
||||
href={navigation.sidebarUrl}
|
||||
variant="soft"
|
||||
aria-label="Back to sidebar"
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import type { RouteSearchParams } from "@/router/routes";
|
||||
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
import { CoinsRulesScreen } from "./coins-rules-screen";
|
||||
|
||||
export default async function CoinsRulesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).coinsRules,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
const query = await searchParams;
|
||||
return <CoinsRulesScreen returnTo={query.returnTo} />;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getCharacterBySlug } from "@/data/constants/character";
|
||||
import { CharacterProvider } from "@/providers/character-provider";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import { FeedbackScreen } from "../feedback-screen";
|
||||
@@ -13,8 +11,8 @@ const mocks = vi.hoisted(() => ({
|
||||
submitFeedback: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/router/use-app-navigator", () => ({
|
||||
useAppNavigator: () => ({ replace: mocks.replace }),
|
||||
vi.mock("@/router/use-global-app-navigator", () => ({
|
||||
useGlobalAppNavigator: () => ({ replace: mocks.replace }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/feedback", () => ({
|
||||
@@ -69,6 +67,11 @@ describe("FeedbackScreen", () => {
|
||||
.querySelector("textarea")
|
||||
?.getAttribute("aria-describedby"),
|
||||
).toBe("feedback-content-hint");
|
||||
expect(
|
||||
container.querySelector<HTMLAnchorElement>(
|
||||
'[data-analytics-key="feedback.back_to_sidebar"]',
|
||||
)?.getAttribute("href"),
|
||||
).toBe("/sidebar?returnTo=%2Fcharacters%2Fmaya%2Fchat");
|
||||
});
|
||||
|
||||
it("submits valid text and renders the feedback id", async () => {
|
||||
@@ -103,18 +106,13 @@ describe("FeedbackScreen", () => {
|
||||
});
|
||||
expect(container.textContent).toContain("feedback-123");
|
||||
expect(container.textContent).toContain("Thank you for helping us.");
|
||||
expect(container.textContent).toContain("Back to Sidebar");
|
||||
});
|
||||
});
|
||||
|
||||
function renderFeedbackScreen(root: Root): void {
|
||||
const character = getCharacterBySlug("maya");
|
||||
if (!character) throw new Error("Missing Maya character fixture");
|
||||
act(() =>
|
||||
root.render(
|
||||
<CharacterProvider character={character}>
|
||||
<FeedbackScreen />
|
||||
</CharacterProvider>,
|
||||
),
|
||||
root.render(<FeedbackScreen returnTo="/characters/maya/chat" />),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,11 @@ import {
|
||||
import { BackButton } from "@/app/_components";
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
import type { FeedbackCategory } from "@/data/schemas/feedback";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import {
|
||||
resolveGlobalRouteContext,
|
||||
type GlobalReturnToValue,
|
||||
} from "@/router/global-route-context";
|
||||
import { useGlobalAppNavigator } from "@/router/use-global-app-navigator";
|
||||
|
||||
import {
|
||||
FEEDBACK_IMAGE_ACCEPT,
|
||||
@@ -45,9 +48,13 @@ const CATEGORY_OPTIONS: readonly CategoryOption[] = [
|
||||
{ value: "other", label: "Other", icon: MessageCircleMore },
|
||||
];
|
||||
|
||||
export function FeedbackScreen() {
|
||||
const navigator = useAppNavigator();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
export interface FeedbackScreenProps {
|
||||
returnTo?: GlobalReturnToValue;
|
||||
}
|
||||
|
||||
export function FeedbackScreen({ returnTo }: FeedbackScreenProps) {
|
||||
const navigator = useGlobalAppNavigator();
|
||||
const navigation = resolveGlobalRouteContext(returnTo);
|
||||
const form = useFeedbackSubmission();
|
||||
|
||||
if (form.feedbackId) {
|
||||
@@ -70,11 +77,11 @@ export function FeedbackScreen() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-analytics-key="feedback.back_to_chat"
|
||||
data-analytics-key="feedback.back_to_sidebar"
|
||||
className={styles.primaryButton}
|
||||
onClick={() => navigator.replace(characterRoutes.chat)}
|
||||
onClick={() => navigator.replace(navigation.sidebarUrl)}
|
||||
>
|
||||
Back to Chat
|
||||
Back to Sidebar
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
@@ -95,10 +102,10 @@ export function FeedbackScreen() {
|
||||
|
||||
<header className={styles.header}>
|
||||
<BackButton
|
||||
href={characterRoutes.chat}
|
||||
href={navigation.sidebarUrl}
|
||||
variant="soft"
|
||||
ariaLabel="Back to chat"
|
||||
analyticsKey="feedback.back_to_chat"
|
||||
ariaLabel="Back to sidebar"
|
||||
analyticsKey="feedback.back_to_sidebar"
|
||||
/>
|
||||
<div className={styles.headingBlock}>
|
||||
<h1 className={styles.title}>Help us improve CozSweet</h1>
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import type { RouteSearchParams } from "@/router/routes";
|
||||
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
import { FeedbackScreen } from "./feedback-screen";
|
||||
|
||||
export default async function FeedbackPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).feedback,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
const query = await searchParams;
|
||||
return <FeedbackScreen returnTo={query.returnTo} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { SidebarScreen } from "../sidebar-screen";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openAuth: vi.fn(),
|
||||
openSubscription: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
authDispatch: vi.fn(),
|
||||
userDispatch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/router/use-global-app-navigator", () => ({
|
||||
useGlobalAppNavigator: () => ({
|
||||
openAuth: mocks.openAuth,
|
||||
openSubscription: mocks.openSubscription,
|
||||
push: mocks.push,
|
||||
replace: mocks.replace,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/auth/auth-context", () => ({
|
||||
useAuthDispatch: () => mocks.authDispatch,
|
||||
useAuthState: () => ({
|
||||
hasInitialized: true,
|
||||
isLoading: false,
|
||||
loginStatus: "email",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/user/user-context", () => ({
|
||||
useUserDispatch: () => mocks.userDispatch,
|
||||
useUserState: () => ({
|
||||
avatarUrl: null,
|
||||
creditBalance: 120,
|
||||
currentUser: {
|
||||
id: "user-1",
|
||||
username: "Chase",
|
||||
countryCode: "US",
|
||||
creditBalance: 120,
|
||||
dailyFreeChatLimit: 30,
|
||||
dailyFreeChatRemaining: 20,
|
||||
dailyFreePrivateLimit: 2,
|
||||
dailyFreePrivateRemaining: 1,
|
||||
isVip: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("next-auth/react", () => ({ signOut: vi.fn() }));
|
||||
vi.mock("../use-pwa-install-entry", () => ({
|
||||
usePwaInstallEntry: () => ({
|
||||
canInstall: false,
|
||||
installApp: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("../use-sidebar-user-bootstrap", () => ({
|
||||
useSidebarUserBootstrap: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("SidebarScreen", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
Object.values(mocks).forEach((mock) => mock.mockReset());
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("renders without CharacterProvider and preserves the source character", () => {
|
||||
act(() => {
|
||||
root.render(<SidebarScreen returnTo="/characters/maya/chat" />);
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector<HTMLAnchorElement>(
|
||||
'[data-analytics-key="sidebar.back_to_chat"]',
|
||||
)?.getAttribute("href"),
|
||||
).toBe("/characters/maya/chat");
|
||||
|
||||
clickButton("Coin Usage Rules");
|
||||
expect(mocks.push).toHaveBeenCalledWith(
|
||||
"/coins-rules?returnTo=%2Fcharacters%2Fmaya%2Fchat",
|
||||
);
|
||||
|
||||
clickButton("Feedback");
|
||||
expect(mocks.push).toHaveBeenCalledWith(
|
||||
"/feedback?returnTo=%2Fcharacters%2Fmaya%2Fchat",
|
||||
);
|
||||
|
||||
clickButton("Top Up");
|
||||
expect(mocks.openSubscription).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "topup",
|
||||
sourceCharacterSlug: "maya",
|
||||
returnTo: "sidebar",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
function clickButton(label: string): void {
|
||||
const button = Array.from(container.querySelectorAll("button")).find(
|
||||
(item) => item.textContent?.includes(label),
|
||||
);
|
||||
if (!button) throw new Error(`Missing ${label} button`);
|
||||
act(() => button.click());
|
||||
}
|
||||
});
|
||||
@@ -1,21 +1,12 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import type { RouteSearchParams } from "@/router/routes";
|
||||
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
appendRouteSearchParams,
|
||||
getCharacterRoutes,
|
||||
type RouteSearchParams,
|
||||
} from "@/router/routes";
|
||||
import { SidebarScreen } from "./sidebar-screen";
|
||||
|
||||
export default async function SidebarPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RouteSearchParams>;
|
||||
}) {
|
||||
redirect(
|
||||
appendRouteSearchParams(
|
||||
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).sidebar,
|
||||
await searchParams,
|
||||
),
|
||||
);
|
||||
const query = await searchParams;
|
||||
return <SidebarScreen returnTo={query.returnTo} />;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import { signOut } from "next-auth/react";
|
||||
|
||||
import { BackButton } from "@/app/_components";
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import {
|
||||
resolveGlobalRouteContext,
|
||||
type GlobalReturnToValue,
|
||||
} from "@/router/global-route-context";
|
||||
import { useGlobalAppNavigator } from "@/router/use-global-app-navigator";
|
||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||
|
||||
@@ -17,9 +20,13 @@ import { useSidebarUserBootstrap } from "./use-sidebar-user-bootstrap";
|
||||
|
||||
import styles from "./components/sidebar-screen.module.css";
|
||||
|
||||
export function SidebarScreen() {
|
||||
const navigator = useAppNavigator();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
export interface SidebarScreenProps {
|
||||
returnTo?: GlobalReturnToValue;
|
||||
}
|
||||
|
||||
export function SidebarScreen({ returnTo }: SidebarScreenProps) {
|
||||
const navigator = useGlobalAppNavigator();
|
||||
const navigation = resolveGlobalRouteContext(returnTo);
|
||||
const user = useUserState();
|
||||
const userDispatch = useUserDispatch();
|
||||
const auth = useAuthState();
|
||||
@@ -32,7 +39,7 @@ export function SidebarScreen() {
|
||||
userDispatch({ type: "UserClearLocal" });
|
||||
authDispatch({ type: "AuthLogoutSubmitted" });
|
||||
void signOut({ redirect: false });
|
||||
navigator.replace(characterRoutes.splash, { scroll: false });
|
||||
navigator.replace(navigation.splashUrl, { scroll: false });
|
||||
};
|
||||
|
||||
const view = getSidebarViewModel({
|
||||
@@ -48,7 +55,7 @@ export function SidebarScreen() {
|
||||
|
||||
<div className={styles.topBar}>
|
||||
<BackButton
|
||||
href={characterRoutes.chat}
|
||||
href={navigation.chatUrl}
|
||||
variant="soft"
|
||||
analyticsKey="sidebar.back_to_chat"
|
||||
/>
|
||||
@@ -60,7 +67,7 @@ export function SidebarScreen() {
|
||||
name={view.name}
|
||||
avatarUrl={view.avatarUrl}
|
||||
isLoading={view.isInitializingUser}
|
||||
onLoginClick={() => navigator.openAuth(characterRoutes.sidebar)}
|
||||
onLoginClick={() => navigator.openAuth(navigation.sidebarUrl)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -76,6 +83,8 @@ export function SidebarScreen() {
|
||||
? () =>
|
||||
navigator.openSubscription({
|
||||
type: "vip",
|
||||
sourceCharacterSlug: navigation.characterSlug,
|
||||
returnTo: "sidebar",
|
||||
analytics: {
|
||||
entryPoint: "sidebar",
|
||||
triggerReason: "vip_cta",
|
||||
@@ -86,13 +95,15 @@ export function SidebarScreen() {
|
||||
onTopUp={() =>
|
||||
navigator.openSubscription({
|
||||
type: "topup",
|
||||
sourceCharacterSlug: navigation.characterSlug,
|
||||
returnTo: "sidebar",
|
||||
analytics: {
|
||||
entryPoint: "sidebar",
|
||||
triggerReason: "sidebar_recharge",
|
||||
},
|
||||
})
|
||||
}
|
||||
onRulesClick={() => navigator.push(characterRoutes.coinsRules)}
|
||||
onRulesClick={() => navigator.push(navigation.coinsRulesUrl)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -105,7 +116,7 @@ export function SidebarScreen() {
|
||||
data-analytics-key="sidebar.open_feedback"
|
||||
data-analytics-label="Open feedback"
|
||||
className={`${styles.logoutCard} ${styles.feedbackCard}`}
|
||||
onClick={() => navigator.push(characterRoutes.feedback)}
|
||||
onClick={() => navigator.push(navigation.feedbackUrl)}
|
||||
>
|
||||
<span
|
||||
className={`${styles.logoutIcon} ${styles.feedbackIcon}`}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
screenProps: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../subscription-screen", () => ({
|
||||
SubscriptionScreen: (props: Record<string, unknown>) => {
|
||||
mocks.screenProps(props);
|
||||
return <main>Global subscription</main>;
|
||||
},
|
||||
}));
|
||||
|
||||
import SubscriptionPage from "../page";
|
||||
|
||||
describe("SubscriptionPage", () => {
|
||||
it("renders outside CharacterProvider and treats character as return context", async () => {
|
||||
const page = await SubscriptionPage({
|
||||
searchParams: Promise.resolve({
|
||||
character: "maya",
|
||||
returnTo: "sidebar",
|
||||
type: "topup",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(renderToStaticMarkup(page)).toContain("Global subscription");
|
||||
expect(mocks.screenProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
returnTo: "sidebar",
|
||||
sourceCharacterSlug: "maya",
|
||||
subscriptionType: "topup",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses Elio for missing source navigation context", async () => {
|
||||
const page = await SubscriptionPage({
|
||||
searchParams: Promise.resolve({}),
|
||||
});
|
||||
|
||||
renderToStaticMarkup(page);
|
||||
expect(mocks.screenProps).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ sourceCharacterSlug: "elio" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -23,14 +23,14 @@ export interface SubscriptionCheckoutButtonProps {
|
||||
disabled?: boolean;
|
||||
subscriptionType: "vip" | "topup";
|
||||
returnTo?: SubscriptionReturnTo;
|
||||
characterSlug?: string;
|
||||
sourceCharacterSlug?: string;
|
||||
}
|
||||
|
||||
export function SubscriptionCheckoutButton({
|
||||
disabled = false,
|
||||
subscriptionType,
|
||||
returnTo = null,
|
||||
characterSlug = DEFAULT_CHARACTER_SLUG,
|
||||
sourceCharacterSlug = DEFAULT_CHARACTER_SLUG,
|
||||
}: SubscriptionCheckoutButtonProps) {
|
||||
const payment = usePaymentState();
|
||||
const paymentDispatch = usePaymentDispatch();
|
||||
@@ -40,7 +40,7 @@ export function SubscriptionCheckoutButton({
|
||||
payment,
|
||||
paymentDispatch,
|
||||
returnTo: returnTo ?? undefined,
|
||||
characterSlug,
|
||||
characterSlug: sourceCharacterSlug,
|
||||
subscriptionType,
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async function SubscriptionPage({
|
||||
const subscriptionType = toSubscriptionType(
|
||||
getFirstPaymentSearchParam(query.type),
|
||||
);
|
||||
const characterSlug =
|
||||
const sourceCharacterSlug =
|
||||
getCharacterBySlug(getFirstPaymentSearchParam(query.character))?.slug ??
|
||||
DEFAULT_CHARACTER_SLUG;
|
||||
const analyticsContext = parsePaymentAnalyticsContext({
|
||||
@@ -49,7 +49,7 @@ export default async function SubscriptionPage({
|
||||
returnTo={parseSubscriptionReturnTo(query.returnTo)}
|
||||
initialPayChannel={paymentReturn.initialPayChannel}
|
||||
analyticsContext={analyticsContext}
|
||||
characterSlug={characterSlug}
|
||||
sourceCharacterSlug={sourceCharacterSlug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface SubscriptionScreenProps {
|
||||
returnTo?: SubscriptionReturnTo;
|
||||
initialPayChannel?: PayChannel | null;
|
||||
analyticsContext?: PaymentAnalyticsContext;
|
||||
characterSlug?: string;
|
||||
sourceCharacterSlug?: string;
|
||||
}
|
||||
|
||||
export function SubscriptionScreen({
|
||||
@@ -52,7 +52,7 @@ export function SubscriptionScreen({
|
||||
returnTo = null,
|
||||
initialPayChannel = null,
|
||||
analyticsContext: providedAnalyticsContext,
|
||||
characterSlug = DEFAULT_CHARACTER_SLUG,
|
||||
sourceCharacterSlug = DEFAULT_CHARACTER_SLUG,
|
||||
}: SubscriptionScreenProps) {
|
||||
const userState = useUserState();
|
||||
const countryCode = userState.currentUser?.countryCode;
|
||||
@@ -70,7 +70,7 @@ export function SubscriptionScreen({
|
||||
subscriptionType,
|
||||
shouldResumePendingOrder,
|
||||
returnTo,
|
||||
characterSlug,
|
||||
sourceCharacterSlug,
|
||||
initialPayChannel: paymentMethodConfig.initialPayChannel,
|
||||
});
|
||||
const canSubscribeVip = subscriptionType === "vip";
|
||||
@@ -242,7 +242,7 @@ export function SubscriptionScreen({
|
||||
disabled={!canActivate}
|
||||
subscriptionType={subscriptionType}
|
||||
returnTo={returnTo}
|
||||
characterSlug={characterSlug}
|
||||
sourceCharacterSlug={sourceCharacterSlug}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface UseSubscriptionPaymentFlowInput {
|
||||
shouldResumePendingOrder: boolean;
|
||||
returnTo: SubscriptionReturnTo;
|
||||
initialPayChannel: PayChannel;
|
||||
characterSlug?: string;
|
||||
sourceCharacterSlug?: string;
|
||||
}
|
||||
|
||||
export function useSubscriptionPaymentFlow({
|
||||
@@ -27,7 +27,7 @@ export function useSubscriptionPaymentFlow({
|
||||
shouldResumePendingOrder,
|
||||
returnTo,
|
||||
initialPayChannel,
|
||||
characterSlug = DEFAULT_CHARACTER_SLUG,
|
||||
sourceCharacterSlug = DEFAULT_CHARACTER_SLUG,
|
||||
}: UseSubscriptionPaymentFlowInput) {
|
||||
const router = useRouter();
|
||||
const { payment, paymentDispatch } = usePaymentRouteFlow({
|
||||
@@ -50,7 +50,7 @@ export function useSubscriptionPaymentFlow({
|
||||
const handleBackClick = () => {
|
||||
void (async () => {
|
||||
router.replace(
|
||||
await consumeSubscriptionExitUrl(returnTo, characterSlug),
|
||||
await consumeSubscriptionExitUrl(returnTo, sourceCharacterSlug),
|
||||
);
|
||||
})();
|
||||
};
|
||||
@@ -60,7 +60,10 @@ export function useSubscriptionPaymentFlow({
|
||||
paymentDispatch({ type: "PaymentReset" });
|
||||
void (async () => {
|
||||
router.replace(
|
||||
await resolveSubscriptionSuccessExitUrl(returnTo, characterSlug),
|
||||
await resolveSubscriptionSuccessExitUrl(
|
||||
returnTo,
|
||||
sourceCharacterSlug,
|
||||
),
|
||||
);
|
||||
})();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user