feat(chat): implement external browser prompt logic and update tests
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user