feat(chat): implement external browser prompt logic and update tests

This commit is contained in:
2026-07-09 19:10:12 +08:00
parent a99db98ace
commit 270e84385b
4 changed files with 88 additions and 9 deletions
@@ -1,10 +1,15 @@
import { describe, expect, it } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { import {
getInsufficientCreditsMessageLimitView, getInsufficientCreditsMessageLimitView,
shouldStartExternalBrowserPrompt,
shouldShowMessageLimitBanner, shouldShowMessageLimitBanner,
} from "../chat-screen.helpers"; } from "../chat-screen.helpers";
afterEach(() => {
vi.unstubAllEnvs();
});
describe("getInsufficientCreditsMessageLimitView", () => { describe("getInsufficientCreditsMessageLimitView", () => {
it("asks guests to log in for more free chats", () => { it("asks guests to log in for more free chats", () => {
expect(getInsufficientCreditsMessageLimitView("guest")).toEqual({ expect(getInsufficientCreditsMessageLimitView("guest")).toEqual({
@@ -65,3 +70,73 @@ describe("shouldShowMessageLimitBanner", () => {
).toBe(false); ).toBe(false);
}); });
}); });
describe("shouldStartExternalBrowserPrompt", () => {
it("shows for facebook users inside an in-app browser", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
expect(
shouldStartExternalBrowserPrompt({
hasInitialized: true,
isLoading: false,
loginStatus: "facebook",
isInAppBrowser: true,
}),
).toBe(true);
});
it.each(["guest", "notLoggedIn", "email", "google"] as const)(
"hides for %s users even inside an in-app browser",
(loginStatus) => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
expect(
shouldStartExternalBrowserPrompt({
hasInitialized: true,
isLoading: false,
loginStatus,
isInAppBrowser: true,
}),
).toBe(false);
},
);
it("hides for facebook users in normal browsers", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
expect(
shouldStartExternalBrowserPrompt({
hasInitialized: true,
isLoading: false,
loginStatus: "facebook",
isInAppBrowser: false,
}),
).toBe(false);
});
it("shows in development for easier testing", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "development");
expect(
shouldStartExternalBrowserPrompt({
hasInitialized: true,
isLoading: false,
loginStatus: "guest",
isInAppBrowser: false,
}),
).toBe(true);
});
it("waits for auth initialization", () => {
vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production");
expect(
shouldStartExternalBrowserPrompt({
hasInitialized: false,
isLoading: false,
loginStatus: "facebook",
isInAppBrowser: true,
}),
).toBe(false);
});
});
+3 -1
View File
@@ -8,6 +8,7 @@ export interface ExternalBrowserPromptState {
hasInitialized: boolean; hasInitialized: boolean;
isLoading: boolean; isLoading: boolean;
loginStatus: LoginStatus; loginStatus: LoginStatus;
isInAppBrowser?: boolean;
} }
export type InsufficientCreditsAction = "auth" | "topup"; export type InsufficientCreditsAction = "auth" | "topup";
@@ -63,11 +64,12 @@ export function shouldStartExternalBrowserPrompt({
hasInitialized, hasInitialized,
isLoading, isLoading,
loginStatus, loginStatus,
isInAppBrowser = BrowserDetector.isInAppBrowser(),
}: ExternalBrowserPromptState): boolean { }: ExternalBrowserPromptState): boolean {
if (!hasInitialized || isLoading) return false; if (!hasInitialized || isLoading) return false;
return ( return (
isChatDevelopmentEnvironment() || isChatDevelopmentEnvironment() ||
(loginStatus === "facebook" && BrowserDetector.isInAppBrowser()) (loginStatus === "facebook" && isInAppBrowser)
); );
} }
+7 -2
View File
@@ -28,7 +28,7 @@ import {
} from "./chat-image-overlay-url"; } from "./chat-image-overlay-url";
import { import {
deriveIsGuest, deriveIsGuest,
isChatDevelopmentEnvironment, shouldStartExternalBrowserPrompt,
} from "./chat-screen.helpers"; } from "./chat-screen.helpers";
import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-banner"; import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-banner";
import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow"; import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow";
@@ -90,6 +90,11 @@ export function ChatScreen() {
loginStatus: authState.loginStatus, loginStatus: authState.loginStatus,
}); });
const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10; const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10;
const shouldShowBrowserHint = shouldStartExternalBrowserPrompt({
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
loginStatus: authState.loginStatus,
});
useChatGuestLogin({ useChatGuestLogin({
dispatch: authDispatch, dispatch: authDispatch,
@@ -186,7 +191,7 @@ export function ChatScreen() {
</div> </div>
{/* 浏览器提示(应用内浏览器检测) */} {/* 浏览器提示(应用内浏览器检测) */}
<BrowserHintOverlay forceShow={isChatDevelopmentEnvironment()} /> {shouldShowBrowserHint ? <BrowserHintOverlay forceShow /> : null}
{/* PWA 安装提示触发器(聊天消息数量达到阈值后才允许弹出) */} {/* PWA 安装提示触发器(聊天消息数量达到阈值后才允许弹出) */}
<PwaInstallOverlay enabled={shouldShowPwaInstall} /> <PwaInstallOverlay enabled={shouldShowPwaInstall} />
@@ -14,7 +14,6 @@ import { useEffect, useState } from "react";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser"; import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser";
import { BrowserDetector } from "@/utils";
import styles from "./browser-hint-overlay.module.css"; import styles from "./browser-hint-overlay.module.css";
@@ -39,13 +38,11 @@ export function BrowserHintOverlay({
forceShow = false, forceShow = false,
autoCollapseDelayMs = DEFAULT_AUTO_COLLAPSE_DELAY_MS, autoCollapseDelayMs = DEFAULT_AUTO_COLLAPSE_DELAY_MS,
}: BrowserHintOverlayProps) { }: BrowserHintOverlayProps) {
// lazy initializer:首次渲染即计算(避免 useEffect 中调 setState 的 lint 错)
const [isInAppBrowser] = useState(BrowserDetector.isInAppBrowser);
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(true);
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false); const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
// 开发模式或应用内浏览器才显示 // 展示条件由调用方控制;forceShow 保留给开发/测试入口。
const shouldShow = forceShow || isInAppBrowser; const shouldShow = forceShow;
useEffect(() => { useEffect(() => {
if (!shouldShow || hasAutoCollapsed || autoCollapseDelayMs <= 0) return; if (!shouldShow || hasAutoCollapsed || autoCollapseDelayMs <= 0) return;