refactor: scope providers by route and use XState selectors

This commit is contained in:
2026-07-13 19:07:53 +08:00
parent f9b5f22ee9
commit 37ae152abb
24 changed files with 512 additions and 336 deletions
+13 -2
View File
@@ -8,13 +8,24 @@
* 不再承担全局状态同步职责。
*/
import { useEffect, useRef } from "react";
import { shallowEqual } from "@xstate/react";
import { AuthStorage } from "@/data/storage/auth";
import { useAuthState } from "@/stores/auth/auth-context";
import {
selectAuthIsLoading,
useAuthSelector,
} from "@/stores/auth/auth-context";
import { useChatDispatch } from "@/stores/chat/chat-context";
export function ChatAuthSync() {
const authState = useAuthState();
const authState = useAuthSelector(
(state) => ({
hasInitialized: state.context.hasInitialized,
isLoading: selectAuthIsLoading(state),
loginStatus: state.context.loginStatus,
}),
shallowEqual,
);
const chatDispatch = useChatDispatch();
const prevSessionKeyRef = useRef<string | null>(null);
+51 -16
View File
@@ -1,28 +1,33 @@
"use client";
/**
* Payment → User / Chat 同步器
* Payment → User 同步器
*
* 支付成功是跨模块事件:
* - User 需要重新拉取 profile + entitlements,刷新 isVip / creditBalance。
* - Chat 需要根据历史锁定消息数量决定直接解锁或弹窗确认。
* 支付成功后刷新用户权益、清理套餐缓存,并消费首充标记。
*/
import { useEffect, useRef } from "react";
import { shallowEqual } from "@xstate/react";
import { useChatDispatch } from "@/stores/chat/chat-context";
import { useUserDispatch } from "@/stores/user/user-context";
import { hasPendingChatUnlock } from "@/lib/navigation/chat_unlock_session";
import {
usePaymentDispatch,
usePaymentState,
usePaymentSelector,
} from "@/stores/payment/payment-context";
import { getPaymentRepository } from "@/data/repositories/payment_repository";
export function PaymentSuccessSync() {
const paymentState = usePaymentState();
const paymentState = usePaymentSelector(
(state) => ({
currentOrderId: state.context.currentOrderId,
isPaid: state.matches("paid"),
orderStatus: state.context.orderStatus,
}),
shallowEqual,
);
const paymentDispatch = usePaymentDispatch();
const userDispatch = useUserDispatch();
const chatDispatch = useChatDispatch();
const lastPaidKeyRef = useRef<string | null>(null);
useEffect(() => {
@@ -33,22 +38,13 @@ export function PaymentSuccessSync() {
lastPaidKeyRef.current = paidKey;
paymentDispatch({ type: "PaymentFirstRechargeConsumed" });
let cancelled = false;
const syncPaymentSuccess = async () => {
await getPaymentRepository().clearCachedPlans();
userDispatch({ type: "UserFetch" });
if (await hasPendingChatUnlock()) return;
if (cancelled) return;
chatDispatch({ type: "ChatPaymentSucceeded" });
};
void syncPaymentSuccess();
return () => {
cancelled = true;
};
}, [
chatDispatch,
paymentState.currentOrderId,
paymentState.isPaid,
paymentState.orderStatus,
@@ -58,3 +54,42 @@ export function PaymentSuccessSync() {
return null;
}
/** Payment → Chat bridge, mounted only while the chat route is active. */
export function ChatPaymentSuccessSync() {
const paymentState = usePaymentSelector(
(state) => ({
currentOrderId: state.context.currentOrderId,
isPaid: state.matches("paid"),
orderStatus: state.context.orderStatus,
}),
shallowEqual,
);
const chatDispatch = useChatDispatch();
const lastPaidKeyRef = useRef<string | null>(null);
useEffect(() => {
if (!paymentState.isPaid || !paymentState.currentOrderId) return;
const paidKey = `${paymentState.currentOrderId}:${paymentState.orderStatus}`;
if (lastPaidKeyRef.current === paidKey) return;
lastPaidKeyRef.current = paidKey;
let cancelled = false;
void (async () => {
if (await hasPendingChatUnlock()) return;
if (!cancelled) chatDispatch({ type: "ChatPaymentSucceeded" });
})();
return () => {
cancelled = true;
};
}, [
chatDispatch,
paymentState.currentOrderId,
paymentState.isPaid,
paymentState.orderStatus,
]);
return null;
}
+13 -2
View File
@@ -9,13 +9,24 @@
* - guest / email / OAuth → 重新 hydrate user store
*/
import { useEffect, useRef } from "react";
import { shallowEqual } from "@xstate/react";
import type { LoginStatus } from "@/data/dto/auth";
import { useAuthState } from "@/stores/auth/auth-context";
import {
selectAuthIsLoading,
useAuthSelector,
} from "@/stores/auth/auth-context";
import { useUserDispatch } from "@/stores/user/user-context";
export function UserAuthSync() {
const { hasInitialized, isLoading, loginStatus } = useAuthState();
const { hasInitialized, isLoading, loginStatus } = useAuthSelector(
(state) => ({
hasInitialized: state.context.hasInitialized,
isLoading: selectAuthIsLoading(state),
loginStatus: state.context.loginStatus,
}),
shallowEqual,
);
const userDispatch = useUserDispatch();
const prevLoginStatusRef = useRef<LoginStatus | null>(null);