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;