feat(navigation): add external entry routing

This commit is contained in:
2026-07-09 14:43:28 +08:00
parent 0659e52d29
commit 9159f5346d
17 changed files with 398 additions and 152 deletions
@@ -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,
);
});
});
+110
View File
@@ -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<void> {
const authStorage = AuthStorage.getInstance();
const userStorage = UserStorage.getInstance();
const tasks: Array<Promise<unknown>> = [];
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;
}