From 6b8345e31882a3dc8f2baa843260135d48b245ed Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 8 Jul 2026 11:15:32 +0800 Subject: [PATCH] feat(chat): tailor insufficient credits guidance --- .../__tests__/chat-screen.helpers.test.ts | 35 +++++++++++++++++++ src/app/chat/chat-screen.helpers.ts | 29 +++++++++++++++ src/app/chat/chat-screen.tsx | 17 ++++++--- ...hat-insufficient-credits-banner.module.css | 11 +++++- .../chat-insufficient-credits-banner.tsx | 5 +++ 5 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 src/app/chat/__tests__/chat-screen.helpers.test.ts diff --git a/src/app/chat/__tests__/chat-screen.helpers.test.ts b/src/app/chat/__tests__/chat-screen.helpers.test.ts new file mode 100644 index 00000000..690afada --- /dev/null +++ b/src/app/chat/__tests__/chat-screen.helpers.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { getInsufficientCreditsMessageLimitView } from "../chat-screen.helpers"; + +describe("getInsufficientCreditsMessageLimitView", () => { + it("asks guests to log in for more free chats", () => { + expect(getInsufficientCreditsMessageLimitView("guest")).toEqual({ + title: "Log in to get more free chats", + description: "Free chats refresh every day. Log in to get more chances.", + ctaLabel: "Log in to continue", + action: "auth", + }); + }); + + it("treats the transient not logged in state like guest", () => { + expect(getInsufficientCreditsMessageLimitView("notLoggedIn")).toEqual({ + title: "Log in to get more free chats", + description: "Free chats refresh every day. Log in to get more chances.", + ctaLabel: "Log in to continue", + action: "auth", + }); + }); + + it.each(["email", "facebook", "google", "apple"] as const)( + "asks %s users to top up credits", + (loginStatus) => { + expect(getInsufficientCreditsMessageLimitView(loginStatus)).toEqual({ + title: "Insufficient credits\nTop up to continue chatting", + description: "Free chats refresh every day. You can also top up credits now.", + ctaLabel: "Top up credits to continue", + action: "topup", + }); + }, + ); +}); diff --git a/src/app/chat/chat-screen.helpers.ts b/src/app/chat/chat-screen.helpers.ts index dbb4e122..f01025c1 100644 --- a/src/app/chat/chat-screen.helpers.ts +++ b/src/app/chat/chat-screen.helpers.ts @@ -9,10 +9,39 @@ export interface ExternalBrowserPromptState { loginStatus: LoginStatus; } +export type InsufficientCreditsAction = "auth" | "topup"; + +export interface InsufficientCreditsMessageLimitView { + title: string; + description: string; + ctaLabel: string; + action: InsufficientCreditsAction; +} + export function deriveIsGuest(loginStatus: LoginStatus): boolean { return loginStatus === "guest"; } +export function getInsufficientCreditsMessageLimitView( + loginStatus: LoginStatus, +): InsufficientCreditsMessageLimitView { + if (loginStatus === "guest" || loginStatus === "notLoggedIn") { + return { + title: "Log in to get more free chats", + description: "Free chats refresh every day. Log in to get more chances.", + ctaLabel: "Log in to continue", + action: "auth", + }; + } + + return { + title: "Insufficient credits", + description: "Free chats refresh every day. You can also top up credits now.", + ctaLabel: "Top up credits to continue", + action: "topup", + }; +} + export function isChatDevelopmentEnvironment(): boolean { return AppEnvUtil.isDevelopment(); } diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index 94a6d8e9..4cb4a918 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -30,6 +30,7 @@ import { } from "./components"; import { deriveIsGuest, + getInsufficientCreditsMessageLimitView, getInsufficientCreditsSubscriptionType, isChatDevelopmentEnvironment, shouldStartExternalBrowserPrompt, @@ -61,9 +62,9 @@ export function ChatScreen() { const showMessageLimitBanner = state.upgradePromptVisible && state.upgradeReason === "insufficient_credits"; - const messageLimitTitle = - "Insufficient credits\nTop up to continue chatting"; - const messageLimitCtaLabel = "Top up credits to continue"; + const messageLimitView = getInsufficientCreditsMessageLimitView( + authState.loginStatus, + ); const firstRechargeOfferBanner = useFirstRechargeOfferBanner({ historyLoaded: state.historyLoaded, loginStatus: authState.loginStatus, @@ -141,6 +142,11 @@ export function ChatScreen() { } function handleMessageLimitUnlock(): void { + if (messageLimitView.action === "auth") { + navigator.openAuth(ROUTES.chat); + return; + } + navigator.openSubscription({ type: getInsufficientCreditsSubscriptionType(userState.isVip), returnTo: "chat", @@ -190,8 +196,9 @@ export function ChatScreen() { {showMessageLimitBanner ? ( ) : ( diff --git a/src/app/chat/components/chat-insufficient-credits-banner.module.css b/src/app/chat/components/chat-insufficient-credits-banner.module.css index ac65a814..b8424c5f 100644 --- a/src/app/chat/components/chat-insufficient-credits-banner.module.css +++ b/src/app/chat/components/chat-insufficient-credits-banner.module.css @@ -25,11 +25,20 @@ font-size: var(--responsive-card-title, 18px); font-weight: 600; line-height: 1.4; - margin: 0 0 var(--spacing-lg, 16px); + margin: 0 0 var(--spacing-sm, 8px); /* 与 splash 标题的粉色字保持视觉一致(白底渐变叠加层需要白字保证可读性) */ opacity: 0.95; } +.description { + max-width: min(100%, 360px); + margin: 0 auto var(--spacing-lg, 16px); + color: rgba(255, 255, 255, 0.88); + font-size: var(--responsive-caption, 13px); + font-weight: 600; + line-height: 1.45; +} + .cta { width: 100%; min-height: var(--responsive-control-height, 48px); diff --git a/src/app/chat/components/chat-insufficient-credits-banner.tsx b/src/app/chat/components/chat-insufficient-credits-banner.tsx index 435fcd9b..531f5cd6 100644 --- a/src/app/chat/components/chat-insufficient-credits-banner.tsx +++ b/src/app/chat/components/chat-insufficient-credits-banner.tsx @@ -22,6 +22,7 @@ import styles from "./chat-insufficient-credits-banner.module.css"; export interface ChatInsufficientCreditsBannerProps { title?: string; + description?: string; ctaLabel?: string; /** * 自定义点击回调(不传则默认走统一订阅导航) @@ -33,6 +34,7 @@ export interface ChatInsufficientCreditsBannerProps { export function ChatInsufficientCreditsBanner({ title = "Insufficient credits\nTop up to continue chatting", + description, ctaLabel = "Top up credits to continue", onUnlock, }: ChatInsufficientCreditsBannerProps) { @@ -62,6 +64,9 @@ export function ChatInsufficientCreditsBanner({ ))}

+ {description ? ( +

{description}

+ ) : null}