diff --git a/src/app/api/payment/create-checkout-session/route.ts b/src/app/api/payment/create-checkout-session/route.ts index 63f4aa1d..131199a8 100644 --- a/src/app/api/payment/create-checkout-session/route.ts +++ b/src/app/api/payment/create-checkout-session/route.ts @@ -19,6 +19,9 @@ import { SUBSCRIPTION_PLANS } from "@/data/constants/subscription-plans"; import type { SubscriptionPlanId } from "@/data/constants/subscription-plans"; import { getPlanPriceId } from "@/lib/stripe/stripe-products"; import { getStripeClient } from "@/lib/stripe/stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("AppApiPaymentCreateCheckoutSessionRoute"); interface CreateCheckoutRequestBody { planId: SubscriptionPlanId; @@ -86,7 +89,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ url: session.url }); } catch (e) { const err = e as { message?: string; type?: string; code?: string }; - console.error("[api/create-checkout-session] Stripe error", { + log.error("[api/create-checkout-session] Stripe error", { message: err.message, type: err.type, code: err.code, diff --git a/src/app/api/payment/customer-portal/route.ts b/src/app/api/payment/customer-portal/route.ts index aa9e17e6..76925b28 100644 --- a/src/app/api/payment/customer-portal/route.ts +++ b/src/app/api/payment/customer-portal/route.ts @@ -14,6 +14,9 @@ import { NextRequest, NextResponse } from "next/server"; import { getStripeClient } from "@/lib/stripe/stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("AppApiPaymentCustomerPortalRoute"); interface CreatePortalRequestBody { customerId: string | null; @@ -56,7 +59,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ url: session.url }); } catch (e) { const err = e as { message?: string; type?: string; code?: string }; - console.error("[api/customer-portal] Stripe error", { + log.error("[api/customer-portal] Stripe error", { customerId: body.customerId, message: err.message, type: err.type, diff --git a/src/app/api/payment/webhook/route.ts b/src/app/api/payment/webhook/route.ts index 8d4abf94..861da91d 100644 --- a/src/app/api/payment/webhook/route.ts +++ b/src/app/api/payment/webhook/route.ts @@ -27,11 +27,14 @@ import type Stripe from "stripe"; import { dispatchStripeEvent } from "@/lib/stripe/stripe-events"; import { getStripeClient } from "@/lib/stripe/stripe-client"; +import { Logger } from "@/utils"; + +const log = new Logger("AppApiPaymentWebhookRoute"); export async function POST(req: NextRequest) { const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; if (!webhookSecret) { - console.error("[api/webhook] STRIPE_WEBHOOK_SECRET is not set"); + log.error("[api/webhook] STRIPE_WEBHOOK_SECRET is not set"); return NextResponse.json( { error: "Webhook secret not configured" }, { status: 500 }, @@ -42,7 +45,7 @@ export async function POST(req: NextRequest) { const rawBody = await req.text(); const sigHeader = req.headers.get("stripe-signature"); if (!sigHeader) { - console.error("[api/webhook] Missing stripe-signature header"); + log.error("[api/webhook] Missing stripe-signature header"); return NextResponse.json( { error: "Missing stripe-signature header" }, { status: 400 }, @@ -56,7 +59,7 @@ export async function POST(req: NextRequest) { event = stripe.webhooks.constructEvent(rawBody, sigHeader, webhookSecret); } catch (e) { const err = e as Error; - console.error("[api/webhook] Signature verification failed", { + log.error("[api/webhook] Signature verification failed", { message: err.message, }); return NextResponse.json( @@ -66,7 +69,7 @@ export async function POST(req: NextRequest) { } // 分发到 handler - console.log("[api/webhook] Received event", { + log.debug("[api/webhook] Received event", { eventId: event.id, type: event.type, created: new Date(event.created * 1000).toISOString(), @@ -75,11 +78,11 @@ export async function POST(req: NextRequest) { try { const handled = await dispatchStripeEvent(event); if (!handled) { - console.log(`[api/webhook] Ignored event type: ${event.type}`); + log.debug(`[api/webhook] Ignored event type: ${event.type}`); } } catch (e) { const err = e as Error; - console.error("[api/webhook] Handler error", { + log.error("[api/webhook] Handler error", { eventId: event.id, type: event.type, message: err.message, diff --git a/src/app/auth/auth-screen.tsx b/src/app/auth/auth-screen.tsx index a86a8d4b..a16ca88c 100644 --- a/src/app/auth/auth-screen.tsx +++ b/src/app/auth/auth-screen.tsx @@ -13,6 +13,9 @@ import { ROUTES } from "@/router/routes"; import { AuthBackground, AuthPanel } from "./components"; import styles from "./components/auth-screen.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppAuthAuthScreen"); export function AuthScreen() { const state = useAuthState(); @@ -23,7 +26,7 @@ export function AuthScreen() { // 用 pendingRedirect flag(不是 useRef transition detection)—— // re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。 useEffect(() => { - console.log("[auth-screen] useEffect (loginStatus + pendingRedirect)", { + log.debug("[auth-screen] useEffect (loginStatus + pendingRedirect)", { pending: state.pendingRedirect, loginStatus: state.loginStatus, willRedirect: @@ -31,7 +34,7 @@ export function AuthScreen() { }); if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") { - console.log( + log.debug( "[auth-screen] useEffect → router.replace(/chat) [via pendingRedirect flag]", ); authDispatch({ type: "AuthClearPendingRedirect" }); diff --git a/src/app/auth/components/email-login-form.tsx b/src/app/auth/components/email-login-form.tsx index 094e58f5..ae65e8e0 100644 --- a/src/app/auth/components/email-login-form.tsx +++ b/src/app/auth/components/email-login-form.tsx @@ -14,6 +14,9 @@ import { AuthTextField } from "./auth-text-field"; import { AuthPrimaryButton } from "./auth-primary-button"; import { AuthErrorMessage } from "./auth-error-message"; import styles from "./email-form.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppAuthComponentsEmailLoginForm"); export interface EmailLoginFormProps { globalError?: string | null; @@ -44,7 +47,7 @@ export function EmailLoginForm({ }; const submit = () => { - console.log("[email-login-form] submit ENTRY", { + log.debug("[email-login-form] submit ENTRY", { emailLength: email.length, emailPreview: email.slice(0, 8), passwordLength: password.length, @@ -53,7 +56,7 @@ export function EmailLoginForm({ const passwordErr = validatePassword(password); if (emailErr || passwordErr) { const firstErr = emailErr ?? passwordErr ?? null; - console.warn( + log.warn( "[email-login-form] submit VALIDATION FAILED (silent return)", { emailErr, passwordErr, surfaced: firstErr }, ); @@ -61,7 +64,7 @@ export function EmailLoginForm({ return; } setValidationError(null); - console.log("[email-login-form] submit dispatching AuthEmailLoginSubmitted"); + log.debug("[email-login-form] submit dispatching AuthEmailLoginSubmitted"); dispatch({ type: "AuthEmailLoginSubmitted", email, password }); }; diff --git a/src/app/auth/components/email-register-form.tsx b/src/app/auth/components/email-register-form.tsx index 9a7c8c04..7be6f5dc 100644 --- a/src/app/auth/components/email-register-form.tsx +++ b/src/app/auth/components/email-register-form.tsx @@ -25,6 +25,9 @@ import { AuthTextField } from "./auth-text-field"; import { AuthPrimaryButton } from "./auth-primary-button"; import { AuthErrorMessage } from "./auth-error-message"; import styles from "./email-form.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppAuthComponentsEmailRegisterForm"); export interface EmailRegisterFormProps { globalError?: string | null; @@ -66,7 +69,7 @@ export function EmailRegisterForm({ }; const submit = () => { - console.log("[email-register-form] submit ENTRY", { + log.debug("[email-register-form] submit ENTRY", { emailLength: email.length, emailPreview: email.slice(0, 8), passwordLength: password.length, @@ -81,7 +84,7 @@ export function EmailRegisterForm({ // 按 username → email → password → confirm 顺序挑第一个错(与字段顺序一致) const firstErr = usernameErr ?? emailErr ?? passwordErr ?? confirmErr ?? null; - console.warn( + log.warn( "[email-register-form] submit VALIDATION FAILED (silent return)", { emailErr, @@ -95,7 +98,7 @@ export function EmailRegisterForm({ return; } setValidationError(null); - console.log( + log.debug( "[email-register-form] submit dispatching AuthEmailRegisterSubmitted", ); dispatch({ diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index af29ff6e..e010fac4 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -1,28 +1,17 @@ "use client"; /** * ChatScreen 编排层 - * - * 子组件职责: - * - ChatHeader:顶部栏(游客 banner / 菜单按钮) - * - ChatArea:消息列表 - * - ChatInputBar:输入栏(文字 + 发送 + More) - * - PwaInstallOverlay:PWA 安装提示触发器 - * - BrowserHintOverlay:浏览器提示 - * - * 鉴权解耦(事件驱动): - * - chat 机器不感知鉴权 / 不管 WebSocket - * - auth → chat 的生命周期同步由根级 负责 - * - ChatScreen 只派生 UI 展示所需的 isGuest */ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; 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, UrlLauncherUtil } from "@/utils"; +import { AppEnvUtil, BrowserDetector, todayString, UrlLauncherUtil } from "@/utils"; import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; @@ -68,16 +57,36 @@ export function ChatScreen() { useEffect(() => { if (!authState.hasInitialized || authState.isLoading) return; - if (authState.loginStatus !== "facebook") return; - if (externalBrowserPromptShownRef.current) return; - if (!BrowserDetector.isInAppBrowser()) return; + const isDev = AppEnvUtil.isDevelopment(); + const canTriggerPrompt = + isDev || + (authState.loginStatus === "facebook" && BrowserDetector.isInAppBrowser()); + if (!canTriggerPrompt) return; - externalBrowserPromptShownRef.current = true; - const timer = window.setTimeout(() => { - setShowExternalBrowserDialog(true); - }, 3000); + let mounted = true; + let timer: number | undefined; - return () => window.clearTimeout(timer); + const init = async () => { + if (externalBrowserPromptShownRef.current) return; + + const today = todayString(); + const canShowResult = await AppStorage.canShowExternalBrowserDialog(today); + if (!mounted || !canShowResult.success || !canShowResult.data) return; + + externalBrowserPromptShownRef.current = true; + timer = window.setTimeout(() => { + if (!mounted) return; + setShowExternalBrowserDialog(true); + void AppStorage.recordExternalBrowserDialogShown(today); + }, 3000); + }; + + void init(); + + return () => { + mounted = false; + if (timer !== undefined) window.clearTimeout(timer); + }; }, [ authState.hasInitialized, authState.isLoading, diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx index 40bdb923..eb90ede9 100644 --- a/src/app/chat/components/chat-input-bar.tsx +++ b/src/app/chat/components/chat-input-bar.tsx @@ -27,6 +27,9 @@ import { useChatDispatch } from "@/stores/chat/chat-context"; import { ChatInputTextField } from "./chat-input-text-field"; import { ChatSendButton } from "./chat-send-button"; import styles from "./chat-input-bar.module.css"; +import { Logger } from "@/utils"; + +const log = new Logger("AppChatComponentsChatInputBar"); export interface ChatInputBarProps { disabled?: boolean; @@ -42,7 +45,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const handleSend = () => { if (!hasContent) return; - console.log("[chat-input-bar] handleSend", { + log.debug("[chat-input-bar] handleSend", { contentLength: input.length, contentPreview: input.slice(0, 50), hasContent, diff --git a/src/app/chat/components/external-browser-dialog.module.css b/src/app/chat/components/external-browser-dialog.module.css index d33f2f7c..49fbb2f5 100644 --- a/src/app/chat/components/external-browser-dialog.module.css +++ b/src/app/chat/components/external-browser-dialog.module.css @@ -5,18 +5,17 @@ display: flex; align-items: center; justify-content: center; - padding: var(--spacing-md, 16px); background: rgba(0, 0, 0, 0.5); } .dialog { - width: 100%; - max-width: 360px; - padding: 24px 18px 18px; + width: calc(100% - 40px); + max-width: var(--pwa-install-dialog-max-width, 360px); + padding: 24px 16px 16px; text-align: center; background: var(--color-page-background, #ffffff); - border-radius: 32px; - box-shadow: 0 20px 48px rgba(0, 0, 0, 0.22); + border-radius: 40px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); } .title { @@ -28,35 +27,40 @@ } .content { - margin: 0 12px 22px; - font-size: var(--font-size-md, 15px); + margin: 0 24px var(--spacing-xxl, 24px); + font-size: var(--font-size-lg, 16px); line-height: 1.5; - color: var(--color-text-secondary, #555555); + text-align: left; + color: #393939; white-space: pre-line; } .actions { display: flex; gap: var(--spacing-md, 12px); + width: 100%; } .button { flex: 1 1 auto; - height: 44px; + height: var(--pwa-button-height, 44px); border: 0; - border-radius: 999px; - font-size: var(--font-size-md, 15px); - font-weight: 700; + border-radius: var(--radius-bottom-sheet, 28px); + font-size: var(--font-size-lg, 16px); + font-weight: 600; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; } .secondary { - color: var(--color-text-secondary, #555555); - background: #efefef; + color: var(--color-page-background, #ffffff); + background: var(--color-text-secondary, #9e9e9e); } .primary { - color: #ffffff; + color: var(--color-page-background, #ffffff); background: linear-gradient( var(--color-button-gradient-start, #ff67e0), var(--color-button-gradient-end, #ff52a2) diff --git a/src/app/chat/components/external-browser-dialog.tsx b/src/app/chat/components/external-browser-dialog.tsx index 25ff8b17..0d732c84 100644 --- a/src/app/chat/components/external-browser-dialog.tsx +++ b/src/app/chat/components/external-browser-dialog.tsx @@ -27,8 +27,7 @@ export function ExternalBrowserDialog({ Open in browser

- Sync your Facebook profile to an external browser{"\n"} - for the best chat experience. + Sync your Facebook profile to an external browser for the best chat experience.