refactor: scope providers by route and use XState selectors
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user