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}