feat(chat): render role-bound payment guidance
Docker Image / Build and Push Docker Image (push) Successful in 5m2s

This commit is contained in:
Codex
2026-07-27 18:14:04 +08:00
parent 019caae598
commit 8c0d1ad3ce
49 changed files with 882 additions and 81 deletions
@@ -20,6 +20,7 @@ import {
import type { ChatPromotionState } from "@/stores/chat/helper/promotion";
import type { ChatUnlockPaywallRequest } from "@/stores/chat/chat-state";
import type { UiMessage } from "@/stores/chat/ui-message";
import { recordChatActionEventById } from "@/lib/chat/chat_action_events";
import { useUserSelector } from "@/stores/user/user-context";
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
@@ -48,6 +49,7 @@ export interface ChatUnlockDialogModel {
lockedCount: number;
isLoading: boolean;
errorMessage: string | null;
guidance: import("@/data/schemas/chat").PaymentGuidance | null;
};
closeHistoryUnlock: () => void;
confirmHistoryUnlock: () => void;
@@ -87,6 +89,8 @@ export function useChatUnlockCoordinator({
isUnlockingHistory: state.matches({ userSession: "unlockingHistory" }),
lockedHistoryCount: state.context.lockedHistoryCount,
unlockHistoryError: state.context.unlockHistoryError,
unlockHistoryPaymentGuidance:
state.context.unlockHistoryPaymentGuidance,
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
unlockPaywallRequest: state.context.unlockPaywallRequest,
}),
@@ -97,6 +101,20 @@ export function useChatUnlockCoordinator({
? chatState.unlockPaywallRequest
: null;
useEffect(() => {
const guidance = chatState.unlockHistoryPaymentGuidance;
if (!enabled || guidance?.characterId !== chatState.characterId) return;
void recordChatActionEventById(
guidance.guidanceId,
chatState.characterId,
"viewed",
).catch(() => undefined);
}, [
chatState.characterId,
chatState.unlockHistoryPaymentGuidance,
enabled,
]);
useEffect(() => {
if (!unlockPaywallRequest) {
trackedPaywallRef.current = null;
@@ -120,7 +138,15 @@ export function useChatUnlockCoordinator({
},
{ isVip },
);
}, [isVip, unlockPaywallRequest]);
const guidance = unlockPaywallRequest.paymentGuidance;
if (guidance?.characterId === chatState.characterId) {
void recordChatActionEventById(
guidance.guidanceId,
chatState.characterId,
"viewed",
).catch(() => undefined);
}
}, [chatState.characterId, isVip, unlockPaywallRequest]);
useEffect(() => {
if (!enabled) return;
@@ -212,6 +238,32 @@ export function useChatUnlockCoordinator({
}
function confirmHistoryUnlock(): void {
const guidance =
chatState.unlockHistoryPaymentGuidance?.characterId ===
chatState.characterId
? chatState.unlockHistoryPaymentGuidance
: null;
if (guidance) {
if (guidance.mode !== "guide") return;
void recordChatActionEventById(
guidance.guidanceId,
chatState.characterId,
"opened",
).catch(() => undefined);
navigator.openSubscription({
type:
isVip || !guidance.purchaseOptions.includes("vip")
? "topup"
: "vip",
returnTo: "chat",
chatActionId: guidance.guidanceId,
analytics: {
entryPoint: "chat_unlock",
triggerReason: "insufficient_credits",
},
});
return;
}
chatDispatch({ type: "ChatUnlockHistoryConfirmed" });
}
@@ -221,6 +273,12 @@ export function useChatUnlockCoordinator({
function confirmPaywall(): void {
if (!unlockPaywallRequest) return;
const guidance =
unlockPaywallRequest.paymentGuidance?.characterId ===
chatState.characterId
? unlockPaywallRequest.paymentGuidance
: null;
if (guidance && guidance.mode !== "guide") return;
const returnUrl = resolveChatUnlockReturnUrl(
unlockPaywallRequest,
@@ -231,6 +289,13 @@ export function useChatUnlockCoordinator({
},
);
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
if (guidance) {
void recordChatActionEventById(
guidance.guidanceId,
chatState.characterId,
"opened",
).catch(() => undefined);
}
navigator.openSubscriptionForPendingUnlock({
displayMessageId: unlockPaywallRequest.displayMessageId,
messageId: unlockPaywallRequest.messageId,
@@ -239,13 +304,17 @@ export function useChatUnlockCoordinator({
clientLockId: unlockPaywallRequest.clientLockId,
promotion: unlockPaywallRequest.promotion,
returnUrl,
type: getInsufficientCreditsSubscriptionType(isVip),
type:
isVip || (guidance && !guidance.purchaseOptions.includes("vip"))
? "topup"
: getInsufficientCreditsSubscriptionType(isVip),
analytics: {
entryPoint: "chat_unlock",
triggerReason: unlockPaywallRequest.promotion
? "ad_landing"
: "insufficient_credits",
},
...(guidance ? { chatActionId: guidance.guidanceId } : {}),
});
}
@@ -258,6 +327,7 @@ export function useChatUnlockCoordinator({
lockedCount: chatState.lockedHistoryCount,
isLoading: chatState.isUnlockingHistory,
errorMessage: chatState.unlockHistoryError,
guidance: chatState.unlockHistoryPaymentGuidance,
},
closeHistoryUnlock,
confirmHistoryUnlock,