From 760e4fe343625770868ea10df6c2b1f57c16aae5 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 1 Jul 2026 14:03:25 +0800 Subject: [PATCH] refactor(app): tighten chat and sync boundaries --- src/app/api/sentry-example-api/route.ts | 17 -- src/app/chat/chat-screen.tsx | 91 +------ .../chat-input-action-panel.module.css | 50 ---- .../components/chat-input-action-panel.tsx | 37 --- ...at-insufficient-credits-banner.module.css} | 2 +- ...x => chat-insufficient-credits-banner.tsx} | 24 +- src/app/chat/components/index.ts | 3 +- .../hooks/use-chat-unlock-navigation-flow.ts | 164 ++++++++++++ .../[messageId]/chat-image-viewer-screen.tsx | 102 ++------ src/app/sentry-example-page/page.tsx | 237 ------------------ src/app/subscription/success/page.tsx | 141 ----------- src/providers/root-providers.tsx | 9 +- src/stores/chat/index.ts | 1 - src/stores/payment/index.ts | 1 - src/stores/{chat => sync}/chat-auth-sync.tsx | 3 +- src/stores/sync/index.ts | 3 + .../payment-success-sync.tsx | 3 +- src/stores/{user => sync}/user-auth-sync.tsx | 3 +- src/stores/user/index.ts | 1 - 19 files changed, 215 insertions(+), 677 deletions(-) delete mode 100644 src/app/api/sentry-example-api/route.ts delete mode 100644 src/app/chat/components/chat-input-action-panel.module.css delete mode 100644 src/app/chat/components/chat-input-action-panel.tsx rename src/app/chat/components/{chat-quota-exhausted-banner.module.css => chat-insufficient-credits-banner.module.css} (95%) rename src/app/chat/components/{chat-quota-exhausted-banner.tsx => chat-insufficient-credits-banner.tsx} (65%) create mode 100644 src/app/chat/hooks/use-chat-unlock-navigation-flow.ts delete mode 100644 src/app/sentry-example-page/page.tsx delete mode 100644 src/app/subscription/success/page.tsx rename src/stores/{chat => sync}/chat-auth-sync.tsx (96%) create mode 100644 src/stores/sync/index.ts rename src/stores/{payment => sync}/payment-success-sync.tsx (94%) rename src/stores/{user => sync}/user-auth-sync.tsx (94%) diff --git a/src/app/api/sentry-example-api/route.ts b/src/app/api/sentry-example-api/route.ts deleted file mode 100644 index aa23929a..00000000 --- a/src/app/api/sentry-example-api/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -export const dynamic = "force-dynamic"; - -class SentryExampleAPIError extends Error { - constructor(message: string | undefined) { - super(message); - this.name = "SentryExampleAPIError"; - } -} - -// A faulty API route to test Sentry's error monitoring -export function GET() { - Sentry.logger.info("Sentry example API called"); - throw new SentryExampleAPIError( - "This error is raised on the backend called by the example page.", - ); -} diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index 9e0e7694..7f83b3da 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -7,18 +7,12 @@ import { useRouter } from "next/navigation"; import { useAuthState } from "@/stores/auth/auth-context"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; import { useUserState } from "@/stores/user/user-context"; -import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; +import { ROUTES } from "@/router/routes"; import { openChatInExternalBrowser, recordExternalBrowserPromptShown, resolveExternalBrowserPromptEligibility, } from "@/lib/chat/chat_external_browser"; -import { - consumePendingChatUnlock, - peekPendingChatUnlock, - savePendingChatUnlock, - type PendingChatUnlockKind, -} from "@/lib/navigation/chat_unlock_session"; import { MobileShell } from "@/app/_components/core"; @@ -27,7 +21,7 @@ import { ChatArea, ChatHeader, ChatInputBar, - ChatQuotaExhaustedBanner, + ChatInsufficientCreditsBanner, ExternalBrowserDialog, HistoryUnlockDialog, InsufficientCreditsDialog, @@ -40,6 +34,7 @@ import { isChatDevelopmentEnvironment, shouldStartExternalBrowserPrompt, } from "./chat-screen.helpers"; +import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow"; import styles from "./components/chat-screen.module.css"; export function ChatScreen() { @@ -50,13 +45,15 @@ export function ChatScreen() { const router = useRouter(); const [showExternalBrowserDialog, setShowExternalBrowserDialog] = useState(false); - const unlockPaywallRequest = state.unlockPaywallRequest; + const { + unlockPaywallRequest, + requestMessageUnlock, + closeInsufficientCreditsDialog, + confirmInsufficientCreditsDialog, + } = useChatUnlockNavigationFlow({ returnUrl: ROUTES.chat }); // isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段) const isGuest = deriveIsGuest(authState.loginStatus); - const isAuthenticatedUser = - authState.loginStatus !== "notLoggedIn" && - authState.loginStatus !== "guest"; // 发送能力由后端统一返回,当前只处理积分不足引导。 const showMessageLimitBanner = @@ -108,75 +105,11 @@ export function ChatScreen() { authState.loginStatus, ]); - useEffect(() => { - if (!state.historyLoaded || !isAuthenticatedUser) return; - - const pending = peekPendingChatUnlock(); - if (!pending || pending.returnUrl !== ROUTES.chat) return; - - const consumed = consumePendingChatUnlock(); - if (!consumed) return; - - chatDispatch({ - type: "ChatUnlockMessageRequested", - messageId: consumed.messageId, - kind: consumed.kind, - }); - }, [ - chatDispatch, - isAuthenticatedUser, - state.historyLoaded, - ]); - - function handleCloseInsufficientCreditsDialog(): void { - chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" }); - } - - function handleConfirmInsufficientCreditsDialog(): void { - if (!unlockPaywallRequest) return; - - savePendingChatUnlock({ - messageId: unlockPaywallRequest.messageId, - kind: unlockPaywallRequest.kind, - returnUrl: ROUTES.chat, - stage: "payment", - }); - chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" }); - router.push( - ROUTE_BUILDERS.subscription( - getInsufficientCreditsSubscriptionType(userState.isVip), - { returnTo: "chat" }, - ), - ); - } - async function handleOpenExternalBrowser(): Promise { setShowExternalBrowserDialog(false); await openChatInExternalBrowser(); } - function requestMessageUnlock( - messageId: string, - kind: PendingChatUnlockKind, - ): void { - if (!isAuthenticatedUser) { - savePendingChatUnlock({ - messageId, - kind, - returnUrl: ROUTES.chat, - stage: "auth", - }); - router.push(ROUTE_BUILDERS.authWithRedirect(ROUTES.chat)); - return; - } - - chatDispatch({ - type: "ChatUnlockMessageRequested", - messageId, - kind, - }); - } - function handleUnlockPrivateMessage(messageId: string): void { requestMessageUnlock(messageId, "private"); } @@ -222,7 +155,7 @@ export function ChatScreen() { /> {showMessageLimitBanner ? ( - diff --git a/src/app/chat/components/chat-input-action-panel.module.css b/src/app/chat/components/chat-input-action-panel.module.css deleted file mode 100644 index dc2bcc5f..00000000 --- a/src/app/chat/components/chat-input-action-panel.module.css +++ /dev/null @@ -1,50 +0,0 @@ -.panel { - min-height: 170px; - padding: 24px 16px 20px; - border-top: 1px solid rgba(246, 87, 160, 0.14); - background: #fff1f4; -} - -.voiceCard { - width: 142px; - min-height: 153px; - padding: 12px 12px 14px; - border: 1px solid rgba(30, 30, 30, 0.08); - border-radius: 18px; - background: #ffffff; - box-shadow: 0 1px 2px rgba(30, 30, 30, 0.03); - color: #1e1e1e; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; -} - -.voiceCard:focus-visible { - outline: 2px solid var(--color-accent, #f84d96); - outline-offset: 3px; -} - -.voiceIcon { - display: block; - width: 78px; - height: 78px; - object-fit: contain; -} - -.voiceTitle, -.voiceMinutes { - font-family: var(--font-athelas), Athelas, serif; - font-size: 14px; - line-height: 1.08; - text-align: center; -} - -.voiceTitle { - margin-top: 8px; -} - -.voiceMinutes { - color: var(--color-accent, #f84d96); -} diff --git a/src/app/chat/components/chat-input-action-panel.tsx b/src/app/chat/components/chat-input-action-panel.tsx deleted file mode 100644 index 44388111..00000000 --- a/src/app/chat/components/chat-input-action-panel.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import Image from "next/image"; - -import styles from "./chat-input-action-panel.module.css"; - -export interface ChatInputActionPanelProps { - voiceMinutesRemaining: number; - onVoiceMessageClick?: () => void; -} - -export function ChatInputActionPanel({ - voiceMinutesRemaining, - onVoiceMessageClick, -}: ChatInputActionPanelProps) { - return ( -
- -
- ); -} diff --git a/src/app/chat/components/chat-quota-exhausted-banner.module.css b/src/app/chat/components/chat-insufficient-credits-banner.module.css similarity index 95% rename from src/app/chat/components/chat-quota-exhausted-banner.module.css rename to src/app/chat/components/chat-insufficient-credits-banner.module.css index a0da91f7..85ace838 100644 --- a/src/app/chat/components/chat-quota-exhausted-banner.module.css +++ b/src/app/chat/components/chat-insufficient-credits-banner.module.css @@ -1,4 +1,4 @@ -/* ChatQuotaExhaustedBanner — 游客配额耗尽横幅 +/* ChatInsufficientCreditsBanner — 积分不足横幅 * 视觉规格(与设计稿对齐): * - 粉→品红渐变背景(与 splash gradient 一致) * - 居中文案 + 渐变 pill 按钮 diff --git a/src/app/chat/components/chat-quota-exhausted-banner.tsx b/src/app/chat/components/chat-insufficient-credits-banner.tsx similarity index 65% rename from src/app/chat/components/chat-quota-exhausted-banner.tsx rename to src/app/chat/components/chat-insufficient-credits-banner.tsx index ae7ca4af..50fe5740 100644 --- a/src/app/chat/components/chat-quota-exhausted-banner.tsx +++ b/src/app/chat/components/chat-insufficient-credits-banner.tsx @@ -1,6 +1,6 @@ "use client"; /** - * ChatQuotaExhaustedBanner 发送受限横幅 + * ChatInsufficientCreditsBanner 积分不足横幅 * * 何时显示: * - 后端返回 cannotSendReason="insufficient_credits" @@ -8,38 +8,36 @@ * 视觉规格(与设计稿对齐): * - 粉→品红渐变背景(与 splash 渐变一致) * - 大字号粉色/白色提示文案 - * - 粉→品红渐变 pill 按钮 → 跳 /subscription + * - 粉→品红渐变 pill 按钮 → 跳 /subscription?type=topup * * 业务关系: - * - 与 `src/app/sidebar/components/vip-benefits-card.tsx` 的 "Activate VIP Membership" 行为一致 - * - 与订阅页 VIP 入口行为一致 + * - 与订阅页 top up 入口行为一致 * * 不做的事: * - 不直接管理 state machine(chat 机器不感知 UI 层) - * - 不显示具体剩余分钟数(已用 total<=0 表达"耗尽"足够) */ import { useRouter } from "next/navigation"; -import { ROUTES } from "@/router/routes"; +import { ROUTE_BUILDERS } from "@/router/routes"; -import styles from "./chat-quota-exhausted-banner.module.css"; +import styles from "./chat-insufficient-credits-banner.module.css"; -export interface ChatQuotaExhaustedBannerProps { +export interface ChatInsufficientCreditsBannerProps { title?: string; ctaLabel?: string; /** - * 自定义点击回调(不传则默认 router.push(ROUTES.subscription)) + * 自定义点击回调(不传则默认 router.push(topup subscription)) * - 测试时可传 mock * - 嵌入其他场景时(如 nested overlay)可传自定义 */ onUnlock?: () => void; } -export function ChatQuotaExhaustedBanner({ +export function ChatInsufficientCreditsBanner({ title = "Insufficient credits\nTop up to continue chatting", ctaLabel = "Top up credits to continue", onUnlock, -}: ChatQuotaExhaustedBannerProps) { +}: ChatInsufficientCreditsBannerProps) { const router = useRouter(); const titleLines = title.split("\n"); @@ -48,7 +46,7 @@ export function ChatQuotaExhaustedBanner({ onUnlock(); return; } - router.push(`${ROUTES.subscription}?type=vip`); + router.push(ROUTE_BUILDERS.subscription("topup")); }; return ( @@ -56,7 +54,7 @@ export function ChatQuotaExhaustedBanner({ className={styles.banner} role="status" aria-live="polite" - data-testid="chat-quota-exhausted-banner" + data-testid="chat-insufficient-credits-banner" >

{titleLines.map((line, index) => ( diff --git a/src/app/chat/components/index.ts b/src/app/chat/components/index.ts index b6870668..7974ab5c 100644 --- a/src/app/chat/components/index.ts +++ b/src/app/chat/components/index.ts @@ -6,10 +6,9 @@ export * from "./ai-disclosure-banner"; export * from "./browser-hint-overlay"; export * from "./chat-area"; export * from "./chat-header"; -export * from "./chat-input-action-panel"; +export * from "./chat-insufficient-credits-banner"; export * from "./chat-input-bar"; export * from "./chat-input-text-field"; -export * from "./chat-quota-exhausted-banner"; export * from "./chat-send-button"; export * from "./date-header"; export * from "./external-browser-dialog"; diff --git a/src/app/chat/hooks/use-chat-unlock-navigation-flow.ts b/src/app/chat/hooks/use-chat-unlock-navigation-flow.ts new file mode 100644 index 00000000..20791437 --- /dev/null +++ b/src/app/chat/hooks/use-chat-unlock-navigation-flow.ts @@ -0,0 +1,164 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +import { ROUTE_BUILDERS } from "@/router/routes"; +import { useAuthState } from "@/stores/auth/auth-context"; +import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; +import type { ChatUnlockPaywallRequest } from "@/stores/chat/chat-state"; +import { useUserState } from "@/stores/user/user-context"; +import { + consumePendingChatUnlock, + peekPendingChatUnlock, + savePendingChatUnlock, + type PendingChatUnlock, + type PendingChatUnlockKind, +} from "@/lib/navigation/chat_unlock_session"; + +import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers"; + +export interface UseChatUnlockNavigationFlowInput { + returnUrl: string; + expectedKind?: PendingChatUnlockKind; + expectedMessageId?: string; +} + +export interface UseChatUnlockNavigationFlowOutput { + unlockPaywallRequest: ChatUnlockPaywallRequest | null; + requestMessageUnlock: ( + messageId: string, + kind: PendingChatUnlockKind, + ) => void; + closeInsufficientCreditsDialog: () => void; + confirmInsufficientCreditsDialog: () => void; +} + +export function useChatUnlockNavigationFlow({ + returnUrl, + expectedKind, + expectedMessageId, +}: UseChatUnlockNavigationFlowInput): UseChatUnlockNavigationFlowOutput { + const router = useRouter(); + const authState = useAuthState(); + const userState = useUserState(); + const chatState = useChatState(); + const chatDispatch = useChatDispatch(); + const isAuthenticatedUser = + authState.loginStatus !== "notLoggedIn" && + authState.loginStatus !== "guest"; + const unlockPaywallRequest = getScopedUnlockPaywallRequest({ + request: chatState.unlockPaywallRequest, + expectedKind, + expectedMessageId, + }); + + useEffect(() => { + if (!chatState.historyLoaded || !isAuthenticatedUser) return; + + const pending = peekPendingChatUnlock(); + if ( + !pending || + pending.returnUrl !== returnUrl || + !matchesPendingUnlockScope({ + pending, + expectedKind, + expectedMessageId, + }) + ) { + return; + } + + const consumed = consumePendingChatUnlock(); + if (!consumed) return; + + chatDispatch({ + type: "ChatUnlockMessageRequested", + messageId: consumed.messageId, + kind: consumed.kind, + }); + }, [ + chatDispatch, + chatState.historyLoaded, + expectedKind, + expectedMessageId, + isAuthenticatedUser, + returnUrl, + ]); + + function requestMessageUnlock( + messageId: string, + kind: PendingChatUnlockKind, + ): void { + if (!isAuthenticatedUser) { + savePendingChatUnlock({ + messageId, + kind, + returnUrl, + stage: "auth", + }); + router.push(ROUTE_BUILDERS.authWithRedirect(returnUrl)); + return; + } + + chatDispatch({ + type: "ChatUnlockMessageRequested", + messageId, + kind, + }); + } + + function closeInsufficientCreditsDialog(): void { + chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" }); + } + + function confirmInsufficientCreditsDialog(): void { + if (!unlockPaywallRequest) return; + + savePendingChatUnlock({ + messageId: unlockPaywallRequest.messageId, + kind: unlockPaywallRequest.kind, + returnUrl, + stage: "payment", + }); + chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" }); + router.push( + ROUTE_BUILDERS.subscription( + getInsufficientCreditsSubscriptionType(userState.isVip), + { returnTo: "chat" }, + ), + ); + } + + return { + unlockPaywallRequest, + requestMessageUnlock, + closeInsufficientCreditsDialog, + confirmInsufficientCreditsDialog, + }; +} + +function getScopedUnlockPaywallRequest(input: { + request: ChatUnlockPaywallRequest | null; + expectedKind?: PendingChatUnlockKind; + expectedMessageId?: string; +}): ChatUnlockPaywallRequest | null { + const { request, expectedKind, expectedMessageId } = input; + if (!request) return null; + if (expectedKind && request.kind !== expectedKind) return null; + if (expectedMessageId && request.messageId !== expectedMessageId) return null; + return request; +} + +function matchesPendingUnlockScope(input: { + pending: PendingChatUnlock; + expectedKind?: PendingChatUnlockKind; + expectedMessageId?: string; +}): boolean { + const { pending, expectedKind, expectedMessageId } = input; + if (expectedKind && pending.kind !== expectedKind) return false; + if (expectedMessageId && pending.messageId !== expectedMessageId) { + return false; + } + return true; +} 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 cb08007f..53a7aa1f 100644 --- a/src/app/chat/image/[messageId]/chat-image-viewer-screen.tsx +++ b/src/app/chat/image/[messageId]/chat-image-viewer-screen.tsx @@ -1,25 +1,17 @@ "use client"; -import { useEffect } from "react"; import { useRouter } from "next/navigation"; 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 { useUserState } from "@/stores/user/user-context"; -import { - consumePendingChatUnlock, - peekPendingChatUnlock, - savePendingChatUnlock, -} from "@/lib/navigation/chat_unlock_session"; -import { getInsufficientCreditsSubscriptionType } from "../../chat-screen.helpers"; import { FullscreenImageViewer, HistoryUnlockDialog, InsufficientCreditsDialog, } from "../../components"; +import { useChatUnlockNavigationFlow } from "../../hooks/use-chat-unlock-navigation-flow"; import styles from "./chat-image-viewer-screen.module.css"; export interface ChatImageViewerScreenProps { @@ -30,95 +22,29 @@ export function ChatImageViewerScreen({ messageId, }: ChatImageViewerScreenProps) { const router = useRouter(); - const authState = useAuthState(); - const userState = useUserState(); const chatState = useChatState(); const chatDispatch = useChatDispatch(); const returnUrl = ROUTE_BUILDERS.chatImage(messageId); - const unlockPaywallRequest = - chatState.unlockPaywallRequest?.kind === "image" && - chatState.unlockPaywallRequest.messageId === messageId - ? chatState.unlockPaywallRequest - : null; - const isAuthenticatedUser = - authState.loginStatus !== "notLoggedIn" && - authState.loginStatus !== "guest"; + const { + unlockPaywallRequest, + requestMessageUnlock, + closeInsufficientCreditsDialog, + confirmInsufficientCreditsDialog, + } = useChatUnlockNavigationFlow({ + returnUrl, + expectedKind: "image", + expectedMessageId: messageId, + }); const message = chatState.messages.find( (item) => item.id === messageId && item.imageUrl, ); - useEffect(() => { - if (!chatState.historyLoaded || !isAuthenticatedUser) return; - - const pending = peekPendingChatUnlock(); - if ( - !pending || - pending.kind !== "image" || - pending.messageId !== messageId || - pending.returnUrl !== returnUrl - ) { - return; - } - - const consumed = consumePendingChatUnlock(); - if (!consumed) return; - - chatDispatch({ - type: "ChatUnlockMessageRequested", - messageId: consumed.messageId, - kind: consumed.kind, - }); - }, [ - chatDispatch, - chatState.historyLoaded, - isAuthenticatedUser, - messageId, - returnUrl, - ]); - - const handleCloseInsufficientCreditsDialog = () => { - chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" }); - }; - - const handleConfirmInsufficientCreditsDialog = () => { - if (!unlockPaywallRequest) return; - - savePendingChatUnlock({ - messageId: unlockPaywallRequest.messageId, - kind: unlockPaywallRequest.kind, - returnUrl, - stage: "payment", - }); - chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" }); - router.push( - ROUTE_BUILDERS.subscription( - getInsufficientCreditsSubscriptionType(userState.isVip), - { returnTo: "chat" }, - ), - ); - }; - const handleClose = () => { router.push(ROUTES.chat); }; const handleUnlockImagePaywall = () => { - if (!isAuthenticatedUser) { - savePendingChatUnlock({ - messageId, - kind: "image", - returnUrl, - stage: "auth", - }); - router.push(ROUTE_BUILDERS.authWithRedirect(returnUrl)); - return; - } - - chatDispatch({ - type: "ChatUnlockMessageRequested", - messageId, - kind: "image", - }); + requestMessageUnlock(messageId, "image"); }; const historyUnlockDialog = ( @@ -136,8 +62,8 @@ export function ChatImageViewerScreen({ creditBalance={unlockPaywallRequest?.creditBalance ?? 0} requiredCredits={unlockPaywallRequest?.requiredCredits ?? 0} shortfallCredits={unlockPaywallRequest?.shortfallCredits ?? 0} - onClose={handleCloseInsufficientCreditsDialog} - onConfirm={handleConfirmInsufficientCreditsDialog} + onClose={closeInsufficientCreditsDialog} + onConfirm={confirmInsufficientCreditsDialog} /> ); diff --git a/src/app/sentry-example-page/page.tsx b/src/app/sentry-example-page/page.tsx deleted file mode 100644 index 603de271..00000000 --- a/src/app/sentry-example-page/page.tsx +++ /dev/null @@ -1,237 +0,0 @@ -"use client"; - -import * as Sentry from "@sentry/nextjs"; -import Head from "next/head"; -import { useEffect, useState } from "react"; - -class SentryExampleFrontendError extends Error { - constructor(message: string | undefined) { - super(message); - this.name = "SentryExampleFrontendError"; - } -} - -export default function Page() { - const [hasSentError, setHasSentError] = useState(false); - const [isConnected, setIsConnected] = useState(true); - - useEffect(() => { - Sentry.logger.info("Sentry example page loaded"); - async function checkConnectivity() { - const result = await Sentry.diagnoseSdkConnectivity(); - setIsConnected(result !== "sentry-unreachable"); - } - checkConnectivity(); - }, []); - - return ( -

- - sentry-example-page - - - -
-
- - - -

sentry-example-page

- -

- Click the button below, and view the sample error on the Sentry{" "} - - Issues Page - - . For more details about setting up Sentry,{" "} - - read our docs - - . -

- - - - {hasSentError ? ( -

Error sent to Sentry.

- ) : !isConnected ? ( -
-

- It looks like network requests to Sentry are being blocked, which - will prevent errors from being captured. Try disabling your - ad-blocker to complete the test. -

-
- ) : ( -
- )} - -
-
- - -
- ); -} diff --git a/src/app/subscription/success/page.tsx b/src/app/subscription/success/page.tsx deleted file mode 100644 index 47b8c737..00000000 --- a/src/app/subscription/success/page.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client"; -/** - * 订阅成功回跳页 - * - * 流程: - * 1. 用户在支付服务托管页完成结账 - * 2. 支付服务重定向到 `/subscription/success` - * 3. 这个页面显示 "购买成功" + 调 VIP 状态接口确认权益 - * 4. 点 "返回订阅页" 跳回 `/subscription` - * - * 注意:权益发放由后端支付服务链路异步完成。 - * 此页面轮询几次 VIP 状态接口等待权益完成更新。 - */ -import { useEffect, useRef, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; - -import { MobileShell } from "@/app/_components/core/mobile-shell"; -import { getCurrentVipPaymentStatus } from "@/lib/payment/payment_status"; -import { ROUTES } from "@/router/routes"; - -export default function SubscriptionSuccessPage() { - const router = useRouter(); - const [status, setStatus] = useState<"polling" | "ok" | "error">("polling"); - const [errorMessage, setErrorMessage] = useState(null); - const hasRefreshed = useRef(false); - - // 轮询 VIP 状态接口等待后端权益发放完成 - useEffect(() => { - if (hasRefreshed.current) return; - hasRefreshed.current = true; - - let cancelled = false; - const tryRefresh = async (attemptsLeft: number) => { - if (cancelled) return; - const r = await getCurrentVipPaymentStatus(); - if (cancelled) return; - if (r.success) { - if (r.data.isVip) { - setStatus("ok"); - return; - } - } else { - setErrorMessage(r.error?.message ?? "Unknown error"); - } - if (attemptsLeft > 0) { - setTimeout(() => void tryRefresh(attemptsLeft - 1), 2000); - } else { - setStatus("error"); - } - }; - void tryRefresh(5); - return () => { - cancelled = true; - }; - }, []); - - return ( - -
- {status === "polling" ? ( - <> -

- Processing your subscription… -

-

- Your payment was received. We are confirming the details with - our server. -

- - ) : status === "ok" ? ( - <> -

- 🎉 Subscription activated! -

-

- Welcome to VIP. Enjoy unlimited chat and more premium benefits. -

- - - Back to subscription - - - ) : ( - <> -

- Subscription status unclear -

-

- We received your payment but could not confirm your VIP - status. {errorMessage ?? "Please refresh in a few seconds."} -

- - Back to subscription - - - )} -
-
- ); -} diff --git a/src/providers/root-providers.tsx b/src/providers/root-providers.tsx index e9cf499c..df6cecdf 100644 --- a/src/providers/root-providers.tsx +++ b/src/providers/root-providers.tsx @@ -19,10 +19,13 @@ import type { ReactNode } from "react"; import { AuthProvider } from "@/stores/auth/auth-context"; import { AuthStatusChecker } from "@/stores/auth/auth-status-checker"; import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync"; -import { ChatAuthSync } from "@/stores/chat/chat-auth-sync"; import { ChatProvider } from "@/stores/chat/chat-context"; -import { PaymentProvider, PaymentSuccessSync } from "@/stores/payment"; -import { UserAuthSync } from "@/stores/user/user-auth-sync"; +import { PaymentProvider } from "@/stores/payment"; +import { + ChatAuthSync, + PaymentSuccessSync, + UserAuthSync, +} from "@/stores/sync"; import { UserProvider } from "@/stores/user/user-context"; export interface RootProvidersProps { diff --git a/src/stores/chat/index.ts b/src/stores/chat/index.ts index 57fcea66..78cd11ae 100644 --- a/src/stores/chat/index.ts +++ b/src/stores/chat/index.ts @@ -2,7 +2,6 @@ * @file Automatically generated by barrelsby. */ -export * from "./chat-auth-sync"; export * from "./chat-context"; export * from "./chat-events"; export * from "./chat-machine.actors"; diff --git a/src/stores/payment/index.ts b/src/stores/payment/index.ts index 35f921b8..f80c359b 100644 --- a/src/stores/payment/index.ts +++ b/src/stores/payment/index.ts @@ -6,5 +6,4 @@ export * from "./payment-context"; export * from "./payment-events"; export * from "./payment-machine"; export * from "./payment-machine.actors"; -export * from "./payment-success-sync"; export * from "./payment-state"; diff --git a/src/stores/chat/chat-auth-sync.tsx b/src/stores/sync/chat-auth-sync.tsx similarity index 96% rename from src/stores/chat/chat-auth-sync.tsx rename to src/stores/sync/chat-auth-sync.tsx index fe47e785..db4a458f 100644 --- a/src/stores/chat/chat-auth-sync.tsx +++ b/src/stores/sync/chat-auth-sync.tsx @@ -12,10 +12,9 @@ import { usePathname, useRouter } from "next/navigation"; import { AuthStorage } from "@/data/storage/auth"; import { useAuthState } from "@/stores/auth/auth-context"; +import { useChatDispatch } from "@/stores/chat/chat-context"; import { PROTECTED_ROUTES, ROUTES, type StaticRoute } from "@/router/routes"; -import { useChatDispatch } from "./chat-context"; - function isProtectedPath(pathname: string): boolean { return PROTECTED_ROUTES.some((route: StaticRoute) => pathname === route); } diff --git a/src/stores/sync/index.ts b/src/stores/sync/index.ts new file mode 100644 index 00000000..71c150f4 --- /dev/null +++ b/src/stores/sync/index.ts @@ -0,0 +1,3 @@ +export * from "./chat-auth-sync"; +export * from "./payment-success-sync"; +export * from "./user-auth-sync"; diff --git a/src/stores/payment/payment-success-sync.tsx b/src/stores/sync/payment-success-sync.tsx similarity index 94% rename from src/stores/payment/payment-success-sync.tsx rename to src/stores/sync/payment-success-sync.tsx index 2f5022e0..b666498e 100644 --- a/src/stores/payment/payment-success-sync.tsx +++ b/src/stores/sync/payment-success-sync.tsx @@ -12,8 +12,7 @@ import { useEffect, useRef } from "react"; import { useChatDispatch } from "@/stores/chat/chat-context"; import { useUserDispatch } from "@/stores/user/user-context"; import { hasPendingChatUnlock } from "@/lib/navigation/chat_unlock_session"; - -import { usePaymentState } from "./payment-context"; +import { usePaymentState } from "@/stores/payment/payment-context"; export function PaymentSuccessSync() { const paymentState = usePaymentState(); diff --git a/src/stores/user/user-auth-sync.tsx b/src/stores/sync/user-auth-sync.tsx similarity index 94% rename from src/stores/user/user-auth-sync.tsx rename to src/stores/sync/user-auth-sync.tsx index 3d57041a..3759bb2a 100644 --- a/src/stores/user/user-auth-sync.tsx +++ b/src/stores/sync/user-auth-sync.tsx @@ -12,8 +12,7 @@ import { useEffect, useRef } from "react"; import type { LoginStatus } from "@/data/dto/auth"; import { useAuthState } from "@/stores/auth/auth-context"; - -import { useUserDispatch } from "./user-context"; +import { useUserDispatch } from "@/stores/user/user-context"; export function UserAuthSync() { const { hasInitialized, isLoading, loginStatus } = useAuthState(); diff --git a/src/stores/user/index.ts b/src/stores/user/index.ts index d4a710ac..b6c636b5 100644 --- a/src/stores/user/index.ts +++ b/src/stores/user/index.ts @@ -2,7 +2,6 @@ * @file Automatically generated by barrelsby. */ -export * from "./user-auth-sync"; export * from "./user-context"; export * from "./user-events"; export * from "./user-machine.actors";