feat(chat): unlock paid messages before payment

This commit is contained in:
2026-06-30 18:56:17 +08:00
parent 5f94105bc6
commit e7a9e7abe5
21 changed files with 774 additions and 31 deletions
+80 -6
View File
@@ -7,11 +7,18 @@ import { useRouter } from "next/navigation";
import { useAuthState } from "@/stores/auth/auth-context";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { useUserState } from "@/stores/user/user-context";
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
import {
openChatInExternalBrowser,
recordExternalBrowserPromptShown,
resolveExternalBrowserPromptEligibility,
} from "@/lib/chat/chat_external_browser";
import {
consumePendingChatUnlock,
peekPendingChatUnlock,
savePendingChatUnlock,
type PendingChatUnlockKind,
} from "@/lib/navigation/chat_unlock_session";
import { MobileShell } from "@/app/_components/core";
@@ -45,6 +52,9 @@ export function ChatScreen() {
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
const isGuest = deriveIsGuest(authState.loginStatus);
const isAuthenticatedUser =
authState.loginStatus !== "notLoggedIn" &&
authState.loginStatus !== "guest";
// 消息数量限制由后端统一返回 lockDetail,前端不再处理本地额度。
const showMessageLimitBanner =
@@ -102,17 +112,79 @@ export function ChatScreen() {
authState.loginStatus,
]);
useEffect(() => {
if (!state.historyLoaded || !isAuthenticatedUser) return;
const pending = peekPendingChatUnlock();
if (!pending || pending.returnUrl !== ROUTES.chat) return;
const consumed = consumePendingChatUnlock();
if (!consumed) return;
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId: consumed.messageId,
kind: consumed.kind,
});
}, [
chatDispatch,
isAuthenticatedUser,
state.historyLoaded,
]);
useEffect(() => {
const request = state.unlockPaywallRequest;
if (!request) return;
savePendingChatUnlock({
messageId: request.messageId,
kind: request.kind,
returnUrl: ROUTES.chat,
stage: "payment",
});
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
router.push(
ROUTE_BUILDERS.subscription(
getInsufficientCreditsSubscriptionType(userState.isVip),
{ returnTo: "chat" },
),
);
}, [
chatDispatch,
router,
state.unlockPaywallRequest,
userState.isVip,
]);
async function handleOpenExternalBrowser(): Promise<void> {
setShowExternalBrowserDialog(false);
await openChatInExternalBrowser();
}
function openChatPaywallSubscription(): void {
router.push(getChatPaywallNavigationUrl(authState.loginStatus));
function requestMessageUnlock(
messageId: string,
kind: PendingChatUnlockKind,
): void {
if (!isAuthenticatedUser) {
savePendingChatUnlock({
messageId,
kind,
returnUrl: ROUTES.chat,
stage: "auth",
});
router.push(ROUTE_BUILDERS.authWithRedirect(ROUTES.chat));
return;
}
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId,
kind,
});
}
function handleUnlockPrivateMessage(): void {
openChatPaywallSubscription();
function handleUnlockPrivateMessage(messageId: string): void {
requestMessageUnlock(messageId, "private");
}
function handleMessageLimitUnlock(): void {
@@ -129,8 +201,8 @@ export function ChatScreen() {
);
}
function handleUnlockVoiceMessage(): void {
openChatPaywallSubscription();
function handleUnlockVoiceMessage(messageId: string): void {
requestMessageUnlock(messageId, "voice");
}
return (
@@ -154,6 +226,8 @@ export function ChatScreen() {
messages={state.messages}
isReplyingAI={state.isReplyingAI}
isGuest={isGuest}
isUnlockingMessage={state.isUnlockingMessage}
unlockingMessageId={state.unlockingMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage}
onUnlockVoiceMessage={handleUnlockVoiceMessage}
/>