From 270e84385beacf3c917847bb8a86d0804fa91644 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 9 Jul 2026 19:10:12 +0800 Subject: [PATCH] feat(chat): implement external browser prompt logic and update tests --- .../__tests__/chat-screen.helpers.test.ts | 77 ++++++++++++++++++- src/app/chat/chat-screen.helpers.ts | 4 +- src/app/chat/chat-screen.tsx | 9 ++- .../chat/components/browser-hint-overlay.tsx | 7 +- 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/app/chat/__tests__/chat-screen.helpers.test.ts b/src/app/chat/__tests__/chat-screen.helpers.test.ts index a46e2c27..9c8de28a 100644 --- a/src/app/chat/__tests__/chat-screen.helpers.test.ts +++ b/src/app/chat/__tests__/chat-screen.helpers.test.ts @@ -1,10 +1,15 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { getInsufficientCreditsMessageLimitView, + shouldStartExternalBrowserPrompt, shouldShowMessageLimitBanner, } from "../chat-screen.helpers"; +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe("getInsufficientCreditsMessageLimitView", () => { it("asks guests to log in for more free chats", () => { expect(getInsufficientCreditsMessageLimitView("guest")).toEqual({ @@ -65,3 +70,73 @@ describe("shouldShowMessageLimitBanner", () => { ).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); + }); +}); diff --git a/src/app/chat/chat-screen.helpers.ts b/src/app/chat/chat-screen.helpers.ts index c36ece04..ed3f258a 100644 --- a/src/app/chat/chat-screen.helpers.ts +++ b/src/app/chat/chat-screen.helpers.ts @@ -8,6 +8,7 @@ export interface ExternalBrowserPromptState { hasInitialized: boolean; isLoading: boolean; loginStatus: LoginStatus; + isInAppBrowser?: boolean; } export type InsufficientCreditsAction = "auth" | "topup"; @@ -63,11 +64,12 @@ export function shouldStartExternalBrowserPrompt({ hasInitialized, isLoading, loginStatus, + isInAppBrowser = BrowserDetector.isInAppBrowser(), }: ExternalBrowserPromptState): boolean { if (!hasInitialized || isLoading) return false; return ( isChatDevelopmentEnvironment() || - (loginStatus === "facebook" && BrowserDetector.isInAppBrowser()) + (loginStatus === "facebook" && isInAppBrowser) ); } diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index bde90076..77b546d4 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -28,7 +28,7 @@ import { } from "./chat-image-overlay-url"; import { deriveIsGuest, - isChatDevelopmentEnvironment, + shouldStartExternalBrowserPrompt, } from "./chat-screen.helpers"; import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-banner"; import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow"; @@ -90,6 +90,11 @@ export function ChatScreen() { loginStatus: authState.loginStatus, }); const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10; + const shouldShowBrowserHint = shouldStartExternalBrowserPrompt({ + hasInitialized: authState.hasInitialized, + isLoading: authState.isLoading, + loginStatus: authState.loginStatus, + }); useChatGuestLogin({ dispatch: authDispatch, @@ -186,7 +191,7 @@ export function ChatScreen() { {/* 浏览器提示(应用内浏览器检测) */} - + {shouldShowBrowserHint ? : null} {/* PWA 安装提示触发器(聊天消息数量达到阈值后才允许弹出) */} diff --git a/src/app/chat/components/browser-hint-overlay.tsx b/src/app/chat/components/browser-hint-overlay.tsx index cca5c9d1..4e4c3392 100644 --- a/src/app/chat/components/browser-hint-overlay.tsx +++ b/src/app/chat/components/browser-hint-overlay.tsx @@ -14,7 +14,6 @@ import { useEffect, useState } from "react"; import { ExternalLink } from "lucide-react"; import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser"; -import { BrowserDetector } from "@/utils"; import styles from "./browser-hint-overlay.module.css"; @@ -39,13 +38,11 @@ export function BrowserHintOverlay({ forceShow = false, autoCollapseDelayMs = DEFAULT_AUTO_COLLAPSE_DELAY_MS, }: BrowserHintOverlayProps) { - // lazy initializer:首次渲染即计算(避免 useEffect 中调 setState 的 lint 错) - const [isInAppBrowser] = useState(BrowserDetector.isInAppBrowser); const [isExpanded, setIsExpanded] = useState(true); const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false); - // 开发模式或应用内浏览器才显示 - const shouldShow = forceShow || isInAppBrowser; + // 展示条件由调用方控制;forceShow 保留给开发/测试入口。 + const shouldShow = forceShow; useEffect(() => { if (!shouldShow || hasAutoCollapsed || autoCollapseDelayMs <= 0) return;