refactor(app): tighten chat and sync boundaries

This commit is contained in:
2026-07-01 14:03:25 +08:00
parent f6e7adbe96
commit 760e4fe343
19 changed files with 215 additions and 677 deletions
@@ -0,0 +1,164 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { ROUTE_BUILDERS } from "@/router/routes";
import { useAuthState } from "@/stores/auth/auth-context";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import type { ChatUnlockPaywallRequest } from "@/stores/chat/chat-state";
import { useUserState } from "@/stores/user/user-context";
import {
consumePendingChatUnlock,
peekPendingChatUnlock,
savePendingChatUnlock,
type PendingChatUnlock,
type PendingChatUnlockKind,
} from "@/lib/navigation/chat_unlock_session";
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
export interface UseChatUnlockNavigationFlowInput {
returnUrl: string;
expectedKind?: PendingChatUnlockKind;
expectedMessageId?: string;
}
export interface UseChatUnlockNavigationFlowOutput {
unlockPaywallRequest: ChatUnlockPaywallRequest | null;
requestMessageUnlock: (
messageId: string,
kind: PendingChatUnlockKind,
) => void;
closeInsufficientCreditsDialog: () => void;
confirmInsufficientCreditsDialog: () => void;
}
export function useChatUnlockNavigationFlow({
returnUrl,
expectedKind,
expectedMessageId,
}: UseChatUnlockNavigationFlowInput): UseChatUnlockNavigationFlowOutput {
const router = useRouter();
const authState = useAuthState();
const userState = useUserState();
const chatState = useChatState();
const chatDispatch = useChatDispatch();
const isAuthenticatedUser =
authState.loginStatus !== "notLoggedIn" &&
authState.loginStatus !== "guest";
const unlockPaywallRequest = getScopedUnlockPaywallRequest({
request: chatState.unlockPaywallRequest,
expectedKind,
expectedMessageId,
});
useEffect(() => {
if (!chatState.historyLoaded || !isAuthenticatedUser) return;
const pending = peekPendingChatUnlock();
if (
!pending ||
pending.returnUrl !== returnUrl ||
!matchesPendingUnlockScope({
pending,
expectedKind,
expectedMessageId,
})
) {
return;
}
const consumed = consumePendingChatUnlock();
if (!consumed) return;
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId: consumed.messageId,
kind: consumed.kind,
});
}, [
chatDispatch,
chatState.historyLoaded,
expectedKind,
expectedMessageId,
isAuthenticatedUser,
returnUrl,
]);
function requestMessageUnlock(
messageId: string,
kind: PendingChatUnlockKind,
): void {
if (!isAuthenticatedUser) {
savePendingChatUnlock({
messageId,
kind,
returnUrl,
stage: "auth",
});
router.push(ROUTE_BUILDERS.authWithRedirect(returnUrl));
return;
}
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId,
kind,
});
}
function closeInsufficientCreditsDialog(): void {
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
}
function confirmInsufficientCreditsDialog(): void {
if (!unlockPaywallRequest) return;
savePendingChatUnlock({
messageId: unlockPaywallRequest.messageId,
kind: unlockPaywallRequest.kind,
returnUrl,
stage: "payment",
});
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
router.push(
ROUTE_BUILDERS.subscription(
getInsufficientCreditsSubscriptionType(userState.isVip),
{ returnTo: "chat" },
),
);
}
return {
unlockPaywallRequest,
requestMessageUnlock,
closeInsufficientCreditsDialog,
confirmInsufficientCreditsDialog,
};
}
function getScopedUnlockPaywallRequest(input: {
request: ChatUnlockPaywallRequest | null;
expectedKind?: PendingChatUnlockKind;
expectedMessageId?: string;
}): ChatUnlockPaywallRequest | null {
const { request, expectedKind, expectedMessageId } = input;
if (!request) return null;
if (expectedKind && request.kind !== expectedKind) return null;
if (expectedMessageId && request.messageId !== expectedMessageId) return null;
return request;
}
function matchesPendingUnlockScope(input: {
pending: PendingChatUnlock;
expectedKind?: PendingChatUnlockKind;
expectedMessageId?: string;
}): boolean {
const { pending, expectedKind, expectedMessageId } = input;
if (expectedKind && pending.kind !== expectedKind) return false;
if (expectedMessageId && pending.messageId !== expectedMessageId) {
return false;
}
return true;
}