refactor(chat): remove weekly limit flow

This commit is contained in:
2026-07-01 13:12:14 +08:00
parent d1ccf3631e
commit f6e7adbe96
9 changed files with 33 additions and 89 deletions
+5 -16
View File
@@ -58,19 +58,13 @@ export function ChatScreen() {
authState.loginStatus !== "notLoggedIn" && authState.loginStatus !== "notLoggedIn" &&
authState.loginStatus !== "guest"; authState.loginStatus !== "guest";
// 消息数量限制由后端统一返回 lockDetail,前端不再处理本地额度 // 发送能力由后端统一返回,当前只处理积分不足引导
const showMessageLimitBanner = const showMessageLimitBanner =
state.upgradePromptVisible && state.upgradePromptVisible &&
(state.upgradeReason === "weekly_limit" || state.upgradeReason === "insufficient_credits";
state.upgradeReason === "insufficient_credits");
const messageLimitTitle = const messageLimitTitle =
state.upgradeReason === "insufficient_credits" "Insufficient credits\nTop up to continue chatting";
? "Insufficient credits\nTop up to continue chatting" const messageLimitCtaLabel = "Top up credits to continue";
: "The limit for free chat times\nhas been reached";
const messageLimitCtaLabel =
state.upgradeReason === "insufficient_credits"
? "Top up credits to continue"
: "Unlock your membership to continue";
const externalBrowserPromptShownRef = useRef(false); const externalBrowserPromptShownRef = useRef(false);
@@ -188,15 +182,10 @@ export function ChatScreen() {
} }
function handleMessageLimitUnlock(): void { function handleMessageLimitUnlock(): void {
const subscriptionType =
state.upgradeReason === "insufficient_credits"
? getInsufficientCreditsSubscriptionType(userState.isVip)
: "vip";
router.push( router.push(
getChatPaywallNavigationUrl( getChatPaywallNavigationUrl(
authState.loginStatus, authState.loginStatus,
subscriptionType, getInsufficientCreditsSubscriptionType(userState.isVip),
), ),
); );
} }
@@ -1,14 +1,14 @@
"use client"; "use client";
/** /**
* ChatQuotaExhaustedBanner 游客配额耗尽横幅 * ChatQuotaExhaustedBanner 发送受限横幅
* *
* 何时显示: * 何时显示:
* - 后端返回 lockDetail.reason="weekly_limit" 且 showUpgrade=true * - 后端返回 cannotSendReason="insufficient_credits"
* *
* 视觉规格(与设计稿对齐): * 视觉规格(与设计稿对齐):
* - 粉→品红渐变背景(与 splash 渐变一致) * - 粉→品红渐变背景(与 splash 渐变一致)
* - 大字号粉色/白色提示文案"The limit for free chat times has been reached today" * - 大字号粉色/白色提示文案
* - 粉→品红渐变 pill 按钮"Unlock your membership to continue" → 跳 /subscription * - 粉→品红渐变 pill 按钮 → 跳 /subscription
* *
* 业务关系: * 业务关系:
* - 与 `src/app/sidebar/components/vip-benefits-card.tsx` 的 "Activate VIP Membership" 行为一致 * - 与 `src/app/sidebar/components/vip-benefits-card.tsx` 的 "Activate VIP Membership" 行为一致
@@ -36,8 +36,8 @@ export interface ChatQuotaExhaustedBannerProps {
} }
export function ChatQuotaExhaustedBanner({ export function ChatQuotaExhaustedBanner({
title = "The limit for free chat times\nhas been reached today", title = "Insufficient credits\nTop up to continue chatting",
ctaLabel = "Unlock your membership to continue", ctaLabel = "Top up credits to continue",
onUnlock, onUnlock,
}: ChatQuotaExhaustedBannerProps) { }: ChatQuotaExhaustedBannerProps) {
const router = useRouter(); const router = useRouter();
+1 -1
View File
@@ -14,5 +14,5 @@ export class ChatStorage {
} }
// 本地游客消息数量额度逻辑已停用: // 本地游客消息数量额度逻辑已停用:
// 游客 / 非游客的消息数量限制统一后端接口返回 lockDetail/daily_limit // 游客 / 非游客的发送能力统一后端返回的积分状态为准
} }
@@ -143,43 +143,6 @@ describe("sendResponseToUiMessage", () => {
}); });
describe("applyHttpSendOutput", () => { describe("applyHttpSendOutput", () => {
it("removes the optimistic user message when the weekly limit is reached", () => {
const context = makeChatState({
messages: [
{
content: "hello",
isFromAI: false,
date: "2026-06-25",
},
],
});
const nextState = applyHttpSendOutput(context, {
response: makeResponse({
reply: "",
messageId: "",
lockDetail: {
locked: true,
showContent: false,
showUpgrade: true,
reason: "weekly_limit",
hint: "Free message limit reached.",
detail: {
type: "weekly_msg_limit",
usedThisWeek: 3,
limit: 3,
},
},
}),
reply: null,
});
expect(nextState.messages).toEqual([]);
expect(nextState.isReplyingAI).toBe(false);
expect(nextState.upgradePromptVisible).toBe(true);
expect(nextState.upgradeReason).toBe("weekly_limit");
});
it("stores insufficient credit state and removes failed optimistic messages", () => { it("stores insufficient credit state and removes failed optimistic messages", () => {
const context = makeChatState({ const context = makeChatState({
messages: [ messages: [
@@ -363,7 +326,7 @@ describe("countLockedHistoryMessages", () => {
isFromAI: true, isFromAI: true,
date: "2026-06-29", date: "2026-06-29",
locked: true, locked: true,
lockReason: "weekly_limit", lockReason: "insufficient_credits",
}, },
{ {
content: "user text", content: "user text",
@@ -767,7 +767,7 @@ describe("chatMachine transitions", () => {
actor.stop(); actor.stop();
}); });
it("clears the weekly limit prompt after payment succeeds", async () => { it("clears the insufficient credits prompt after payment succeeds", async () => {
const actor = createActor(createTestChatMachine()).start(); const actor = createActor(createTestChatMachine()).start();
actor.send({ type: "ChatUserLogin", token: "token" }); actor.send({ type: "ChatUserLogin", token: "token" });
@@ -789,12 +789,12 @@ describe("chatMachine transitions", () => {
locked: true, locked: true,
showContent: false, showContent: false,
showUpgrade: true, showUpgrade: true,
reason: "weekly_limit", reason: "insufficient_credits",
hint: "Free message limit reached.", hint: "Insufficient credits.",
detail: { detail: {
type: "weekly_msg_limit", requiredCredits: 2,
usedThisWeek: 3, currentCredits: 0,
limit: 3, shortfallCredits: 2,
}, },
}, },
canSendMessage: false, canSendMessage: false,
@@ -808,7 +808,9 @@ describe("chatMachine transitions", () => {
}); });
expect(actor.getSnapshot().context.upgradePromptVisible).toBe(true); expect(actor.getSnapshot().context.upgradePromptVisible).toBe(true);
expect(actor.getSnapshot().context.upgradeReason).toBe("weekly_limit"); expect(actor.getSnapshot().context.upgradeReason).toBe(
"insufficient_credits",
);
expect(actor.getSnapshot().context.canSendMessage).toBe(false); expect(actor.getSnapshot().context.canSendMessage).toBe(false);
actor.send({ type: "ChatPaymentSucceeded" }); actor.send({ type: "ChatPaymentSucceeded" });
+2 -2
View File
@@ -21,9 +21,9 @@
* - guestSession / userSession:队列串行走 HTTP,限制以后端 `lockDetail` 为准 * - guestSession / userSession:队列串行走 HTTP,限制以后端 `lockDetail` 为准
* - `pendingReplyCount` 跟踪待回复数量,`isReplyingAI` 由计数派生 * - `pendingReplyCount` 跟踪待回复数量,`isReplyingAI` 由计数派生
* *
* 配额 * 发送能力
* - 前端不再处理本地消息额度;游客 / 注册用户均以后端响应为准。 * - 前端不再处理本地消息额度;游客 / 注册用户均以后端响应为准。
* - 后端返回 `lockDetail.reason="weekly_limit"` 且需要升级时展示会员引导。 * - 后端返回 `cannotSendReason="insufficient_credits"` 时展示充值引导。
* *
*/ */
+1 -7
View File
@@ -167,10 +167,6 @@ async function sendMessageViaHttp(content: string): Promise<{
throw result.error; throw result.error;
} }
void chatRepo.prefetchMediaForSendResponse(result.data); void chatRepo.prefetchMediaForSendResponse(result.data);
const isMessageLimit =
result.data.lockDetail.locked &&
result.data.lockDetail.showUpgrade &&
result.data.lockDetail.reason === "weekly_limit";
const isInsufficientCredits = const isInsufficientCredits =
result.data.canSendMessage === false && result.data.canSendMessage === false &&
result.data.cannotSendReason === "insufficient_credits" && result.data.cannotSendReason === "insufficient_credits" &&
@@ -178,9 +174,7 @@ async function sendMessageViaHttp(content: string): Promise<{
return { return {
response: result.data, response: result.data,
reply: reply:
isMessageLimit || isInsufficientCredits isInsufficientCredits ? null : sendResponseToUiMessage(result.data),
? null
: sendResponseToUiMessage(result.data),
}; };
} }
+7 -11
View File
@@ -35,19 +35,12 @@ export function applyHttpSendOutput(
): Partial<ChatState> { ): Partial<ChatState> {
const { response, reply } = output; const { response, reply } = output;
const lockDetail = response.lockDetail;
const isWeeklyLimitBlocked =
lockDetail.locked &&
lockDetail.showUpgrade &&
lockDetail.reason === "weekly_limit";
const isInsufficientCredits = const isInsufficientCredits =
response.canSendMessage === false && response.canSendMessage === false &&
response.cannotSendReason === "insufficient_credits"; response.cannotSendReason === "insufficient_credits";
const upgradeReason: ChatUpgradeReason | null = isWeeklyLimitBlocked const upgradeReason: ChatUpgradeReason | null = isInsufficientCredits
? "weekly_limit" ? "insufficient_credits"
: isInsufficientCredits : null;
? "insufficient_credits"
: null;
const sendCapabilityState = getSendCapabilityState(response); const sendCapabilityState = getSendCapabilityState(response);
if (upgradeReason) { if (upgradeReason) {
@@ -73,7 +66,10 @@ export function applyHttpSendOutput(
}; };
} }
if (lockDetail.showUpgrade && lockDetail.reason === "private_message") { if (
response.lockDetail.showUpgrade &&
response.lockDetail.reason === "private_message"
) {
return { return {
messages: [...context.messages, reply], messages: [...context.messages, reply],
...finishPendingReply(context), ...finishPendingReply(context),
+1 -1
View File
@@ -1,7 +1,7 @@
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/data/dto/chat";
import type { PendingChatUnlockKind } from "@/lib/navigation/chat_unlock_session"; import type { PendingChatUnlockKind } from "@/lib/navigation/chat_unlock_session";
export type ChatUpgradeReason = "weekly_limit" | "insufficient_credits"; export type ChatUpgradeReason = "insufficient_credits";
export interface ChatUnlockPaywallRequest { export interface ChatUnlockPaywallRequest {
messageId: string; messageId: string;