feat(chat): tailor insufficient credits guidance

This commit is contained in:
2026-07-08 11:15:32 +08:00
parent 43e5cc95af
commit 6b8345e318
5 changed files with 91 additions and 6 deletions
@@ -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",
});
},
);
});
+29
View File
@@ -9,10 +9,39 @@ export interface ExternalBrowserPromptState {
loginStatus: LoginStatus; loginStatus: LoginStatus;
} }
export type InsufficientCreditsAction = "auth" | "topup";
export interface InsufficientCreditsMessageLimitView {
title: string;
description: string;
ctaLabel: string;
action: InsufficientCreditsAction;
}
export function deriveIsGuest(loginStatus: LoginStatus): boolean { export function deriveIsGuest(loginStatus: LoginStatus): boolean {
return loginStatus === "guest"; 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 { export function isChatDevelopmentEnvironment(): boolean {
return AppEnvUtil.isDevelopment(); return AppEnvUtil.isDevelopment();
} }
+12 -5
View File
@@ -30,6 +30,7 @@ import {
} from "./components"; } from "./components";
import { import {
deriveIsGuest, deriveIsGuest,
getInsufficientCreditsMessageLimitView,
getInsufficientCreditsSubscriptionType, getInsufficientCreditsSubscriptionType,
isChatDevelopmentEnvironment, isChatDevelopmentEnvironment,
shouldStartExternalBrowserPrompt, shouldStartExternalBrowserPrompt,
@@ -61,9 +62,9 @@ export function ChatScreen() {
const showMessageLimitBanner = const showMessageLimitBanner =
state.upgradePromptVisible && state.upgradePromptVisible &&
state.upgradeReason === "insufficient_credits"; state.upgradeReason === "insufficient_credits";
const messageLimitTitle = const messageLimitView = getInsufficientCreditsMessageLimitView(
"Insufficient credits\nTop up to continue chatting"; authState.loginStatus,
const messageLimitCtaLabel = "Top up credits to continue"; );
const firstRechargeOfferBanner = useFirstRechargeOfferBanner({ const firstRechargeOfferBanner = useFirstRechargeOfferBanner({
historyLoaded: state.historyLoaded, historyLoaded: state.historyLoaded,
loginStatus: authState.loginStatus, loginStatus: authState.loginStatus,
@@ -141,6 +142,11 @@ export function ChatScreen() {
} }
function handleMessageLimitUnlock(): void { function handleMessageLimitUnlock(): void {
if (messageLimitView.action === "auth") {
navigator.openAuth(ROUTES.chat);
return;
}
navigator.openSubscription({ navigator.openSubscription({
type: getInsufficientCreditsSubscriptionType(userState.isVip), type: getInsufficientCreditsSubscriptionType(userState.isVip),
returnTo: "chat", returnTo: "chat",
@@ -190,8 +196,9 @@ export function ChatScreen() {
{showMessageLimitBanner ? ( {showMessageLimitBanner ? (
<ChatInsufficientCreditsBanner <ChatInsufficientCreditsBanner
title={messageLimitTitle} title={messageLimitView.title}
ctaLabel={messageLimitCtaLabel} description={messageLimitView.description}
ctaLabel={messageLimitView.ctaLabel}
onUnlock={handleMessageLimitUnlock} onUnlock={handleMessageLimitUnlock}
/> />
) : ( ) : (
@@ -25,11 +25,20 @@
font-size: var(--responsive-card-title, 18px); font-size: var(--responsive-card-title, 18px);
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
margin: 0 0 var(--spacing-lg, 16px); margin: 0 0 var(--spacing-sm, 8px);
/* 与 splash 标题的粉色字保持视觉一致(白底渐变叠加层需要白字保证可读性) */ /* 与 splash 标题的粉色字保持视觉一致(白底渐变叠加层需要白字保证可读性) */
opacity: 0.95; 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 { .cta {
width: 100%; width: 100%;
min-height: var(--responsive-control-height, 48px); min-height: var(--responsive-control-height, 48px);
@@ -22,6 +22,7 @@ import styles from "./chat-insufficient-credits-banner.module.css";
export interface ChatInsufficientCreditsBannerProps { export interface ChatInsufficientCreditsBannerProps {
title?: string; title?: string;
description?: string;
ctaLabel?: string; ctaLabel?: string;
/** /**
* 自定义点击回调(不传则默认走统一订阅导航) * 自定义点击回调(不传则默认走统一订阅导航)
@@ -33,6 +34,7 @@ export interface ChatInsufficientCreditsBannerProps {
export function ChatInsufficientCreditsBanner({ export function ChatInsufficientCreditsBanner({
title = "Insufficient credits\nTop up to continue chatting", title = "Insufficient credits\nTop up to continue chatting",
description,
ctaLabel = "Top up credits to continue", ctaLabel = "Top up credits to continue",
onUnlock, onUnlock,
}: ChatInsufficientCreditsBannerProps) { }: ChatInsufficientCreditsBannerProps) {
@@ -62,6 +64,9 @@ export function ChatInsufficientCreditsBanner({
</span> </span>
))} ))}
</p> </p>
{description ? (
<p className={styles.description}>{description}</p>
) : null}
<button <button
type="button" type="button"
className={styles.cta} className={styles.cta}