From 9159f5346d5a765b71c02aad129498ca2c6d5670 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 9 Jul 2026 14:43:28 +0800 Subject: [PATCH] feat(navigation): add external entry routing --- .../deviceid/[deviceId]/DeepLinkPersist.tsx | 64 ---------- src/app/chat/deviceid/[deviceId]/page.tsx | 35 ------ .../external-entry/external-entry-persist.tsx | 79 +++++++++++++ src/app/external-entry/page.tsx | 61 ++++++++++ .../components/sidebar-wallet-card.module.css | 25 ++++ .../components/sidebar-wallet-card.tsx | 29 +++-- src/app/sidebar/sidebar-screen.tsx | 2 + src/data/storage/auth/auth_storage.ts | 18 ++- src/data/storage/auth/iauth_storage.ts | 6 +- src/data/storage/storage_keys.ts | 1 + src/lib/auth/__tests__/auth_session.test.ts | 3 + src/lib/chat/chat_external_browser.ts | 45 +++---- .../__tests__/external_entry.test.ts | 47 ++++++++ src/lib/navigation/external_entry.ts | 110 ++++++++++++++++++ src/router/__tests__/route-meta.test.ts | 9 +- src/router/route-meta.ts | 2 +- src/router/routes.ts | 14 +-- 17 files changed, 398 insertions(+), 152 deletions(-) delete mode 100644 src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx delete mode 100644 src/app/chat/deviceid/[deviceId]/page.tsx create mode 100644 src/app/external-entry/external-entry-persist.tsx create mode 100644 src/app/external-entry/page.tsx create mode 100644 src/lib/navigation/__tests__/external_entry.test.ts create mode 100644 src/lib/navigation/external_entry.ts diff --git a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx deleted file mode 100644 index 92733bdc..00000000 --- a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -/** - * 深链落地:把 deviceId + fbid/avatarUrl 写进本地存储,然后跳 `/chat`。 - * - * 设计: - * - 不走 Server `redirect()`:避免把 fbid 暴露在 URL 里。 - * - deviceId / fbid / avatarUrl 统一交给 `lib/chat` 落地。 - * - 写入完成后通过统一导航回到 `/chat`,URL 不留深链痕迹。 - */ - -import { useEffect, useState } from "react"; - -import { persistExternalBrowserChatDeepLink } from "@/lib/chat/chat_external_browser"; -import { useAppNavigator } from "@/router/use-app-navigator"; -import { Logger } from "@/utils"; - -const log = new Logger("AppChatDeviceidDeviceIdDeepLinkPersist"); - -interface DeepLinkPersistProps { - deviceId: string; - fbid: string | null; - avatarUrl: string | null; -} - -export default function DeepLinkPersist({ - deviceId, - fbid, - avatarUrl, -}: DeepLinkPersistProps) { - const navigator = useAppNavigator(); - const [done, setDone] = useState(false); - - useEffect(() => { - void (async () => { - try { - await persistExternalBrowserChatDeepLink({ - deviceId, - facebookId: fbid, - avatarUrl, - }); - } catch (err) { - // 写不进 localStorage 时(例如隐私模式)也不阻塞跳转;记录到 console 即可。 - log.warn("[DeepLinkPersist] failed to persist", err); - } finally { - setDone(true); - navigator.openChat({ replace: true }); - } - })(); - }, [avatarUrl, deviceId, fbid, navigator]); - - return ( -
-
-
- ); -} diff --git a/src/app/chat/deviceid/[deviceId]/page.tsx b/src/app/chat/deviceid/[deviceId]/page.tsx deleted file mode 100644 index 74c4ab6a..00000000 --- a/src/app/chat/deviceid/[deviceId]/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 深链入口(Server Component) - * - * 对齐 Flutter `lib/router/app_router.dart:60-78` 的 `GoRoute(path: '/chat/deviceid/:deviceId')`: - * 把 `deviceId` 路径参数与 `fbid` / `avatarUrl` 查询参数透传给 Client 组件 - * ``,由它把数据写入 localStorage 后通过统一导航回到 `/chat`。 - * - * Next.js 15+ 约定:`params` / `searchParams` 是 Promise,必须 await。 - * Next.js 16 全局 helper `PageProps<'/literal'>` 由 `next dev` / `next build` / - * `next typegen` 生成。 - * - * 这里不调用 `redirect()`:把 fbid / avatarUrl 暴露在 URL 上对用户不友好; - * 跳转推迟到 Client 完成持久化之后。 - */ - -import DeepLinkPersist from "./DeepLinkPersist"; - -export default async function ChatDeviceIdPage( - props: PageProps<"/chat/deviceid/[deviceId]">, -) { - const { deviceId } = await props.params; - const { avatarUrl, fbid } = await props.searchParams; - const fbidStr = Array.isArray(fbid) ? fbid[0] : fbid ?? null; - const avatarUrlStr = Array.isArray(avatarUrl) - ? avatarUrl[0] - : avatarUrl ?? null; - - return ( - - ); -} diff --git a/src/app/external-entry/external-entry-persist.tsx b/src/app/external-entry/external-entry-persist.tsx new file mode 100644 index 00000000..efbe5698 --- /dev/null +++ b/src/app/external-entry/external-entry-persist.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +import { + persistExternalEntryPayload, + resolveExternalEntryTarget, +} from "@/lib/navigation/external_entry"; +import { Logger } from "@/utils"; + +const log = new Logger("ExternalEntryPersist"); + +interface ExternalEntryPersistProps { + deviceId: string | null; + facebookId: string | null; + facebookAppId: string | null; + avatarUrl: string | null; + target: string | null; + redirect: string | null; + next: string | null; +} + +export default function ExternalEntryPersist({ + deviceId, + facebookId, + facebookAppId, + avatarUrl, + target, + redirect, + next, +}: ExternalEntryPersistProps) { + const router = useRouter(); + + useEffect(() => { + const destination = resolveExternalEntryTarget({ + target, + redirect, + next, + }); + + void (async () => { + try { + await persistExternalEntryPayload({ + deviceId, + facebookId, + facebookAppId, + avatarUrl, + }); + } catch (error) { + log.warn("[ExternalEntryPersist] failed to persist payload", error); + } finally { + router.replace(destination); + } + })(); + }, [ + avatarUrl, + deviceId, + facebookAppId, + facebookId, + next, + redirect, + router, + target, + ]); + + return ( +
+
+
+ ); +} diff --git a/src/app/external-entry/page.tsx b/src/app/external-entry/page.tsx new file mode 100644 index 00000000..13b26e07 --- /dev/null +++ b/src/app/external-entry/page.tsx @@ -0,0 +1,61 @@ +/** + * 外部入口落地页(Server Component) + * + * 外部应用可以通过 query 传入 Facebook 相关信息和最终目标页,例如: + * `/external-entry?target=chat&fbid=xxx&fb_app_id=yyy` + * + * 页面不直接 `redirect()`,而是把数据交给 Client 组件先写入本地存储, + * 再通过 `router.replace()` 清理 URL 并跳转到最终页面。 + */ + +import ExternalEntryPersist from "./external-entry-persist"; + +type SearchParams = Promise>; + +interface ExternalEntryPageProps { + searchParams: SearchParams; +} + +export default async function ExternalEntryPage({ + searchParams, +}: ExternalEntryPageProps) { + const params = await searchParams; + + return ( + + ); +} + +function pickFirst( + ...values: Array +): string | null { + for (const value of values) { + if (Array.isArray(value)) { + const first = value.find((item) => item.trim().length > 0); + if (first) return first; + continue; + } + if (value && value.trim().length > 0) return value; + } + return null; +} diff --git a/src/app/sidebar/components/sidebar-wallet-card.module.css b/src/app/sidebar/components/sidebar-wallet-card.module.css index 6e1f3342..4eace43f 100644 --- a/src/app/sidebar/components/sidebar-wallet-card.module.css +++ b/src/app/sidebar/components/sidebar-wallet-card.module.css @@ -136,6 +136,14 @@ gap: clamp(8px, 1.852vw, 10px); } +.freeAllowanceContent { + display: flex; + min-width: 0; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} + .freeAllowanceItem { display: inline-flex; align-items: baseline; @@ -153,6 +161,15 @@ font-weight: 900; } +.dailyFreeLimit { + margin: 0; + color: rgba(81, 70, 75, 0.68); + font-size: var(--responsive-caption, 12px); + font-weight: 720; + line-height: 1.2; + text-align: right; +} + .activateButton, .topUpButton { display: inline-flex; @@ -242,4 +259,12 @@ .freeAllowanceItems { justify-content: flex-start; } + + .freeAllowanceContent { + align-items: flex-start; + } + + .dailyFreeLimit { + text-align: left; + } } diff --git a/src/app/sidebar/components/sidebar-wallet-card.tsx b/src/app/sidebar/components/sidebar-wallet-card.tsx index 3dec33dc..8c056223 100644 --- a/src/app/sidebar/components/sidebar-wallet-card.tsx +++ b/src/app/sidebar/components/sidebar-wallet-card.tsx @@ -6,7 +6,9 @@ import styles from "./sidebar-wallet-card.module.css"; export interface SidebarWalletCardProps { creditBalance: number; + dailyFreeChatLimit?: number; dailyFreeChatRemaining?: number; + dailyFreePrivateLimit?: number; dailyFreePrivateRemaining?: number; onActivateVip?: () => void; onTopUp: () => void; @@ -18,7 +20,9 @@ const formatCoins = (value: number): string => export function SidebarWalletCard({ creditBalance, + dailyFreeChatLimit = 0, dailyFreeChatRemaining = 0, + dailyFreePrivateLimit = 0, dailyFreePrivateRemaining = 0, onActivateVip, onTopUp, @@ -63,17 +67,22 @@ export function SidebarWalletCard({
-
+

Free Allowance

-
- - {dailyFreeChatRemaining} - chats left - - - {dailyFreePrivateRemaining} - private left - +
+
+ + {dailyFreeChatRemaining} + chats left + + + {dailyFreePrivateRemaining} + private left + +
+

+ Daily free: {dailyFreeChatLimit} chats / {dailyFreePrivateLimit} private +

diff --git a/src/app/sidebar/sidebar-screen.tsx b/src/app/sidebar/sidebar-screen.tsx index e95ceca3..f17d62fc 100644 --- a/src/app/sidebar/sidebar-screen.tsx +++ b/src/app/sidebar/sidebar-screen.tsx @@ -109,7 +109,9 @@ export function SidebarScreen() {
> { + return SpAsyncUtil.getString(StorageKeys.facebookAppId); + } + setFacebookAppId(id: string): Promise> { + return SpAsyncUtil.setString(StorageKeys.facebookAppId, id); + } + clearFacebookAppId(): Promise> { + return SpAsyncUtil.remove(StorageKeys.facebookAppId); + } + // ---- bulk clear ---- async clearBusinessAuthData(): Promise> { @@ -110,6 +122,8 @@ export class AuthStorage implements IAuthStorage { if (!r2.success) return r2; const r3 = await this.clearFacebookId(); if (!r3.success) return r3; + const r4 = await this.clearFacebookAppId(); + if (!r4.success) return r4; return this.clearLoginProvider(); } @@ -122,7 +136,9 @@ export class AuthStorage implements IAuthStorage { if (!r3.success) return r3; const r4 = await this.clearRefreshToken(); if (!r4.success) return r4; - return this.clearFacebookId(); + const r5 = await this.clearFacebookId(); + if (!r5.success) return r5; + return this.clearFacebookAppId(); } } diff --git a/src/data/storage/auth/iauth_storage.ts b/src/data/storage/auth/iauth_storage.ts index be6adf5e..3a003687 100644 --- a/src/data/storage/auth/iauth_storage.ts +++ b/src/data/storage/auth/iauth_storage.ts @@ -4,7 +4,7 @@ * IAuthStorage 接口 * * 对齐 Dart 端 `IAuthStorage`(lib/data/services/storage/iauth_storage.dart): - * - loginToken / loginProvider / guestToken / deviceId / refreshToken / facebookId 字段的 CRUD + * - loginToken / loginProvider / guestToken / deviceId / refreshToken / facebookId / facebookAppId 字段的 CRUD * - `clearAuthData()` 批量清空登录态 * - `clearBusinessAuthData()` 仅清空真实用户登录态,保留 guest token / deviceId * - 所有方法返回 `Promise>`,与 Dart `Future>` 对齐 @@ -39,6 +39,10 @@ export interface IAuthStorage { setFacebookId(id: string): Promise>; clearFacebookId(): Promise>; + getFacebookAppId(): Promise>; + setFacebookAppId(id: string): Promise>; + clearFacebookAppId(): Promise>; + clearBusinessAuthData(): Promise>; clearAuthData(): Promise>; } diff --git a/src/data/storage/storage_keys.ts b/src/data/storage/storage_keys.ts index 82907cd4..fbba197d 100644 --- a/src/data/storage/storage_keys.ts +++ b/src/data/storage/storage_keys.ts @@ -14,6 +14,7 @@ export const StorageKeys = { deviceId: "device_id", refreshToken: "refresh_token", facebookId: "facebook_id", + facebookAppId: "facebook_app_id", // user user: "user", diff --git a/src/lib/auth/__tests__/auth_session.test.ts b/src/lib/auth/__tests__/auth_session.test.ts index a47b68d9..fc913f69 100644 --- a/src/lib/auth/__tests__/auth_session.test.ts +++ b/src/lib/auth/__tests__/auth_session.test.ts @@ -35,6 +35,9 @@ function makeAuthStorageMock(input: { getFacebookId: vi.fn(async () => Result.ok(null)), setFacebookId: vi.fn(async () => Result.ok(undefined)), clearFacebookId: vi.fn(async () => Result.ok(undefined)), + getFacebookAppId: vi.fn(async () => Result.ok(null)), + setFacebookAppId: vi.fn(async () => Result.ok(undefined)), + clearFacebookAppId: vi.fn(async () => Result.ok(undefined)), clearBusinessAuthData: vi.fn(async () => Result.ok(undefined)), clearAuthData: vi.fn(async () => Result.ok(undefined)), }; diff --git a/src/lib/chat/chat_external_browser.ts b/src/lib/chat/chat_external_browser.ts index 526a5325..fe3c2d27 100644 --- a/src/lib/chat/chat_external_browser.ts +++ b/src/lib/chat/chat_external_browser.ts @@ -3,7 +3,7 @@ import { AppStorage } from "@/data/storage/app/app_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; -import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; +import { ROUTES } from "@/router/routes"; import { todayString, UrlLauncherUtil } from "@/utils"; export async function resolveExternalBrowserPromptEligibility(): Promise<{ @@ -27,42 +27,31 @@ export function recordExternalBrowserPromptShown( export async function openChatInExternalBrowser(): Promise { const authStorage = AuthStorage.getInstance(); const userStorage = UserStorage.getInstance(); - const [deviceIdR, facebookIdR, avatarUrlR] = await Promise.all([ - authStorage.getDeviceId(), - authStorage.getFacebookId(), - userStorage.getAvatarUrl(), - ]); + const [deviceIdR, facebookIdR, facebookAppIdR, avatarUrlR] = + await Promise.all([ + authStorage.getDeviceId(), + authStorage.getFacebookId(), + authStorage.getFacebookAppId(), + userStorage.getAvatarUrl(), + ]); const deviceId = deviceIdR.success ? deviceIdR.data : null; const facebookId = facebookIdR.success ? facebookIdR.data : null; + const facebookAppId = facebookAppIdR.success ? facebookAppIdR.data : null; const avatarUrl = avatarUrlR.success ? avatarUrlR.data : null; if (deviceId && facebookId) { - UrlLauncherUtil.openUrlWithExternalBrowser( - ROUTE_BUILDERS.chatDeviceId(deviceId), - { - queryParams: { - fbid: facebookId, - ...(avatarUrl ? { avatarUrl } : {}), - }, + UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.externalEntry, { + queryParams: { + target: "chat", + deviceId, + fbid: facebookId, + ...(facebookAppId ? { fb_app_id: facebookAppId } : {}), + ...(avatarUrl ? { avatarUrl } : {}), }, - ); + }); return; } UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash); } - -export async function persistExternalBrowserChatDeepLink(input: { - deviceId: string; - facebookId?: string | null; - avatarUrl?: string | null; -}): Promise { - await AuthStorage.getInstance().setDeviceId(input.deviceId); - if (input.facebookId && input.facebookId.length > 0) { - await AuthStorage.getInstance().setFacebookId(input.facebookId); - } - if (input.avatarUrl && input.avatarUrl.length > 0) { - await UserStorage.getInstance().setAvatarUrl(input.avatarUrl); - } -} diff --git a/src/lib/navigation/__tests__/external_entry.test.ts b/src/lib/navigation/__tests__/external_entry.test.ts new file mode 100644 index 00000000..b0a29282 --- /dev/null +++ b/src/lib/navigation/__tests__/external_entry.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; + +import { ROUTES } from "@/router/routes"; + +import { resolveExternalEntryTarget } from "../external_entry"; + +describe("external entry navigation", () => { + it("defaults to chat", () => { + expect(resolveExternalEntryTarget({})).toBe(ROUTES.chat); + }); + + it("resolves supported target aliases", () => { + expect(resolveExternalEntryTarget({ target: "chat" })).toBe(ROUTES.chat); + expect(resolveExternalEntryTarget({ target: "tip" })).toBe(ROUTES.tip); + expect(resolveExternalEntryTarget({ target: "coffee" })).toBe(ROUTES.tip); + expect(resolveExternalEntryTarget({ target: "private-room" })).toBe( + ROUTES.privateRoom, + ); + expect(resolveExternalEntryTarget({ target: "private_room" })).toBe( + ROUTES.privateRoom, + ); + }); + + it("prefers safe explicit redirect routes over target aliases", () => { + expect( + resolveExternalEntryTarget({ + target: "chat", + redirect: ROUTES.tip, + }), + ).toBe(ROUTES.tip); + }); + + it("rejects unsupported and external redirect routes", () => { + expect( + resolveExternalEntryTarget({ + redirect: "https://example.com/chat", + target: "unknown", + }), + ).toBe(ROUTES.chat); + expect(resolveExternalEntryTarget({ redirect: "//example.com" })).toBe( + ROUTES.chat, + ); + expect(resolveExternalEntryTarget({ redirect: "/sidebar" })).toBe( + ROUTES.chat, + ); + }); +}); diff --git a/src/lib/navigation/external_entry.ts b/src/lib/navigation/external_entry.ts new file mode 100644 index 00000000..8a12572d --- /dev/null +++ b/src/lib/navigation/external_entry.ts @@ -0,0 +1,110 @@ +"use client"; + +import { AuthStorage } from "@/data/storage/auth/auth_storage"; +import { UserStorage } from "@/data/storage/user/user_storage"; +import { ROUTES } from "@/router/routes"; + +export type ExternalEntryTarget = + | typeof ROUTES.chat + | typeof ROUTES.tip + | typeof ROUTES.privateRoom; + +export interface ExternalEntryPayload { + deviceId?: string | null; + facebookId?: string | null; + facebookAppId?: string | null; + avatarUrl?: string | null; +} + +export interface ExternalEntryTargetInput { + target?: string | null; + redirect?: string | null; + next?: string | null; +} + +export function resolveExternalEntryTarget({ + target, + redirect, + next, +}: ExternalEntryTargetInput): ExternalEntryTarget { + return ( + resolveExplicitRoute(redirect) ?? + resolveExplicitRoute(next) ?? + resolveTargetAlias(target) ?? + ROUTES.chat + ); +} + +export async function persistExternalEntryPayload({ + deviceId, + facebookId, + facebookAppId, + avatarUrl, +}: ExternalEntryPayload): Promise { + const authStorage = AuthStorage.getInstance(); + const userStorage = UserStorage.getInstance(); + const tasks: Array> = []; + + if (hasValue(deviceId)) { + tasks.push(authStorage.setDeviceId(deviceId)); + } + if (hasValue(facebookId)) { + tasks.push(authStorage.setFacebookId(facebookId)); + } + if (hasValue(facebookAppId)) { + tasks.push(authStorage.setFacebookAppId(facebookAppId)); + } + if (hasValue(avatarUrl)) { + tasks.push(userStorage.setAvatarUrl(avatarUrl)); + } + + await Promise.all(tasks); +} + +function resolveExplicitRoute( + value: string | null | undefined, +): ExternalEntryTarget | null { + if (!value) return null; + const route = normalizeRoute(value); + if ( + route === ROUTES.chat || + route === ROUTES.tip || + route === ROUTES.privateRoom + ) { + return route; + } + return null; +} + +function resolveTargetAlias( + value: string | null | undefined, +): ExternalEntryTarget | null { + if (!value) return null; + const target = value.trim().toLowerCase(); + + if (target === "chat") return ROUTES.chat; + if (target === "tip" || target === "tips" || target === "coffee") { + return ROUTES.tip; + } + if ( + target === "private-room" || + target === "private_room" || + target === "privateroom" || + target === "private" || + target === "room" + ) { + return ROUTES.privateRoom; + } + + return null; +} + +function normalizeRoute(value: string): string { + const trimmed = value.trim(); + if (!trimmed.startsWith("/") || trimmed.startsWith("//")) return ""; + return trimmed.split(/[?#]/, 1)[0] ?? ""; +} + +function hasValue(value: string | null | undefined): value is string { + return typeof value === "string" && value.trim().length > 0; +} diff --git a/src/router/__tests__/route-meta.test.ts b/src/router/__tests__/route-meta.test.ts index 90ee9b74..5a78f0cb 100644 --- a/src/router/__tests__/route-meta.test.ts +++ b/src/router/__tests__/route-meta.test.ts @@ -10,15 +10,12 @@ describe("route meta", () => { expect(getRouteAccess(ROUTES.chat)).toBe("guestEntry"); expect(getRouteAccess(ROUTES.privateRoom)).toBe("guestEntry"); expect(getRouteAccess(ROUTES.tip)).toBe("public"); + expect(getRouteAccess(ROUTES.externalEntry)).toBe("public"); expect(getRouteAccess(ROUTES.sidebar)).toBe("session"); expect(getRouteAccess(ROUTES.subscription)).toBe("realUser"); expect(getRouteAccess(ROUTES.coinsRules)).toBe("public"); }); - it("classifies known dynamic chat routes", () => { - expect(getRouteAccess("/chat/deviceid/device_1")).toBe("public"); - }); - it("does not keep the removed chat image route as a guarded route", () => { expect(getRouteAccess("/chat/image/msg_1")).toBe("public"); }); @@ -31,6 +28,10 @@ describe("route meta", () => { expect(ALL_STATIC_ROUTES).toContain(ROUTES.tip); }); + it("includes external entry in static routes", () => { + expect(ALL_STATIC_ROUTES).toContain(ROUTES.externalEntry); + }); + it("treats unknown routes as public by default", () => { expect(getRouteAccess("/unknown")).toBe("public"); }); diff --git a/src/router/route-meta.ts b/src/router/route-meta.ts index d8a63798..4371b7d6 100644 --- a/src/router/route-meta.ts +++ b/src/router/route-meta.ts @@ -14,13 +14,13 @@ const STATIC_ROUTE_ACCESS: Partial> = { [ROUTES.chat]: "guestEntry", [ROUTES.privateRoom]: "guestEntry", [ROUTES.tip]: "public", + [ROUTES.externalEntry]: "public", [ROUTES.sidebar]: "session", [ROUTES.subscription]: "realUser", [ROUTES.coinsRules]: "public", }; export function getRouteAccess(pathname: string): RouteAccess { - if (pathname.startsWith("/chat/deviceid/")) return "public"; return STATIC_ROUTE_ACCESS[pathname] ?? "public"; } diff --git a/src/router/routes.ts b/src/router/routes.ts index f797043b..3123aa0a 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -4,7 +4,7 @@ * 关键设计: * - `as const` 让每个值都是字符串字面量类型,可用于 `PageProps<'/literal'>`。 * - 不导出 Client-only 模块(`use-auth-gate` 走独立路径导入),保证 Server bundle 不被污染。 - * - 动态深链通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。 + * - 动态查询路由通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。 */ import type { PayChannel } from "@/data/dto/payment"; @@ -16,6 +16,7 @@ export const ROUTES = { chat: "/chat", privateRoom: "/private-room", tip: "/tip", + externalEntry: "/external-entry", auth: "/auth", sidebar: "/sidebar", subscription: "/subscription", @@ -24,10 +25,8 @@ export const ROUTES = { export type StaticRoute = (typeof ROUTES)[keyof typeof ROUTES]; -/** 动态深链构造器 */ +/** 动态查询路由构造器 */ export const ROUTE_BUILDERS = { - chatDeviceId: (deviceId: string): `/chat/deviceid/${string}` => - `/chat/deviceid/${encodeURIComponent(deviceId)}` as const, subscription: ( type: "vip" | "topup", options: { payChannel?: PayChannel; returnTo?: "chat" } = {}, @@ -47,12 +46,11 @@ export const ALL_STATIC_ROUTES: readonly StaticRoute[] = [ ROUTES.chat, ROUTES.privateRoom, ROUTES.tip, + ROUTES.externalEntry, ROUTES.auth, ROUTES.sidebar, ROUTES.coinsRules, ] as const; -/** 联合路由类型,包含静态路由与已知动态路由 */ -export type Route = - | StaticRoute - | `/chat/deviceid/${string}`; +/** 联合路由类型 */ +export type Route = StaticRoute;