feat(chat): add history unlock api

This commit is contained in:
2026-06-26 19:27:56 +08:00
parent 1b73c3ac10
commit 455204e1e4
10 changed files with 227 additions and 61 deletions
+93
View File
@@ -0,0 +1,93 @@
"use client";
import type { LoginStatus } from "@/data/dto/auth";
import { AppStorage } from "@/data/storage/app/app_storage";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { UserStorage } from "@/data/storage/user/user_storage";
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
import { AppEnvUtil, BrowserDetector, todayString, UrlLauncherUtil } from "@/utils";
export interface ExternalBrowserPromptState {
hasInitialized: boolean;
isLoading: boolean;
loginStatus: LoginStatus;
}
export function deriveIsGuest(loginStatus: LoginStatus): boolean {
return loginStatus === "guest";
}
export function isChatDevelopmentEnvironment(): boolean {
return AppEnvUtil.isDevelopment();
}
export function shouldStartExternalBrowserPrompt({
hasInitialized,
isLoading,
loginStatus,
}: ExternalBrowserPromptState): boolean {
if (!hasInitialized || isLoading) return false;
return (
isChatDevelopmentEnvironment() ||
(loginStatus === "facebook" && BrowserDetector.isInAppBrowser())
);
}
export async function resolveExternalBrowserPromptEligibility(): Promise<{
canShow: boolean;
today: string;
}> {
const today = todayString();
const result = await AppStorage.canShowExternalBrowserDialog(today);
return {
canShow: result.success && result.data,
today,
};
}
export function recordExternalBrowserPromptShown(today: string): Promise<unknown> {
return AppStorage.recordExternalBrowserDialogShown(today);
}
export async function openChatInExternalBrowser(): Promise<void> {
const authStorage = AuthStorage.getInstance();
const userStorage = UserStorage.getInstance();
const [deviceIdR, facebookIdR, avatarUrlR] = await Promise.all([
authStorage.getDeviceId(),
authStorage.getFacebookId(),
userStorage.getAvatarUrl(),
]);
const deviceId = deviceIdR.success ? deviceIdR.data : null;
const facebookId = facebookIdR.success ? facebookIdR.data : null;
const avatarUrl = avatarUrlR.success ? avatarUrlR.data : null;
if (deviceId && facebookId) {
UrlLauncherUtil.openUrlWithExternalBrowser(
ROUTE_BUILDERS.chatDeviceId(deviceId),
{
queryParams: {
fbid: facebookId,
...(avatarUrl ? { avatarUrl } : {}),
},
},
);
return;
}
UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash);
}
export function getChatPaywallSubscriptionUrl(): string {
return ROUTE_BUILDERS.subscription("vip", {
returnTo: "chat",
});
}
export function getChatPaywallNavigationUrl(loginStatus: LoginStatus): string {
const subscriptionUrl = getChatPaywallSubscriptionUrl();
if (deriveIsGuest(loginStatus)) {
return ROUTE_BUILDERS.authWithRedirect(subscriptionUrl);
}
return subscriptionUrl;
}
+24 -61
View File
@@ -5,13 +5,7 @@ import Image from "next/image";
import { useRouter } from "next/navigation";
import { useAuthState } from "@/stores/auth/auth-context";
import type { LoginStatus } from "@/data/dto/auth";
import { AppStorage } from "@/data/storage/app/app_storage";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { UserStorage } from "@/data/storage/user/user_storage";
import { useChatState } from "@/stores/chat/chat-context";
import { AppEnvUtil, BrowserDetector, todayString, UrlLauncherUtil } from "@/utils";
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
import { MobileShell } from "@/app/_components/core";
@@ -24,16 +18,17 @@ import {
ExternalBrowserDialog,
PwaInstallOverlay,
} from "./components";
import {
deriveIsGuest,
getChatPaywallNavigationUrl,
isChatDevelopmentEnvironment,
openChatInExternalBrowser,
recordExternalBrowserPromptShown,
resolveExternalBrowserPromptEligibility,
shouldStartExternalBrowserPrompt,
} from "./chat-screen.helpers";
import styles from "./components/chat-screen.module.css";
/**
* 派生 isGuest —— 单一来源:`auth.loginStatus`
*/
function deriveIsGuest(loginStatus: LoginStatus): boolean {
return loginStatus === "guest";
}
export function ChatScreen() {
const state = useChatState();
const authState = useAuthState();
@@ -50,17 +45,17 @@ export function ChatScreen() {
const messageLimitTitle = "The limit for free chat times\nhas been reached";
const externalBrowserPromptShownRef = useRef(false);
const chatPaywallSubscriptionUrl = ROUTE_BUILDERS.subscription("vip", {
returnTo: "chat",
});
useEffect(() => {
if (!authState.hasInitialized || authState.isLoading) return;
const isDev = AppEnvUtil.isDevelopment();
const canTriggerPrompt =
isDev ||
(authState.loginStatus === "facebook" && BrowserDetector.isInAppBrowser());
if (!canTriggerPrompt) return;
if (
!shouldStartExternalBrowserPrompt({
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
loginStatus: authState.loginStatus,
})
) {
return;
}
let mounted = true;
let timer: number | undefined;
@@ -68,15 +63,14 @@ export function ChatScreen() {
const init = async () => {
if (externalBrowserPromptShownRef.current) return;
const today = todayString();
const canShowResult = await AppStorage.canShowExternalBrowserDialog(today);
if (!mounted || !canShowResult.success || !canShowResult.data) return;
const eligibility = await resolveExternalBrowserPromptEligibility();
if (!mounted || !eligibility.canShow) return;
externalBrowserPromptShownRef.current = true;
timer = window.setTimeout(() => {
if (!mounted) return;
setShowExternalBrowserDialog(true);
void AppStorage.recordExternalBrowserDialogShown(today);
void recordExternalBrowserPromptShown(eligibility.today);
}, 3000);
};
@@ -94,41 +88,11 @@ export function ChatScreen() {
async function handleOpenExternalBrowser(): Promise<void> {
setShowExternalBrowserDialog(false);
const authStorage = AuthStorage.getInstance();
const userStorage = UserStorage.getInstance();
const [deviceIdR, facebookIdR, avatarUrlR] = await Promise.all([
authStorage.getDeviceId(),
authStorage.getFacebookId(),
userStorage.getAvatarUrl(),
]);
const deviceId = deviceIdR.success ? deviceIdR.data : null;
const facebookId = facebookIdR.success ? facebookIdR.data : null;
const avatarUrl = avatarUrlR.success ? avatarUrlR.data : null;
if (deviceId && facebookId) {
UrlLauncherUtil.openUrlWithExternalBrowser(
ROUTE_BUILDERS.chatDeviceId(deviceId),
{
queryParams: {
fbid: facebookId,
...(avatarUrl ? { avatarUrl } : {}),
},
},
);
return;
}
UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash);
await openChatInExternalBrowser();
}
function openChatPaywallSubscription(): void {
if (isGuest) {
router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
return;
}
router.push(chatPaywallSubscriptionUrl);
router.push(getChatPaywallNavigationUrl(authState.loginStatus));
}
function handleUnlockPrivateMessage(): void {
@@ -182,11 +146,10 @@ export function ChatScreen() {
) : (
<ChatInputBar disabled={!state.historyLoaded} />
)}
</div>
{/* 浏览器提示(应用内浏览器检测) */}
<BrowserHintOverlay forceShow={AppEnvUtil.isDevelopment()} />
<BrowserHintOverlay forceShow={isChatDevelopmentEnvironment()} />
{/* PWA 安装提示触发器(无可见 UI3.5s 后弹 dialog */}
<PwaInstallOverlay />