refactor(app): tighten feature boundaries
This commit is contained in:
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user