refactor(router): introduce app navigation manager

This commit is contained in:
2026-07-03 14:09:13 +08:00
parent 8a586e4471
commit 91dde42f92
23 changed files with 669 additions and 277 deletions
+16 -65
View File
@@ -1,31 +1,17 @@
"use client";
import { z } from "zod";
import {
NavigationStorage,
type PendingChatUnlock,
type PendingChatUnlockKind,
type PendingChatUnlockStage,
} from "@/data/storage/navigation";
import { StorageKeys } from "@/data/storage/storage_keys";
import { Result, SessionAsyncUtil } from "@/utils";
const MAX_AGE_MS = 30 * 60 * 1000;
export type PendingChatUnlockKind = "private" | "voice" | "image";
export type PendingChatUnlockStage = "auth" | "payment";
const PendingChatUnlockSchema = z.object({
reason: z.literal("single_message_unlock"),
messageId: z.string().min(1),
kind: z.enum(["private", "voice", "image"]),
returnUrl: z
.string()
.min(1)
.refine(
(value) => value.startsWith("/") && !value.startsWith("//"),
"returnUrl must be an internal route",
),
stage: z.enum(["auth", "payment"]),
createdAt: z.number(),
});
export type PendingChatUnlock = z.output<typeof PendingChatUnlockSchema>;
export type {
PendingChatUnlock,
PendingChatUnlockKind,
PendingChatUnlockStage,
};
export async function savePendingChatUnlock(input: {
messageId: string;
@@ -33,56 +19,21 @@ export async function savePendingChatUnlock(input: {
returnUrl: string;
stage: PendingChatUnlockStage;
}): Promise<void> {
const payload: PendingChatUnlock = {
reason: "single_message_unlock",
messageId: input.messageId,
kind: input.kind,
returnUrl: input.returnUrl,
stage: input.stage,
createdAt: Date.now(),
};
await SessionAsyncUtil.setJson(
StorageKeys.pendingChatUnlock,
payload,
PendingChatUnlockSchema,
);
await NavigationStorage.savePendingChatUnlock(input);
}
export async function consumePendingChatUnlock(): Promise<PendingChatUnlock | null> {
const result = await SessionAsyncUtil.getJson(
StorageKeys.pendingChatUnlock,
PendingChatUnlockSchema,
);
await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
if (Result.isErr(result)) return null;
return parsePendingChatUnlock(result.data);
return NavigationStorage.consumePendingChatUnlock();
}
export async function peekPendingChatUnlock(): Promise<PendingChatUnlock | null> {
const result = await SessionAsyncUtil.getJson(
StorageKeys.pendingChatUnlock,
PendingChatUnlockSchema,
);
if (Result.isErr(result)) return null;
const value = parsePendingChatUnlock(result.data);
if (!value) {
await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
}
return value;
return NavigationStorage.peekPendingChatUnlock();
}
export async function hasPendingChatUnlock(): Promise<boolean> {
return (await peekPendingChatUnlock()) !== null;
return NavigationStorage.hasPendingChatUnlock();
}
export async function clearPendingChatUnlock(): Promise<void> {
await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
}
function parsePendingChatUnlock(
value: PendingChatUnlock | null,
): PendingChatUnlock | null {
if (!value) return null;
if (Date.now() - value.createdAt > MAX_AGE_MS) return null;
return value;
await NavigationStorage.clearPendingChatUnlock();
}