From ba6ce57e62bce3a0d9c2940f511201cc2dbb1541 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 30 Jun 2026 14:50:31 +0800 Subject: [PATCH] refactor(app): tighten feature boundaries --- src/app/_components/index.ts | 1 + .../user-message-avatar.module.css | 19 +++++++ .../user-message-avatar.tsx | 2 +- src/app/chat/components/index.ts | 1 - src/app/chat/components/message-avatar.tsx | 2 +- .../deviceid/[deviceId]/DeepLinkPersist.tsx | 18 +++---- .../[messageId]/chat-image-viewer-screen.tsx | 2 +- src/app/sidebar/components/user-header.tsx | 2 +- src/app/splash/splash-screen.tsx | 6 +-- .../subscription-checkout-button.tsx | 6 +-- .../components/subscription-user-row.tsx | 2 +- src/app/subscription/return/page.tsx | 26 +++------ src/app/subscription/subscription-screen.tsx | 15 +++--- src/lib/auth/auth_session.ts | 8 +++ src/lib/chat/chat_external_browser.ts | 14 +++++ .../navigation/chat_image_return_session.ts} | 0 src/lib/payment/pending_payment_order.ts | 54 +++++++++++++++++++ 17 files changed, 126 insertions(+), 52 deletions(-) create mode 100644 src/app/_components/user-message-avatar.module.css rename src/app/{chat/components => _components}/user-message-avatar.tsx (94%) create mode 100644 src/lib/auth/auth_session.ts rename src/{app/chat/chat-image-return-session.ts => lib/navigation/chat_image_return_session.ts} (100%) create mode 100644 src/lib/payment/pending_payment_order.ts diff --git a/src/app/_components/index.ts b/src/app/_components/index.ts index b1560313..29863bfa 100644 --- a/src/app/_components/index.ts +++ b/src/app/_components/index.ts @@ -3,3 +3,4 @@ */ export * from "./back-button"; +export * from "./user-message-avatar"; diff --git a/src/app/_components/user-message-avatar.module.css b/src/app/_components/user-message-avatar.module.css new file mode 100644 index 00000000..1bd63b43 --- /dev/null +++ b/src/app/_components/user-message-avatar.module.css @@ -0,0 +1,19 @@ +.avatar { + flex: 0 0 auto; + width: 43px; + height: 43px; + border-radius: 9999px; + overflow: hidden; + border: 2px solid var(--color-avatar-border, #fbf3f5); + box-shadow: var(--shadow-input-box, 0 1px 2px rgba(0, 0, 0, 0.1)); + background: var(--color-avatar-border, #fbf3f5); + display: flex; + align-items: center; + justify-content: center; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} diff --git a/src/app/chat/components/user-message-avatar.tsx b/src/app/_components/user-message-avatar.tsx similarity index 94% rename from src/app/chat/components/user-message-avatar.tsx rename to src/app/_components/user-message-avatar.tsx index 50ddd196..623c31fe 100644 --- a/src/app/chat/components/user-message-avatar.tsx +++ b/src/app/_components/user-message-avatar.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; -import styles from "./message-avatar.module.css"; +import styles from "./user-message-avatar.module.css"; export interface UserMessageAvatarProps { avatarUrl?: string | null; diff --git a/src/app/chat/components/index.ts b/src/app/chat/components/index.ts index d41968a6..2069e9b3 100644 --- a/src/app/chat/components/index.ts +++ b/src/app/chat/components/index.ts @@ -25,6 +25,5 @@ export * from "./private-message-card"; export * from "./pwa-install-dialog"; export * from "./pwa-install-overlay"; export * from "./text-bubble"; -export * from "./user-message-avatar"; export * from "./voice-bubble"; export * from "./voice-unlock-options-dialog"; diff --git a/src/app/chat/components/message-avatar.tsx b/src/app/chat/components/message-avatar.tsx index fe723ffe..1a189bf8 100644 --- a/src/app/chat/components/message-avatar.tsx +++ b/src/app/chat/components/message-avatar.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import { UserMessageAvatar } from "./user-message-avatar"; +import { UserMessageAvatar } from "@/app/_components"; import styles from "./message-avatar.module.css"; export interface MessageAvatarProps { diff --git a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx index 97a074c4..62645b11 100644 --- a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx +++ b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx @@ -5,16 +5,14 @@ * * 设计: * - 不走 Server `redirect()`:避免把 fbid 暴露在 URL 里。 - * - deviceId / fbid 都走 `AuthStorage.getInstance().setXxx`(与 `token_interceptor.ts` 同源) - * - avatarUrl 写入 UserStorage 的头像 slot,外部浏览器首次恢复时可带给后端。 + * - deviceId / fbid / avatarUrl 统一交给 `lib/chat` 落地。 * - 写入完成后 `router.replace('/chat')`,URL 不留深链痕迹。 */ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { AuthStorage } from "@/data/storage/auth/auth_storage"; -import { UserStorage } from "@/data/storage/user/user_storage"; +import { persistExternalBrowserChatDeepLink } from "@/lib/chat/chat_external_browser"; import { ROUTES } from "@/router/routes"; import { Logger } from "@/utils"; @@ -37,13 +35,11 @@ export default function DeepLinkPersist({ useEffect(() => { void (async () => { try { - await AuthStorage.getInstance().setDeviceId(deviceId); - if (fbid && fbid.length > 0) { - await AuthStorage.getInstance().setFacebookId(fbid); - } - if (avatarUrl && avatarUrl.length > 0) { - await UserStorage.getInstance().setAvatarUrl(avatarUrl); - } + await persistExternalBrowserChatDeepLink({ + deviceId, + facebookId: fbid, + avatarUrl, + }); } catch (err) { // 写不进 localStorage 时(例如隐私模式)也不阻塞跳转;记录到 console 即可。 log.warn("[DeepLinkPersist] failed to persist", err); diff --git a/src/app/chat/image/[messageId]/chat-image-viewer-screen.tsx b/src/app/chat/image/[messageId]/chat-image-viewer-screen.tsx index 568f33aa..cbf8d82b 100644 --- a/src/app/chat/image/[messageId]/chat-image-viewer-screen.tsx +++ b/src/app/chat/image/[messageId]/chat-image-viewer-screen.tsx @@ -6,9 +6,9 @@ import { MobileShell } from "@/app/_components/core"; import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; import { useAuthState } from "@/stores/auth/auth-context"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; +import { savePendingChatImageReturn } from "@/lib/navigation/chat_image_return_session"; import { getChatPaywallNavigationUrl } from "../../chat-screen.helpers"; -import { savePendingChatImageReturn } from "../../chat-image-return-session"; import { FullscreenImageViewer, HistoryUnlockDialog } from "../../components"; import styles from "./chat-image-viewer-screen.module.css"; diff --git a/src/app/sidebar/components/user-header.tsx b/src/app/sidebar/components/user-header.tsx index 392ea95f..d7d3e297 100644 --- a/src/app/sidebar/components/user-header.tsx +++ b/src/app/sidebar/components/user-header.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; -import { UserMessageAvatar } from "@/app/chat/components/user-message-avatar"; +import { UserMessageAvatar } from "@/app/_components"; import styles from "./user-header.module.css"; diff --git a/src/app/splash/splash-screen.tsx b/src/app/splash/splash-screen.tsx index 34ff7f03..37a31ac4 100644 --- a/src/app/splash/splash-screen.tsx +++ b/src/app/splash/splash-screen.tsx @@ -4,8 +4,8 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { MobileShell } from "@/app/_components/core"; +import { hasBusinessLoginToken } from "@/lib/auth/auth_session"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; -import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { ROUTES } from "@/router/routes"; import { pwaUtil, Logger } from "@/utils"; @@ -48,9 +48,7 @@ export function SplashScreen() { useEffect(() => { let cancelled = false; void (async () => { - // 用 getLoginToken() 拿 string | null(不是 hasLoginToken() 拿 boolean) - const r = await AuthStorage.getInstance().getLoginToken(); - if (!cancelled && r.success && r.data) { + if (!cancelled && (await hasBusinessLoginToken())) { router.replace(ROUTES.chat); } })(); diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index b71d3c27..12022336 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useRef, useState } from "react"; -import { PendingPaymentOrderStorage } from "@/data/storage"; +import { savePendingEzpayOrder } from "@/lib/payment/pending_payment_order"; import { usePaymentDispatch, usePaymentState, @@ -280,12 +280,10 @@ async function launchEzpayRedirect({ return; } - const saveResult = await PendingPaymentOrderStorage.setPendingOrder({ + const saveResult = await savePendingEzpayOrder({ orderId, - payChannel: "ezpay", subscriptionType, ...(returnTo ? { returnTo } : {}), - createdAt: Date.now(), }); if (Result.isErr(saveResult)) { const errorMessage = diff --git a/src/app/subscription/components/subscription-user-row.tsx b/src/app/subscription/components/subscription-user-row.tsx index 4f69fbc6..08bc931e 100644 --- a/src/app/subscription/components/subscription-user-row.tsx +++ b/src/app/subscription/components/subscription-user-row.tsx @@ -7,7 +7,7 @@ * - 头像右侧两行文本:用户名(16px/600)、副标题(12px/secondary) * - 无头像时显示默认用户图标 */ -import { UserMessageAvatar } from "@/app/chat/components/user-message-avatar"; +import { UserMessageAvatar } from "@/app/_components"; import styles from "./subscription-user-row.module.css"; diff --git a/src/app/subscription/return/page.tsx b/src/app/subscription/return/page.tsx index 50d2d2f4..a204ac0a 100644 --- a/src/app/subscription/return/page.tsx +++ b/src/app/subscription/return/page.tsx @@ -4,23 +4,14 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { MobileShell } from "@/app/_components/core"; -import { PendingPaymentOrderStorage } from "@/data/storage"; +import { + buildPendingPaymentSubscriptionUrl, + getPendingPaymentOrder, +} from "@/lib/payment/pending_payment_order"; import { ROUTES } from "@/router/routes"; type ReturnStatus = "loading" | "missing"; -function buildSubscriptionUrl( - subscriptionType: "vip" | "topup", - returnTo?: "chat", -): string { - const params = new URLSearchParams({ - type: subscriptionType, - paymentReturn: "1", - }); - if (returnTo) params.set("returnTo", returnTo); - return `${ROUTES.subscription}?${params.toString()}`; -} - export default function SubscriptionReturnPage() { const router = useRouter(); const [status, setStatus] = useState("loading"); @@ -29,16 +20,11 @@ export default function SubscriptionReturnPage() { let cancelled = false; const redirectToSubscription = async () => { - const result = await PendingPaymentOrderStorage.getPendingOrder(); + const result = await getPendingPaymentOrder(); if (cancelled) return; if (result.success && result.data !== null) { - router.replace( - buildSubscriptionUrl( - result.data.subscriptionType, - result.data.returnTo, - ), - ); + router.replace(buildPendingPaymentSubscriptionUrl(result.data)); return; } diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index d49ed84b..ed59615c 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -3,10 +3,13 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { BackButton } from "@/app/_components"; -import { consumePendingChatImageReturn } from "@/app/chat/chat-image-return-session"; import { Checkbox, MobileShell } from "@/app/_components/core"; import { AppConstants } from "@/core/app_constants"; -import { PendingPaymentOrderStorage } from "@/data/storage"; +import { consumePendingChatImageReturn } from "@/lib/navigation/chat_image_return_session"; +import { + clearPendingPaymentOrder, + getPendingPaymentOrderForType, +} from "@/lib/payment/pending_payment_order"; import { usePaymentDispatch, usePaymentState, @@ -85,13 +88,11 @@ export function SubscriptionScreen({ let cancelled = false; const handlePendingOrder = async () => { - const result = await PendingPaymentOrderStorage.getPendingOrderForType( - subscriptionType, - ); + const result = await getPendingPaymentOrderForType(subscriptionType); if (cancelled || !result.success || result.data === null) return; if (!shouldResumePendingOrder) { - await PendingPaymentOrderStorage.clearPendingOrder(); + await clearPendingPaymentOrder(); if ( payment.currentOrderId === result.data.orderId && (payment.isPollingOrder || payment.isPaid || payment.status === "failed") @@ -130,7 +131,7 @@ export function SubscriptionScreen({ if (!payment.currentOrderId) return; if (!payment.isPaid && payment.status !== "failed") return; - void PendingPaymentOrderStorage.clearPendingOrder(); + void clearPendingPaymentOrder(); }, [payment.currentOrderId, payment.isPaid, payment.status]); const vipOfferPlans = useMemo( diff --git a/src/lib/auth/auth_session.ts b/src/lib/auth/auth_session.ts new file mode 100644 index 00000000..b2c8a9d1 --- /dev/null +++ b/src/lib/auth/auth_session.ts @@ -0,0 +1,8 @@ +"use client"; + +import { AuthStorage } from "@/data/storage/auth/auth_storage"; + +export async function hasBusinessLoginToken(): Promise { + const result = await AuthStorage.getInstance().getLoginToken(); + return result.success && Boolean(result.data); +} diff --git a/src/lib/chat/chat_external_browser.ts b/src/lib/chat/chat_external_browser.ts index 7c0318c9..526a5325 100644 --- a/src/lib/chat/chat_external_browser.ts +++ b/src/lib/chat/chat_external_browser.ts @@ -52,3 +52,17 @@ export async function openChatInExternalBrowser(): Promise { UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash); } + +export async function persistExternalBrowserChatDeepLink(input: { + deviceId: string; + facebookId?: string | null; + avatarUrl?: string | null; +}): Promise { + await AuthStorage.getInstance().setDeviceId(input.deviceId); + if (input.facebookId && input.facebookId.length > 0) { + await AuthStorage.getInstance().setFacebookId(input.facebookId); + } + if (input.avatarUrl && input.avatarUrl.length > 0) { + await UserStorage.getInstance().setAvatarUrl(input.avatarUrl); + } +} diff --git a/src/app/chat/chat-image-return-session.ts b/src/lib/navigation/chat_image_return_session.ts similarity index 100% rename from src/app/chat/chat-image-return-session.ts rename to src/lib/navigation/chat_image_return_session.ts diff --git a/src/lib/payment/pending_payment_order.ts b/src/lib/payment/pending_payment_order.ts new file mode 100644 index 00000000..cce258b5 --- /dev/null +++ b/src/lib/payment/pending_payment_order.ts @@ -0,0 +1,54 @@ +"use client"; + +import { + PendingPaymentOrderStorage, + type PendingPaymentOrder, +} from "@/data/storage"; +import { ROUTES } from "@/router/routes"; +import type { Result } from "@/utils"; + +export type PendingPaymentSubscriptionType = + PendingPaymentOrder["subscriptionType"]; +export type PendingPaymentReturnTo = PendingPaymentOrder["returnTo"]; + +export function savePendingEzpayOrder(input: { + orderId: string; + subscriptionType: PendingPaymentSubscriptionType; + returnTo?: PendingPaymentReturnTo; + createdAt?: number; +}): Promise> { + return PendingPaymentOrderStorage.setPendingOrder({ + orderId: input.orderId, + payChannel: "ezpay", + subscriptionType: input.subscriptionType, + ...(input.returnTo ? { returnTo: input.returnTo } : {}), + createdAt: input.createdAt ?? Date.now(), + }); +} + +export function getPendingPaymentOrder(): Promise< + Result +> { + return PendingPaymentOrderStorage.getPendingOrder(); +} + +export function getPendingPaymentOrderForType( + subscriptionType: PendingPaymentSubscriptionType, +): Promise> { + return PendingPaymentOrderStorage.getPendingOrderForType(subscriptionType); +} + +export function clearPendingPaymentOrder(): Promise> { + return PendingPaymentOrderStorage.clearPendingOrder(); +} + +export function buildPendingPaymentSubscriptionUrl( + order: Pick, +): string { + const params = new URLSearchParams({ + type: order.subscriptionType, + paymentReturn: "1", + }); + if (order.returnTo) params.set("returnTo", order.returnTo); + return `${ROUTES.subscription}?${params.toString()}`; +}