修复泥点消耗刷新不及时 (#138)
refactor: - 主站复用game agent的zustand store fix: - 修复旧请求覆盖问题 - 修复账号切换问题 - 主站原本已经每 4 秒轮询 external generation 任务状态, 在这里补充触发余额刷新的时机 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/138 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #138.
This commit is contained in:
@@ -11,7 +11,10 @@ export function AccountWalletBar({
|
||||
return (
|
||||
<div className="launcher-account-bar" aria-label="账户资产">
|
||||
<PlatformMudPointWalletEntry
|
||||
balance={controller.mudPointBalance?.totalPoints ?? null}
|
||||
balance={
|
||||
controller.mudPointBalance?.totalPoints ??
|
||||
controller.legacyWalletBalance
|
||||
}
|
||||
breakdown={controller.mudPointBalance}
|
||||
isLoading={controller.mudPointBalanceStatus === 'loading'}
|
||||
error={controller.mudPointBalanceError || null}
|
||||
@@ -49,7 +52,11 @@ export function AccountWalletDialogs({
|
||||
{controller.walletLedgerOpen ? (
|
||||
<PlatformProfileWalletLedgerModal
|
||||
ledger={controller.walletLedger}
|
||||
fallbackBalance={controller.mudPointBalance?.totalPoints ?? 0}
|
||||
fallbackBalance={
|
||||
controller.mudPointBalance?.totalPoints ??
|
||||
controller.legacyWalletBalance ??
|
||||
0
|
||||
}
|
||||
isLoading={controller.walletLedgerLoading}
|
||||
error={controller.walletLedgerError}
|
||||
onClose={() => controller.setWalletLedgerOpen(false)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { PlatformProfileRechargeNativePaymentState } from '../../../../../packages/shared/src/components/PlatformProfileRechargeModal';
|
||||
import type {
|
||||
@@ -17,10 +17,15 @@ import { useWalletStore } from '../../stores/useWalletStore';
|
||||
|
||||
export function useAccountWallet(currentUserId: string) {
|
||||
const {
|
||||
ownerUserId,
|
||||
mudPointBalance,
|
||||
legacyWalletBalance,
|
||||
mudPointBalanceStatus,
|
||||
mudPointBalanceError,
|
||||
setWalletOwner,
|
||||
captureWalletBalanceSnapshot,
|
||||
applyWalletBalanceSnapshot,
|
||||
applyLegacyWalletBalanceSnapshot,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
resetWalletBalance,
|
||||
} = useWalletStore();
|
||||
@@ -41,32 +46,98 @@ export function useAccountWallet(currentUserId: string) {
|
||||
const [nativeRechargePayment, setNativeRechargePayment] =
|
||||
useState<PlatformProfileRechargeNativePaymentState | null>(null);
|
||||
const rechargeLifecycleRef = useRef(0);
|
||||
const walletLedgerLifecycleRef = useRef(0);
|
||||
const [walletUiOwnerUserId, setWalletUiOwnerUserId] = useState(currentUserId);
|
||||
const currentUserIdRef = useRef(currentUserId);
|
||||
const walletOwnerMatchesCurrentUser =
|
||||
Boolean(currentUserId) && ownerUserId === currentUserId;
|
||||
const walletUiOwnerMatchesCurrentUser =
|
||||
Boolean(currentUserId) && walletUiOwnerUserId === currentUserId;
|
||||
const walletUiIsVisible =
|
||||
walletOwnerMatchesCurrentUser && walletUiOwnerMatchesCurrentUser;
|
||||
const visibleMudPointBalance = walletOwnerMatchesCurrentUser
|
||||
? mudPointBalance
|
||||
: null;
|
||||
const visibleLegacyWalletBalance = walletOwnerMatchesCurrentUser
|
||||
? legacyWalletBalance
|
||||
: null;
|
||||
const visibleMudPointBalanceStatus = walletOwnerMatchesCurrentUser
|
||||
? mudPointBalanceStatus
|
||||
: 'idle';
|
||||
const visibleMudPointBalanceError = walletOwnerMatchesCurrentUser
|
||||
? mudPointBalanceError
|
||||
: '';
|
||||
const visibleWalletBalance =
|
||||
visibleMudPointBalance?.totalPoints ?? visibleLegacyWalletBalance;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
currentUserIdRef.current = currentUserId;
|
||||
rechargeLifecycleRef.current += 1;
|
||||
walletLedgerLifecycleRef.current += 1;
|
||||
}, [currentUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
resetWalletBalance();
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
setWalletOwner(currentUserId || null);
|
||||
if (currentUserId) {
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
}
|
||||
|
||||
const refreshWalletBalance = () => {
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
if (currentUserId) {
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
}
|
||||
};
|
||||
window.addEventListener('focus', refreshWalletBalance);
|
||||
return () => {
|
||||
window.removeEventListener('focus', refreshWalletBalance);
|
||||
};
|
||||
}, [currentUserId, onWalletBalanceMayHaveChanged, resetWalletBalance]);
|
||||
}, [currentUserId, onWalletBalanceMayHaveChanged, setWalletOwner]);
|
||||
|
||||
useEffect(() => {
|
||||
setWalletUiOwnerUserId(currentUserId);
|
||||
setWalletLedgerOpen(false);
|
||||
setWalletLedger(null);
|
||||
setWalletLedgerLoading(false);
|
||||
setWalletLedgerError(null);
|
||||
setRechargeOpen(false);
|
||||
setRechargeContent(null);
|
||||
setRechargeLoading(false);
|
||||
setRechargeError(null);
|
||||
setSubmittingRechargeProductId(null);
|
||||
setNativeRechargePayment(null);
|
||||
}, [currentUserId]);
|
||||
|
||||
async function loadWalletLedger() {
|
||||
const walletLedgerLifecycle = ++walletLedgerLifecycleRef.current;
|
||||
const snapshotOwnerUserId = currentUserId;
|
||||
setWalletLedgerLoading(true);
|
||||
setWalletLedgerError(null);
|
||||
try {
|
||||
setWalletLedger(await getClientProfileWalletLedger());
|
||||
const ledger = await getClientProfileWalletLedger();
|
||||
if (
|
||||
walletLedgerLifecycleRef.current === walletLedgerLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setWalletLedger(ledger);
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
walletLedgerLifecycleRef.current !== walletLedgerLifecycle ||
|
||||
currentUserIdRef.current !== snapshotOwnerUserId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setWalletLedger(null);
|
||||
setWalletLedgerError(
|
||||
error instanceof Error ? error.message : '读取泥点账单失败',
|
||||
);
|
||||
} finally {
|
||||
setWalletLedgerLoading(false);
|
||||
if (
|
||||
walletLedgerLifecycleRef.current === walletLedgerLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setWalletLedgerLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,37 +146,55 @@ export function useAccountWallet(currentUserId: string) {
|
||||
void loadWalletLedger();
|
||||
}
|
||||
|
||||
function applyRechargeContent(center: ProfileRechargeCenterResponse) {
|
||||
function applyRechargeContent(
|
||||
walletSnapshot: ReturnType<typeof captureWalletBalanceSnapshot>,
|
||||
center: ProfileRechargeCenterResponse,
|
||||
) {
|
||||
const {
|
||||
walletBalance,
|
||||
mudPointBalance: nextMudPointBalance,
|
||||
...content
|
||||
} = center;
|
||||
void walletBalance;
|
||||
if (nextMudPointBalance) {
|
||||
applyWalletBalanceSnapshot(nextMudPointBalance);
|
||||
if (nextMudPointBalance && walletSnapshot) {
|
||||
applyWalletBalanceSnapshot(walletSnapshot, nextMudPointBalance);
|
||||
} else if (walletSnapshot && Number.isFinite(walletBalance)) {
|
||||
applyLegacyWalletBalanceSnapshot(walletSnapshot, walletBalance);
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
} else {
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
}
|
||||
setRechargeContent(content);
|
||||
}
|
||||
|
||||
async function loadRechargeCenter() {
|
||||
const snapshotOwnerUserId = currentUserId;
|
||||
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
|
||||
const rechargeLifecycle = rechargeLifecycleRef.current;
|
||||
setRechargeLoading(true);
|
||||
setRechargeError(null);
|
||||
try {
|
||||
const center = await getClientProfileRechargeCenter();
|
||||
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current !== rechargeLifecycle ||
|
||||
currentUserIdRef.current !== snapshotOwnerUserId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
applyRechargeContent(center);
|
||||
applyRechargeContent(walletSnapshot, center);
|
||||
} catch (error) {
|
||||
if (rechargeLifecycleRef.current === rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current === rechargeLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setRechargeError(
|
||||
error instanceof Error ? error.message : '读取泥点购买信息失败',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (rechargeLifecycleRef.current === rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current === rechargeLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setRechargeLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -130,16 +219,21 @@ export function useAccountWallet(currentUserId: string) {
|
||||
return;
|
||||
}
|
||||
const rechargeLifecycle = rechargeLifecycleRef.current;
|
||||
const snapshotOwnerUserId = currentUserId;
|
||||
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
|
||||
setSubmittingRechargeProductId(product.productId);
|
||||
setRechargeError(null);
|
||||
try {
|
||||
const response = await createClientProfileRechargeOrder(
|
||||
product.productId,
|
||||
);
|
||||
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current !== rechargeLifecycle ||
|
||||
currentUserIdRef.current !== snapshotOwnerUserId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
applyRechargeContent(response.center);
|
||||
applyRechargeContent(walletSnapshot, response.center);
|
||||
const nativePayment = response.wechatNativePayment;
|
||||
const codeUrl = nativePayment?.codeUrl?.trim();
|
||||
const expiresAt = nativePayment?.expiresAt?.trim();
|
||||
@@ -155,11 +249,17 @@ export function useAccountWallet(currentUserId: string) {
|
||||
isConfirming: false,
|
||||
});
|
||||
} catch (error) {
|
||||
if (rechargeLifecycleRef.current === rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current === rechargeLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setRechargeError(error instanceof Error ? error.message : '充值失败');
|
||||
}
|
||||
} finally {
|
||||
if (rechargeLifecycleRef.current === rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current === rechargeLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setSubmittingRechargeProductId(null);
|
||||
}
|
||||
}
|
||||
@@ -170,6 +270,8 @@ export function useAccountWallet(currentUserId: string) {
|
||||
return;
|
||||
}
|
||||
const rechargeLifecycle = rechargeLifecycleRef.current;
|
||||
const snapshotOwnerUserId = currentUserId;
|
||||
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
|
||||
const orderId = nativeRechargePayment.orderId;
|
||||
setNativeRechargePayment((current) =>
|
||||
current?.orderId === orderId
|
||||
@@ -178,10 +280,13 @@ export function useAccountWallet(currentUserId: string) {
|
||||
);
|
||||
try {
|
||||
const response = await confirmClientWechatProfileRechargeOrder(orderId);
|
||||
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current !== rechargeLifecycle ||
|
||||
currentUserIdRef.current !== snapshotOwnerUserId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
applyRechargeContent(response.center);
|
||||
applyRechargeContent(walletSnapshot, response.center);
|
||||
if (response.order.status === 'paid') {
|
||||
setNativeRechargePayment(null);
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
@@ -199,7 +304,10 @@ export function useAccountWallet(currentUserId: string) {
|
||||
: current,
|
||||
);
|
||||
} catch {
|
||||
if (rechargeLifecycleRef.current === rechargeLifecycle) {
|
||||
if (
|
||||
rechargeLifecycleRef.current === rechargeLifecycle &&
|
||||
currentUserIdRef.current === snapshotOwnerUserId
|
||||
) {
|
||||
setNativeRechargePayment((current) =>
|
||||
current?.orderId === orderId
|
||||
? {
|
||||
@@ -214,29 +322,35 @@ export function useAccountWallet(currentUserId: string) {
|
||||
}
|
||||
|
||||
return {
|
||||
mudPointBalance,
|
||||
mudPointBalanceStatus,
|
||||
mudPointBalanceError,
|
||||
ownerUserId,
|
||||
mudPointBalance: visibleMudPointBalance,
|
||||
legacyWalletBalance: visibleLegacyWalletBalance,
|
||||
mudPointBalanceStatus: visibleMudPointBalanceStatus,
|
||||
mudPointBalanceError: visibleMudPointBalanceError,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
resetWalletBalance,
|
||||
walletLedgerOpen,
|
||||
walletLedgerOpen: walletUiIsVisible && walletLedgerOpen,
|
||||
setWalletLedgerOpen,
|
||||
walletLedger,
|
||||
walletLedgerLoading,
|
||||
walletLedgerError,
|
||||
rechargeOpen,
|
||||
walletLedger: walletUiIsVisible ? walletLedger : null,
|
||||
walletLedgerLoading: walletUiIsVisible && walletLedgerLoading,
|
||||
walletLedgerError: walletUiIsVisible ? walletLedgerError : null,
|
||||
rechargeOpen: walletUiIsVisible && rechargeOpen,
|
||||
rechargeModalCenter:
|
||||
rechargeContent && mudPointBalance
|
||||
rechargeContent && visibleWalletBalance !== null
|
||||
? {
|
||||
...rechargeContent,
|
||||
walletBalance: mudPointBalance.totalPoints,
|
||||
mudPointBalance,
|
||||
walletBalance: visibleWalletBalance,
|
||||
...(visibleMudPointBalance
|
||||
? { mudPointBalance: visibleMudPointBalance }
|
||||
: {}),
|
||||
}
|
||||
: null,
|
||||
rechargeLoading,
|
||||
rechargeError,
|
||||
submittingRechargeProductId,
|
||||
nativeRechargePayment,
|
||||
rechargeLoading: walletUiIsVisible && rechargeLoading,
|
||||
rechargeError: walletUiIsVisible ? rechargeError : null,
|
||||
submittingRechargeProductId: walletUiIsVisible
|
||||
? submittingRechargeProductId
|
||||
: null,
|
||||
nativeRechargePayment: walletUiIsVisible ? nativeRechargePayment : null,
|
||||
setNativeRechargePayment,
|
||||
loadWalletLedger,
|
||||
openWalletLedger,
|
||||
|
||||
@@ -137,10 +137,10 @@ export function getClientProfileDashboard() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getClientProfileRechargeCenter() {
|
||||
export function getClientProfileRechargeCenter(signal?: AbortSignal) {
|
||||
return requestClientApi<ProfileRechargeCenterResponse>(
|
||||
'/api/profile/recharge-center',
|
||||
{ method: 'GET' },
|
||||
{ method: 'GET', signal },
|
||||
'读取泥点明细失败',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,100 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import type { ProfileMudPointBalance } from '../../../../packages/shared/src';
|
||||
import { createProfileWalletStore } from '../../../../packages/shared/src';
|
||||
import { getClientProfileRechargeCenter } from '../services/clientApi';
|
||||
|
||||
export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error';
|
||||
|
||||
type WalletStore = {
|
||||
mudPointBalance: ProfileMudPointBalance | null;
|
||||
mudPointBalanceStatus: MudPointBalanceStatus;
|
||||
mudPointBalanceError: string;
|
||||
applyWalletBalanceSnapshot: (balance: ProfileMudPointBalance) => void;
|
||||
onWalletBalanceMayHaveChanged: () => Promise<void>;
|
||||
resetWalletBalance: () => void;
|
||||
};
|
||||
|
||||
const initialWalletState: Pick<
|
||||
WalletStore,
|
||||
'mudPointBalance' | 'mudPointBalanceStatus' | 'mudPointBalanceError'
|
||||
> = {
|
||||
mudPointBalance: null,
|
||||
mudPointBalanceStatus: 'idle',
|
||||
mudPointBalanceError: '',
|
||||
};
|
||||
|
||||
let refreshVersion = 0;
|
||||
let requestGeneration = 0;
|
||||
let activeRefresh: Promise<void> | null = null;
|
||||
|
||||
export const useWalletStore = create<WalletStore>((set) => ({
|
||||
...initialWalletState,
|
||||
applyWalletBalanceSnapshot: (balance) => {
|
||||
requestGeneration += 1;
|
||||
refreshVersion = 0;
|
||||
activeRefresh = null;
|
||||
set({
|
||||
mudPointBalance: balance,
|
||||
mudPointBalanceStatus: 'ready',
|
||||
mudPointBalanceError: '',
|
||||
});
|
||||
},
|
||||
onWalletBalanceMayHaveChanged: () => {
|
||||
refreshVersion += 1;
|
||||
if (activeRefresh) {
|
||||
return activeRefresh;
|
||||
}
|
||||
|
||||
const refreshRequestGeneration = requestGeneration;
|
||||
const refresh = (async () => {
|
||||
while (refreshRequestGeneration === requestGeneration) {
|
||||
const requestedVersion = refreshVersion;
|
||||
set({ mudPointBalanceStatus: 'loading', mudPointBalanceError: '' });
|
||||
try {
|
||||
const center = await getClientProfileRechargeCenter();
|
||||
if (!center.mudPointBalance) {
|
||||
throw new Error('充值中心响应缺少泥点余额');
|
||||
}
|
||||
if (refreshRequestGeneration !== requestGeneration) {
|
||||
return;
|
||||
}
|
||||
if (requestedVersion !== refreshVersion) {
|
||||
continue;
|
||||
}
|
||||
set({
|
||||
mudPointBalance: center.mudPointBalance,
|
||||
mudPointBalanceStatus: 'ready',
|
||||
mudPointBalanceError: '',
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
if (refreshRequestGeneration !== requestGeneration) {
|
||||
return;
|
||||
}
|
||||
if (requestedVersion !== refreshVersion) {
|
||||
continue;
|
||||
}
|
||||
set({
|
||||
mudPointBalanceStatus: 'error',
|
||||
mudPointBalanceError:
|
||||
error instanceof Error ? error.message : '泥点明细读取失败',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
activeRefresh = refresh;
|
||||
void refresh.finally(() => {
|
||||
if (activeRefresh === refresh) {
|
||||
activeRefresh = null;
|
||||
}
|
||||
});
|
||||
return refresh;
|
||||
},
|
||||
resetWalletBalance: () => {
|
||||
requestGeneration += 1;
|
||||
refreshVersion = 0;
|
||||
activeRefresh = null;
|
||||
set(initialWalletState);
|
||||
},
|
||||
}));
|
||||
export const useWalletStore = createProfileWalletStore({
|
||||
getRechargeCenter: getClientProfileRechargeCenter,
|
||||
});
|
||||
|
||||
@@ -303,13 +303,23 @@ export function registerClientHomeTests() {
|
||||
([command]) => command === 'inspect_local_project_directory',
|
||||
).length;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(runtimeHarness.listen).toHaveBeenCalledWith(
|
||||
'game-creator-manifest-invalidated',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
manifestChanged = true;
|
||||
act(() => {
|
||||
runtimeHarness.emitManifestInvalidated('art-asset-plan');
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /runtime-live-hero\.png/ }),
|
||||
await screen.findByRole(
|
||||
'button',
|
||||
{ name: /runtime-live-hero\.png/ },
|
||||
{ timeout: 5_000 },
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByRole('button', { name: /版本 1/ })).not.toBeNull();
|
||||
expect(runButton.getAttribute('data-unavailable')).toBeNull();
|
||||
@@ -359,9 +369,7 @@ export function registerClientHomeTests() {
|
||||
source: { kind: 'generated' },
|
||||
},
|
||||
];
|
||||
let resolveStaleRefresh!: (
|
||||
manifest: typeof staleFirstManifest,
|
||||
) => void;
|
||||
let resolveStaleRefresh!: (manifest: typeof staleFirstManifest) => void;
|
||||
const staleRefresh = new Promise<typeof staleFirstManifest>((resolve) => {
|
||||
resolveStaleRefresh = resolve;
|
||||
});
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createElement } from 'react';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const clientApi = vi.hoisted(() => ({
|
||||
confirmClientWechatProfileRechargeOrder: vi.fn(),
|
||||
createClientProfileRechargeOrder: vi.fn(),
|
||||
getClientProfileRechargeCenter: vi.fn(),
|
||||
getClientProfileWalletLedger: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/clientApi', () => clientApi);
|
||||
|
||||
import { useAccountWallet } from '../src/features/app-shell/useAccountWallet';
|
||||
import { useWalletStore } from '../src/stores/useWalletStore';
|
||||
|
||||
function detailedBalance(totalPoints: number) {
|
||||
@@ -26,10 +32,11 @@ function detailedBalance(totalPoints: number) {
|
||||
describe('useWalletStore', () => {
|
||||
beforeEach(() => {
|
||||
useWalletStore.getState().resetWalletBalance();
|
||||
useWalletStore.getState().setWalletOwner('user-a');
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('keeps mudPointBalance as the only balance state', async () => {
|
||||
test('keeps mudPointBalance as the authoritative detailed balance state', async () => {
|
||||
const mudPointBalance = detailedBalance(120);
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 120,
|
||||
@@ -42,15 +49,21 @@ describe('useWalletStore', () => {
|
||||
});
|
||||
|
||||
expect(result.current.mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(result.current.legacyWalletBalance).toBe(120);
|
||||
expect(result.current.mudPointBalanceStatus).toBe('ready');
|
||||
expect(result.current.mudPointBalanceError).toBe('');
|
||||
expect(Object.keys(result.current).sort()).toEqual([
|
||||
'applyLegacyWalletBalanceSnapshot',
|
||||
'applyWalletBalanceSnapshot',
|
||||
'captureWalletBalanceSnapshot',
|
||||
'legacyWalletBalance',
|
||||
'mudPointBalance',
|
||||
'mudPointBalanceError',
|
||||
'mudPointBalanceStatus',
|
||||
'onWalletBalanceMayHaveChanged',
|
||||
'ownerUserId',
|
||||
'resetWalletBalance',
|
||||
'setWalletOwner',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -61,7 +74,12 @@ describe('useWalletStore', () => {
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
const mudPointBalance = detailedBalance(120);
|
||||
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
|
||||
const snapshot = useWalletStore
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
useWalletStore
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(snapshot!, mudPointBalance);
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
||||
@@ -79,7 +97,12 @@ describe('useWalletStore', () => {
|
||||
|
||||
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const mudPointBalance = detailedBalance(160);
|
||||
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
|
||||
const snapshot = useWalletStore
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
useWalletStore
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(snapshot!, mudPointBalance);
|
||||
rejectRequest?.(new Error('过期请求失败'));
|
||||
await refresh;
|
||||
|
||||
@@ -96,9 +119,10 @@ describe('useWalletStore', () => {
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
||||
expect(useWalletStore.getState().legacyWalletBalance).toBe(120);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe(
|
||||
'充值中心响应缺少泥点余额',
|
||||
'泥点明细读取失败',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -128,10 +152,12 @@ describe('useWalletStore', () => {
|
||||
mudPointBalance: detailedBalance(80),
|
||||
});
|
||||
|
||||
const firstRefresh =
|
||||
useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const trailingRefresh =
|
||||
useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const firstRefresh = useWalletStore
|
||||
.getState()
|
||||
.onWalletBalanceMayHaveChanged();
|
||||
const trailingRefresh = useWalletStore
|
||||
.getState()
|
||||
.onWalletBalanceMayHaveChanged();
|
||||
resolveFirst?.({
|
||||
walletBalance: 120,
|
||||
mudPointBalance: detailedBalance(120),
|
||||
@@ -156,7 +182,9 @@ describe('useWalletStore', () => {
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(90);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('刷新失败');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe(
|
||||
'泥点明细读取失败',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not restore a late response after reset', async () => {
|
||||
@@ -179,4 +207,148 @@ describe('useWalletStore', () => {
|
||||
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('idle');
|
||||
});
|
||||
|
||||
test('clears immediately when the adapter switches wallet owners', () => {
|
||||
const snapshot = useWalletStore
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
useWalletStore
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(snapshot!, detailedBalance(88));
|
||||
|
||||
useWalletStore.getState().setWalletOwner('user-b');
|
||||
|
||||
expect(useWalletStore.getState()).toMatchObject({
|
||||
ownerUserId: 'user-b',
|
||||
mudPointBalance: null,
|
||||
mudPointBalanceStatus: 'idle',
|
||||
});
|
||||
});
|
||||
|
||||
test('masks another owner snapshot before the owner-binding effect runs', () => {
|
||||
const snapshot = useWalletStore
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
useWalletStore
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(snapshot!, detailedBalance(88));
|
||||
|
||||
function WalletProbe() {
|
||||
const wallet = useAccountWallet('user-b');
|
||||
return createElement(
|
||||
'span',
|
||||
null,
|
||||
wallet.mudPointBalance?.totalPoints ?? 'empty',
|
||||
);
|
||||
}
|
||||
|
||||
expect(renderToString(createElement(WalletProbe))).toContain('empty');
|
||||
expect(useWalletStore.getState().ownerUserId).toBe('user-a');
|
||||
});
|
||||
|
||||
test('binds the account owner and refreshes again when the window regains focus', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 18,
|
||||
mudPointBalance: detailedBalance(18),
|
||||
});
|
||||
|
||||
renderHook(() => useAccountWallet('user-a'));
|
||||
await act(async () => undefined);
|
||||
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(new Event('focus'));
|
||||
});
|
||||
|
||||
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(2);
|
||||
expect(useWalletStore.getState()).toMatchObject({
|
||||
ownerUserId: 'user-a',
|
||||
mudPointBalance: detailedBalance(18),
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps a successful legacy recharge total when the breakdown refresh fails', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValueOnce({
|
||||
walletBalance: 90,
|
||||
mudPointBalance: detailedBalance(90),
|
||||
});
|
||||
const { result } = renderHook(() => useAccountWallet('user-a'));
|
||||
await act(async () => undefined);
|
||||
clientApi.getClientProfileRechargeCenter
|
||||
.mockResolvedValueOnce({
|
||||
walletBalance: 37,
|
||||
membership: null,
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('明细刷新失败'));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadRechargeCenter();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.legacyWalletBalance).toBe(37);
|
||||
expect(result.current.mudPointBalance).toBeNull();
|
||||
});
|
||||
|
||||
test('keeps ledger loading independent from the recharge lifecycle', async () => {
|
||||
let resolveLedger!: (value: { entries: [] }) => void;
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 18,
|
||||
mudPointBalance: detailedBalance(18),
|
||||
});
|
||||
clientApi.getClientProfileWalletLedger.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveLedger = resolve;
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() => useAccountWallet('user-a'));
|
||||
await act(async () => undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.openWalletLedger();
|
||||
result.current.openRecharge();
|
||||
});
|
||||
expect(result.current.walletLedgerLoading).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
resolveLedger({ entries: [] });
|
||||
});
|
||||
|
||||
expect(result.current.walletLedger).toEqual({ entries: [] });
|
||||
expect(result.current.walletLedgerLoading).toBe(false);
|
||||
});
|
||||
|
||||
test('hides account-owned dialogs and data when the account changes', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 18,
|
||||
mudPointBalance: detailedBalance(18),
|
||||
});
|
||||
clientApi.getClientProfileWalletLedger.mockResolvedValue({ entries: [] });
|
||||
const { result, rerender } = renderHook(
|
||||
({ userId }) => useAccountWallet(userId),
|
||||
{ initialProps: { userId: 'user-a' } },
|
||||
);
|
||||
await act(async () => undefined);
|
||||
await act(async () => {
|
||||
result.current.openWalletLedger();
|
||||
result.current.openRecharge();
|
||||
});
|
||||
expect(result.current.walletLedgerOpen).toBe(true);
|
||||
expect(result.current.rechargeOpen).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
rerender({ userId: 'user-b' });
|
||||
});
|
||||
|
||||
expect(result.current.walletLedgerOpen).toBe(false);
|
||||
expect(result.current.walletLedger).toBeNull();
|
||||
expect(result.current.rechargeOpen).toBe(false);
|
||||
expect(result.current.nativeRechargePayment).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user