feat(chat): add promotional external entry flow

This commit is contained in:
2026-07-13 12:43:18 +08:00
parent e5ee9940ca
commit 3752b3b729
44 changed files with 1623 additions and 190 deletions
+62 -5
View File
@@ -34,6 +34,7 @@ import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-ba
import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow";
import { useChatGuestLogin } from "./hooks/use-chat-guest-login";
import { useChatMessageLimitBanner } from "./hooks/use-chat-message-limit-banner";
import { useChatPromotionBootstrap } from "./hooks/use-chat-promotion-bootstrap";
import styles from "./components/chat-screen.module.css";
export function ChatScreen() {
@@ -42,6 +43,10 @@ export function ChatScreen() {
const state = useChatState();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
const isPromotionBootstrapReady = useChatPromotionBootstrap();
const visibleMessages = isPromotionBootstrapReady
? state.messages
: state.historyMessages;
const imageMessageId = getChatImageOverlayMessageId(searchParams);
const imageReturnUrl = imageMessageId
? buildChatImageOverlayUrl(imageMessageId)
@@ -54,6 +59,8 @@ export function ChatScreen() {
} = useChatUnlockNavigationFlow({
returnUrl: ROUTES.chat,
ignoredKind: "image",
promotionScope: "exclude",
enabled: isPromotionBootstrapReady,
});
const {
unlockPaywallRequest: imageUnlockPaywallRequest,
@@ -64,16 +71,28 @@ export function ChatScreen() {
returnUrl: imageReturnUrl,
expectedKind: "image",
expectedMessageId: imageMessageId ?? undefined,
enabled: imageMessageId !== null,
promotionScope: "exclude",
enabled: isPromotionBootstrapReady && imageMessageId !== null,
});
const {
unlockPaywallRequest: promotionUnlockPaywallRequest,
requestMessageUnlock: requestPromotionUnlock,
closeInsufficientCreditsDialog: closePromotionInsufficientCreditsDialog,
confirmInsufficientCreditsDialog:
confirmPromotionInsufficientCreditsDialog,
} = useChatUnlockNavigationFlow({
returnUrl: ROUTES.chat,
promotionScope: "only",
enabled: isPromotionBootstrapReady && state.promotion !== null,
});
const selectedImageMessage = useMemo(
() =>
imageMessageId
? state.messages.find(
? visibleMessages.find(
(item) => item.id === imageMessageId && item.imageUrl,
) ?? null
: null,
[imageMessageId, state.messages],
[imageMessageId, visibleMessages],
);
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
@@ -89,7 +108,8 @@ export function ChatScreen() {
historyLoaded: state.historyLoaded,
loginStatus: authState.loginStatus,
});
const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10;
const shouldShowPwaInstall =
state.historyLoaded && state.historyMessages.length >= 10;
const shouldShowBrowserHint = shouldStartExternalBrowserPrompt({
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
@@ -119,13 +139,38 @@ export function ChatScreen() {
]);
function handleUnlockPrivateMessage(messageId: string): void {
if (requestPromotionMessageUnlock(messageId, "private")) return;
requestMessageUnlock(messageId, "private");
}
function handleUnlockVoiceMessage(messageId: string): void {
if (requestPromotionMessageUnlock(messageId, "voice")) return;
requestMessageUnlock(messageId, "voice");
}
function handleUnlockImageMessage(messageId: string): void {
if (requestPromotionMessageUnlock(messageId, "image")) return;
requestMessageUnlock(messageId, "image");
}
function requestPromotionMessageUnlock(
messageId: string,
kind: "private" | "voice" | "image",
): boolean {
const promotion = state.promotion;
if (!promotion || promotion.message.id !== messageId) return false;
const temporaryMessageId = `promotion:${promotion.session.clientLockId}`;
requestPromotionUnlock(messageId, kind, {
remoteMessageId:
messageId === temporaryMessageId ? undefined : messageId,
lockType: promotion.session.lockType,
clientLockId: promotion.session.clientLockId,
promotion: promotion.session,
});
return true;
}
function handleOpenImage(messageId: string): void {
router.push(buildChatImageOverlayUrl(messageId), { scroll: false });
}
@@ -169,12 +214,13 @@ export function ChatScreen() {
/>
<ChatArea
messages={state.messages}
messages={visibleMessages}
isReplyingAI={state.isReplyingAI}
isUnlockingMessage={state.isUnlockingMessage}
unlockingMessageId={state.unlockingMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage}
onUnlockVoiceMessage={handleUnlockVoiceMessage}
onUnlockImageMessage={handleUnlockImageMessage}
onOpenImage={handleOpenImage}
/>
@@ -201,8 +247,19 @@ export function ChatScreen() {
onCloseInsufficientCreditsDialog={closeInsufficientCreditsDialog}
onConfirmInsufficientCreditsDialog={confirmInsufficientCreditsDialog}
/>
<ChatUnlockDialogs
unlockPaywallRequest={promotionUnlockPaywallRequest}
includeHistoryUnlock={false}
onCloseInsufficientCreditsDialog={
closePromotionInsufficientCreditsDialog
}
onConfirmInsufficientCreditsDialog={
confirmPromotionInsufficientCreditsDialog
}
/>
<ChatUnlockDialogs
unlockPaywallRequest={imageUnlockPaywallRequest}
includeHistoryUnlock={false}
onCloseInsufficientCreditsDialog={closeImageInsufficientCreditsDialog}
onConfirmInsufficientCreditsDialog={
confirmImageInsufficientCreditsDialog