refactor(app): tighten feature boundaries

This commit is contained in:
2026-06-30 14:50:31 +08:00
parent 1fed21fa2f
commit ba6ce57e62
17 changed files with 126 additions and 52 deletions
+1
View File
@@ -3,3 +3,4 @@
*/
export * from "./back-button";
export * from "./user-message-avatar";
@@ -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;
}
@@ -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;
-1
View File
@@ -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";
+1 -1
View File
@@ -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 {
@@ -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);
@@ -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";
+1 -1
View File
@@ -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";
+2 -4
View File
@@ -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);
}
})();
@@ -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 =
@@ -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";
+6 -20
View File
@@ -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<ReturnStatus>("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;
}
+8 -7
View File
@@ -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(
+8
View File
@@ -0,0 +1,8 @@
"use client";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
export async function hasBusinessLoginToken(): Promise<boolean> {
const result = await AuthStorage.getInstance().getLoginToken();
return result.success && Boolean(result.data);
}
+14
View File
@@ -52,3 +52,17 @@ export async function openChatInExternalBrowser(): Promise<void> {
UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash);
}
export async function persistExternalBrowserChatDeepLink(input: {
deviceId: string;
facebookId?: string | null;
avatarUrl?: string | null;
}): Promise<void> {
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);
}
}
+54
View File
@@ -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<Result<void>> {
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<PendingPaymentOrder | null>
> {
return PendingPaymentOrderStorage.getPendingOrder();
}
export function getPendingPaymentOrderForType(
subscriptionType: PendingPaymentSubscriptionType,
): Promise<Result<PendingPaymentOrder | null>> {
return PendingPaymentOrderStorage.getPendingOrderForType(subscriptionType);
}
export function clearPendingPaymentOrder(): Promise<Result<void>> {
return PendingPaymentOrderStorage.clearPendingOrder();
}
export function buildPendingPaymentSubscriptionUrl(
order: Pick<PendingPaymentOrder, "subscriptionType" | "returnTo">,
): string {
const params = new URLSearchParams({
type: order.subscriptionType,
paymentReturn: "1",
});
if (order.returnTo) params.set("returnTo", order.returnTo);
return `${ROUTES.subscription}?${params.toString()}`;
}