refactor(routes): move shared pages to global scope
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildGlobalPageUrl,
|
||||
resolveGlobalRouteContext,
|
||||
resolveGlobalRouteContextValue,
|
||||
} from "../global-route-context";
|
||||
import { ROUTES } from "../routes";
|
||||
|
||||
describe("global route context", () => {
|
||||
it("preserves a known character chat as the return target", () => {
|
||||
const context = resolveGlobalRouteContext("/characters/maya/chat");
|
||||
|
||||
expect(context).toMatchObject({
|
||||
characterSlug: "maya",
|
||||
chatUrl: "/characters/maya/chat",
|
||||
splashUrl: "/characters/maya/splash",
|
||||
});
|
||||
expect(context.sidebarUrl).toBe(
|
||||
"/sidebar?returnTo=%2Fcharacters%2Fmaya%2Fchat",
|
||||
);
|
||||
expect(context.feedbackUrl).toBe(
|
||||
"/feedback?returnTo=%2Fcharacters%2Fmaya%2Fchat",
|
||||
);
|
||||
expect(context.coinsRulesUrl).toBe(
|
||||
"/coins-rules?returnTo=%2Fcharacters%2Fmaya%2Fchat",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses only the first repeated return target", () => {
|
||||
expect(
|
||||
resolveGlobalRouteContextValue([
|
||||
"/characters/nayeli/chat",
|
||||
"/characters/maya/chat",
|
||||
]),
|
||||
).toBe("/characters/nayeli/chat");
|
||||
});
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
null,
|
||||
"",
|
||||
"https://example.com/characters/maya/chat",
|
||||
"//example.com/characters/maya/chat",
|
||||
"/characters/unknown/chat",
|
||||
"/characters/maya/private-room",
|
||||
"/characters/maya/chat?image=1",
|
||||
])("falls back to Elio for an invalid return target", (value) => {
|
||||
expect(resolveGlobalRouteContextValue(value)).toBe(
|
||||
"/characters/elio/chat",
|
||||
);
|
||||
});
|
||||
|
||||
it("sanitizes return targets when building a global page url", () => {
|
||||
expect(buildGlobalPageUrl(ROUTES.sidebar, "https://example.com")).toBe(
|
||||
"/sidebar?returnTo=%2Fcharacters%2Felio%2Fchat",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { LEGACY_GLOBAL_ROUTE_REDIRECTS } from "../legacy-global-route-redirects";
|
||||
|
||||
describe("legacy global route redirects", () => {
|
||||
it("redirects character-scoped global pages without permanent caching", () => {
|
||||
expect(LEGACY_GLOBAL_ROUTE_REDIRECTS).toEqual([
|
||||
{
|
||||
source: "/characters/:characterSlug/sidebar",
|
||||
destination: "/sidebar?returnTo=/characters/:characterSlug/chat",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/characters/:characterSlug/feedback",
|
||||
destination: "/feedback?returnTo=/characters/:characterSlug/chat",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/characters/:characterSlug/coins-rules",
|
||||
destination:
|
||||
"/coins-rules?returnTo=/characters/:characterSlug/chat",
|
||||
permanent: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -59,9 +59,10 @@ describe("navigation resolver", () => {
|
||||
ROUTE_BUILDERS.subscription("topup", {
|
||||
payChannel: "stripe",
|
||||
returnTo: "private-room",
|
||||
sourceCharacterSlug: "maya",
|
||||
}),
|
||||
).toBe(
|
||||
"/subscription?type=topup&payChannel=stripe&returnTo=private-room",
|
||||
"/subscription?type=topup&payChannel=stripe&returnTo=private-room&character=maya",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getRouteAccess, isInternalRoute } from "../route-meta";
|
||||
import { ALL_STATIC_ROUTES, getCharacterRoutes, ROUTES } from "../routes";
|
||||
|
||||
describe("route meta", () => {
|
||||
it("classifies static routes by access level", () => {
|
||||
it("classifies global routes by access level", () => {
|
||||
expect(getRouteAccess(ROUTES.splash)).toBe("authOnly");
|
||||
expect(getRouteAccess(ROUTES.auth)).toBe("authOnly");
|
||||
expect(getRouteAccess(ROUTES.chat)).toBe("guestEntry");
|
||||
@@ -23,13 +23,22 @@ describe("route meta", () => {
|
||||
|
||||
it("classifies character-scoped routes by their page", () => {
|
||||
const routes = getCharacterRoutes("maya");
|
||||
expect(routes).toEqual({
|
||||
splash: "/characters/maya/splash",
|
||||
chat: "/characters/maya/chat",
|
||||
privateRoom: "/characters/maya/private-room",
|
||||
tip: "/characters/maya/tip",
|
||||
});
|
||||
expect(getRouteAccess(routes.splash)).toBe("authOnly");
|
||||
expect(getRouteAccess(routes.chat)).toBe("guestEntry");
|
||||
expect(getRouteAccess(routes.privateRoom)).toBe("guestEntry");
|
||||
expect(getRouteAccess(routes.tip)).toBe("public");
|
||||
expect(getRouteAccess(routes.sidebar)).toBe("session");
|
||||
expect(getRouteAccess(routes.feedback)).toBe("session");
|
||||
expect(getRouteAccess(routes.coinsRules)).toBe("public");
|
||||
});
|
||||
|
||||
it("does not classify removed character-scoped global pages", () => {
|
||||
expect(getRouteAccess("/characters/maya/sidebar")).toBe("public");
|
||||
expect(getRouteAccess("/characters/maya/feedback")).toBe("public");
|
||||
expect(getRouteAccess("/characters/maya/coins-rules")).toBe("public");
|
||||
});
|
||||
|
||||
it("includes private room in static routes", () => {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
DEFAULT_CHARACTER,
|
||||
getCharacterBySlug,
|
||||
type CharacterProfile,
|
||||
} from "@/data/constants/character";
|
||||
|
||||
import { getCharacterRoutes, ROUTES } from "./routes";
|
||||
|
||||
export const GLOBAL_RETURN_TO_PARAM = "returnTo";
|
||||
|
||||
export type GlobalPageRoute =
|
||||
| typeof ROUTES.sidebar
|
||||
| typeof ROUTES.feedback
|
||||
| typeof ROUTES.coinsRules;
|
||||
|
||||
export interface GlobalRouteContext {
|
||||
readonly characterSlug: CharacterProfile["slug"];
|
||||
readonly chatUrl: string;
|
||||
readonly splashUrl: string;
|
||||
readonly sidebarUrl: string;
|
||||
readonly feedbackUrl: string;
|
||||
readonly coinsRulesUrl: string;
|
||||
}
|
||||
|
||||
export type GlobalReturnToValue =
|
||||
| string
|
||||
| readonly string[]
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export function resolveGlobalRouteContext(
|
||||
value?: GlobalReturnToValue,
|
||||
): GlobalRouteContext {
|
||||
const character = resolveReturnCharacter(getFirstValue(value));
|
||||
const characterRoutes = getCharacterRoutes(character.slug);
|
||||
const chatUrl = characterRoutes.chat;
|
||||
|
||||
return Object.freeze({
|
||||
characterSlug: character.slug,
|
||||
chatUrl,
|
||||
splashUrl: characterRoutes.splash,
|
||||
sidebarUrl: buildGlobalPageUrl(ROUTES.sidebar, chatUrl),
|
||||
feedbackUrl: buildGlobalPageUrl(ROUTES.feedback, chatUrl),
|
||||
coinsRulesUrl: buildGlobalPageUrl(ROUTES.coinsRules, chatUrl),
|
||||
});
|
||||
}
|
||||
|
||||
export function buildGlobalPageUrl(
|
||||
route: GlobalPageRoute,
|
||||
returnTo: string,
|
||||
): string {
|
||||
const safeReturnTo = resolveGlobalRouteContextValue(returnTo);
|
||||
const params = new URLSearchParams({
|
||||
[GLOBAL_RETURN_TO_PARAM]: safeReturnTo,
|
||||
});
|
||||
return `${route}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function resolveGlobalRouteContextValue(
|
||||
value?: GlobalReturnToValue,
|
||||
): string {
|
||||
const character = resolveReturnCharacter(getFirstValue(value));
|
||||
return getCharacterRoutes(character.slug).chat;
|
||||
}
|
||||
|
||||
function resolveReturnCharacter(value: string | null): CharacterProfile {
|
||||
const match = value?.match(/^\/characters\/([^/?#]+)\/chat$/);
|
||||
if (!match?.[1]) return DEFAULT_CHARACTER;
|
||||
return getCharacterBySlug(match[1]) ?? DEFAULT_CHARACTER;
|
||||
}
|
||||
|
||||
function getFirstValue(value: GlobalReturnToValue): string | null {
|
||||
if (typeof value === "string") return value;
|
||||
if (Array.isArray(value)) return value[0] ?? null;
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface LegacyGlobalRouteRedirect {
|
||||
readonly source: string;
|
||||
readonly destination: string;
|
||||
readonly permanent: false;
|
||||
}
|
||||
|
||||
export const LEGACY_GLOBAL_ROUTE_REDIRECTS: readonly LegacyGlobalRouteRedirect[] =
|
||||
Object.freeze([
|
||||
{
|
||||
source: "/characters/:characterSlug/sidebar",
|
||||
destination: "/sidebar?returnTo=/characters/:characterSlug/chat",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/characters/:characterSlug/feedback",
|
||||
destination: "/feedback?returnTo=/characters/:characterSlug/chat",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/characters/:characterSlug/coins-rules",
|
||||
destination: "/coins-rules?returnTo=/characters/:characterSlug/chat",
|
||||
permanent: false,
|
||||
},
|
||||
]);
|
||||
@@ -7,7 +7,11 @@ import type {
|
||||
} from "@/data/storage/navigation";
|
||||
|
||||
export type AppSubscriptionType = "vip" | "topup";
|
||||
export type AppSubscriptionReturnTo = "chat" | "private-room" | null;
|
||||
export type AppSubscriptionReturnTo =
|
||||
| "chat"
|
||||
| "private-room"
|
||||
| "sidebar"
|
||||
| null;
|
||||
|
||||
export interface OpenSubscriptionInput {
|
||||
type: AppSubscriptionType;
|
||||
|
||||
@@ -7,7 +7,7 @@ export type RouteAccess =
|
||||
| "realUser"
|
||||
| "authOnly";
|
||||
|
||||
const STATIC_ROUTE_ACCESS: Partial<Record<string, RouteAccess>> = {
|
||||
const GLOBAL_ROUTE_ACCESS: Partial<Record<string, RouteAccess>> = {
|
||||
[ROUTES.root]: "public",
|
||||
[ROUTES.splash]: "authOnly",
|
||||
[ROUTES.auth]: "authOnly",
|
||||
@@ -26,14 +26,11 @@ const CHARACTER_ROUTE_ACCESS: Readonly<Record<string, RouteAccess>> = {
|
||||
chat: "guestEntry",
|
||||
"private-room": "guestEntry",
|
||||
tip: "public",
|
||||
sidebar: "session",
|
||||
feedback: "session",
|
||||
"coins-rules": "public",
|
||||
};
|
||||
|
||||
export function getRouteAccess(pathname: string): RouteAccess {
|
||||
const staticAccess = STATIC_ROUTE_ACCESS[pathname];
|
||||
if (staticAccess) return staticAccess;
|
||||
const globalAccess = GLOBAL_ROUTE_ACCESS[pathname];
|
||||
if (globalAccess) return globalAccess;
|
||||
|
||||
const match = pathname.match(/^\/characters\/[^/]+\/([^/]+)\/?$/);
|
||||
return (match?.[1] && CHARACTER_ROUTE_ACCESS[match[1]]) || "public";
|
||||
|
||||
@@ -38,9 +38,6 @@ export interface CharacterRoutes {
|
||||
readonly chat: string;
|
||||
readonly privateRoom: string;
|
||||
readonly tip: string;
|
||||
readonly sidebar: string;
|
||||
readonly feedback: string;
|
||||
readonly coinsRules: string;
|
||||
}
|
||||
|
||||
export function getCharacterRoutes(
|
||||
@@ -52,9 +49,6 @@ export function getCharacterRoutes(
|
||||
chat: `${root}/chat`,
|
||||
privateRoom: `${root}/private-room`,
|
||||
tip: `${root}/tip`,
|
||||
sidebar: `${root}/sidebar`,
|
||||
feedback: `${root}/feedback`,
|
||||
coinsRules: `${root}/coins-rules`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,15 +80,15 @@ export const ROUTE_BUILDERS = {
|
||||
options: {
|
||||
payChannel?: PayChannel;
|
||||
returnTo?: Exclude<AppSubscriptionReturnTo, null>;
|
||||
characterSlug?: string;
|
||||
sourceCharacterSlug?: string;
|
||||
analytics?: PaymentAnalyticsContext;
|
||||
} = {},
|
||||
): `/subscription?${string}` => {
|
||||
const params = new URLSearchParams({ type });
|
||||
if (options.payChannel) params.set("payChannel", options.payChannel);
|
||||
if (options.returnTo) params.set("returnTo", options.returnTo);
|
||||
if (options.characterSlug) {
|
||||
params.set("character", options.characterSlug);
|
||||
if (options.sourceCharacterSlug) {
|
||||
params.set("character", options.sourceCharacterSlug);
|
||||
}
|
||||
if (options.analytics) {
|
||||
params.set(
|
||||
|
||||
+25
-117
@@ -1,39 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { NavigationStorage } from "@/data/storage/navigation";
|
||||
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
|
||||
import {
|
||||
consumeSubscriptionExitUrl,
|
||||
resolveSubscriptionSuccessExitUrl,
|
||||
} from "@/lib/navigation/subscription_exit";
|
||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||
import {
|
||||
behaviorAnalytics,
|
||||
getDefaultPaymentAnalyticsContext,
|
||||
} from "@/lib/analytics";
|
||||
import { useAuthSelector } from "@/stores/auth/auth-context";
|
||||
import { useUserSelector } from "@/stores/user/user-context";
|
||||
import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
|
||||
import {
|
||||
ROUTE_BUILDERS,
|
||||
type Route,
|
||||
} from "./routes";
|
||||
import {
|
||||
isAuthenticatedUser as resolveIsAuthenticatedUser,
|
||||
resolveAuthenticatedNavigation,
|
||||
} from "./navigation-resolver";
|
||||
import { type Route } from "./routes";
|
||||
import type {
|
||||
OpenSubscriptionForPendingUnlockInput,
|
||||
OpenSubscriptionInput,
|
||||
StartMessageUnlockInput,
|
||||
} from "./navigation-types";
|
||||
import { useGlobalAppNavigator } from "./use-global-app-navigator";
|
||||
|
||||
export interface AppNavigator {
|
||||
push: (href: string, options?: { scroll?: boolean }) => void;
|
||||
@@ -42,96 +23,55 @@ export interface AppNavigator {
|
||||
openAuth: (redirectTo?: string) => void;
|
||||
openChat: (options?: { replace?: boolean; scroll?: boolean }) => void;
|
||||
openSubscription: (input: OpenSubscriptionInput) => void;
|
||||
exitSubscription: (
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug?: string,
|
||||
) => void;
|
||||
exitSubscriptionAfterSuccess: (
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug?: string,
|
||||
) => void;
|
||||
startMessageUnlock: (input: StartMessageUnlockInput) => void;
|
||||
openSubscriptionForPendingUnlock: (
|
||||
input: OpenSubscriptionForPendingUnlockInput,
|
||||
) => void;
|
||||
getDefaultPayChannel: () => ReturnType<typeof getDefaultPayChannelForCountryCode>;
|
||||
getDefaultPayChannel: ReturnType<
|
||||
typeof useGlobalAppNavigator
|
||||
>["getDefaultPayChannel"];
|
||||
isAuthenticatedUser: boolean;
|
||||
}
|
||||
|
||||
export function useAppNavigator(): AppNavigator {
|
||||
const router = useRouter();
|
||||
const globalNavigator = useGlobalAppNavigator();
|
||||
const character = useActiveCharacter();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const loginStatus = useAuthSelector((state) => state.context.loginStatus);
|
||||
const countryCode = useUserSelector(
|
||||
(state) => state.context.currentUser?.countryCode,
|
||||
);
|
||||
const isAuthenticatedUser = resolveIsAuthenticatedUser(loginStatus);
|
||||
|
||||
const push = useCallback((href: string, options?: { scroll?: boolean }): void => {
|
||||
router.push(href, options);
|
||||
}, [router]);
|
||||
|
||||
const replace = useCallback((href: string, options?: { scroll?: boolean }): void => {
|
||||
router.replace(href, options);
|
||||
}, [router]);
|
||||
|
||||
const back = useCallback((): void => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
const {
|
||||
back,
|
||||
getDefaultPayChannel,
|
||||
isAuthenticatedUser,
|
||||
push,
|
||||
replace,
|
||||
} = globalNavigator;
|
||||
|
||||
const openAuth = useCallback(
|
||||
(redirectTo: string = characterRoutes.chat): void => {
|
||||
router.push(ROUTE_BUILDERS.authWithRedirect(redirectTo));
|
||||
globalNavigator.openAuth(redirectTo);
|
||||
},
|
||||
[characterRoutes.chat, router],
|
||||
[characterRoutes.chat, globalNavigator],
|
||||
);
|
||||
|
||||
const openChat = useCallback(
|
||||
(options: { replace?: boolean; scroll?: boolean } = {}): void => {
|
||||
const navOptions = { scroll: options.scroll ?? false };
|
||||
if (options.replace) {
|
||||
router.replace(characterRoutes.chat, navOptions);
|
||||
replace(characterRoutes.chat, navOptions);
|
||||
return;
|
||||
}
|
||||
router.push(characterRoutes.chat, navOptions);
|
||||
push(characterRoutes.chat, navOptions);
|
||||
},
|
||||
[characterRoutes.chat, router],
|
||||
[characterRoutes.chat, push, replace],
|
||||
);
|
||||
|
||||
const getDefaultPayChannel = useCallback(() => {
|
||||
return getDefaultPayChannelForCountryCode(countryCode);
|
||||
}, [countryCode]);
|
||||
|
||||
const openSubscription = useCallback(
|
||||
({
|
||||
type,
|
||||
payChannel = getDefaultPayChannel(),
|
||||
returnTo = null,
|
||||
replace: shouldReplace = false,
|
||||
analytics = getDefaultPaymentAnalyticsContext(type),
|
||||
}: OpenSubscriptionInput): void => {
|
||||
const target = ROUTE_BUILDERS.subscription(type, {
|
||||
payChannel,
|
||||
returnTo: returnTo ?? undefined,
|
||||
characterSlug: character.slug,
|
||||
analytics,
|
||||
(input: OpenSubscriptionInput): void => {
|
||||
globalNavigator.openSubscription({
|
||||
...input,
|
||||
sourceCharacterSlug: character.slug,
|
||||
});
|
||||
|
||||
behaviorAnalytics.rechargeModalOpen(analytics);
|
||||
|
||||
const nextUrl = resolveAuthenticatedNavigation({
|
||||
loginStatus,
|
||||
targetUrl: target,
|
||||
});
|
||||
|
||||
if (shouldReplace) {
|
||||
router.replace(nextUrl);
|
||||
return;
|
||||
}
|
||||
router.push(nextUrl);
|
||||
},
|
||||
[character.slug, getDefaultPayChannel, loginStatus, router],
|
||||
[character.slug, globalNavigator],
|
||||
);
|
||||
|
||||
const startMessageUnlock = useCallback(
|
||||
@@ -163,10 +103,10 @@ export function useAppNavigator(): AppNavigator {
|
||||
returnUrl,
|
||||
stage,
|
||||
});
|
||||
router.push(ROUTE_BUILDERS.authWithRedirect(returnUrl));
|
||||
globalNavigator.openAuth(returnUrl);
|
||||
})();
|
||||
},
|
||||
[character.id, isAuthenticatedUser, router],
|
||||
[character.id, globalNavigator, isAuthenticatedUser],
|
||||
);
|
||||
|
||||
const openSubscriptionForPendingUnlock = useCallback(
|
||||
@@ -205,34 +145,6 @@ export function useAppNavigator(): AppNavigator {
|
||||
[character.id, getDefaultPayChannel, openSubscription],
|
||||
);
|
||||
|
||||
const exitSubscription = useCallback(
|
||||
(
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug: string = character.slug,
|
||||
): void => {
|
||||
void (async () => {
|
||||
router.replace(
|
||||
await consumeSubscriptionExitUrl(returnTo, characterSlug),
|
||||
);
|
||||
})();
|
||||
},
|
||||
[character.slug, router],
|
||||
);
|
||||
|
||||
const exitSubscriptionAfterSuccess = useCallback(
|
||||
(
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug: string = character.slug,
|
||||
): void => {
|
||||
void (async () => {
|
||||
router.replace(
|
||||
await resolveSubscriptionSuccessExitUrl(returnTo, characterSlug),
|
||||
);
|
||||
})();
|
||||
},
|
||||
[character.slug, router],
|
||||
);
|
||||
|
||||
return useMemo(() => ({
|
||||
push,
|
||||
replace,
|
||||
@@ -240,16 +152,12 @@ export function useAppNavigator(): AppNavigator {
|
||||
openAuth,
|
||||
openChat,
|
||||
openSubscription,
|
||||
exitSubscription,
|
||||
exitSubscriptionAfterSuccess,
|
||||
startMessageUnlock,
|
||||
openSubscriptionForPendingUnlock,
|
||||
getDefaultPayChannel,
|
||||
isAuthenticatedUser,
|
||||
}), [
|
||||
back,
|
||||
exitSubscription,
|
||||
exitSubscriptionAfterSuccess,
|
||||
getDefaultPayChannel,
|
||||
isAuthenticatedUser,
|
||||
openAuth,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
behaviorAnalytics,
|
||||
getDefaultPaymentAnalyticsContext,
|
||||
} from "@/lib/analytics";
|
||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||
import { useAuthSelector } from "@/stores/auth/auth-context";
|
||||
import { useUserSelector } from "@/stores/user/user-context";
|
||||
|
||||
import {
|
||||
isAuthenticatedUser as resolveIsAuthenticatedUser,
|
||||
resolveAuthenticatedNavigation,
|
||||
} from "./navigation-resolver";
|
||||
import type { OpenSubscriptionInput } from "./navigation-types";
|
||||
import { ROUTE_BUILDERS } from "./routes";
|
||||
|
||||
export interface OpenGlobalSubscriptionInput extends OpenSubscriptionInput {
|
||||
sourceCharacterSlug: string;
|
||||
}
|
||||
|
||||
export interface GlobalAppNavigator {
|
||||
push: (href: string, options?: { scroll?: boolean }) => void;
|
||||
replace: (href: string, options?: { scroll?: boolean }) => void;
|
||||
back: () => void;
|
||||
openAuth: (redirectTo: string) => void;
|
||||
openSubscription: (input: OpenGlobalSubscriptionInput) => void;
|
||||
getDefaultPayChannel: () => ReturnType<
|
||||
typeof getDefaultPayChannelForCountryCode
|
||||
>;
|
||||
isAuthenticatedUser: boolean;
|
||||
}
|
||||
|
||||
export function useGlobalAppNavigator(): GlobalAppNavigator {
|
||||
const router = useRouter();
|
||||
const loginStatus = useAuthSelector((state) => state.context.loginStatus);
|
||||
const countryCode = useUserSelector(
|
||||
(state) => state.context.currentUser?.countryCode,
|
||||
);
|
||||
const isAuthenticatedUser = resolveIsAuthenticatedUser(loginStatus);
|
||||
|
||||
const push = useCallback(
|
||||
(href: string, options?: { scroll?: boolean }): void => {
|
||||
router.push(href, options);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const replace = useCallback(
|
||||
(href: string, options?: { scroll?: boolean }): void => {
|
||||
router.replace(href, options);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const back = useCallback((): void => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const openAuth = useCallback(
|
||||
(redirectTo: string): void => {
|
||||
router.push(ROUTE_BUILDERS.authWithRedirect(redirectTo));
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const getDefaultPayChannel = useCallback(() => {
|
||||
return getDefaultPayChannelForCountryCode(countryCode);
|
||||
}, [countryCode]);
|
||||
|
||||
const openSubscription = useCallback(
|
||||
({
|
||||
type,
|
||||
sourceCharacterSlug,
|
||||
payChannel = getDefaultPayChannel(),
|
||||
returnTo = null,
|
||||
replace: shouldReplace = false,
|
||||
analytics = getDefaultPaymentAnalyticsContext(type),
|
||||
}: OpenGlobalSubscriptionInput): void => {
|
||||
const target = ROUTE_BUILDERS.subscription(type, {
|
||||
payChannel,
|
||||
returnTo: returnTo ?? undefined,
|
||||
sourceCharacterSlug,
|
||||
analytics,
|
||||
});
|
||||
|
||||
behaviorAnalytics.rechargeModalOpen(analytics);
|
||||
|
||||
const nextUrl = resolveAuthenticatedNavigation({
|
||||
loginStatus,
|
||||
targetUrl: target,
|
||||
});
|
||||
|
||||
if (shouldReplace) {
|
||||
router.replace(nextUrl);
|
||||
return;
|
||||
}
|
||||
router.push(nextUrl);
|
||||
},
|
||||
[getDefaultPayChannel, loginStatus, router],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
push,
|
||||
replace,
|
||||
back,
|
||||
openAuth,
|
||||
openSubscription,
|
||||
getDefaultPayChannel,
|
||||
isAuthenticatedUser,
|
||||
}),
|
||||
[
|
||||
back,
|
||||
getDefaultPayChannel,
|
||||
isAuthenticatedUser,
|
||||
openAuth,
|
||||
openSubscription,
|
||||
push,
|
||||
replace,
|
||||
],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user