fix: lock recharge flow until virtual payment settles

This commit is contained in:
kdletters
2026-06-02 01:47:39 +08:00
parent 1cb11bc1dd
commit 2fdeb34567
13 changed files with 1167 additions and 246 deletions

View File

@@ -49,6 +49,7 @@ const {
mockGetRpgProfileTasks,
mockGetRpgProfileWalletLedger,
mockRedeemRpgProfileReferralInviteCode,
mockWatchWechatRpgProfileRechargeOrder,
} = vi.hoisted(() => {
const qrCodeToDataUrl = vi.fn(async () => 'data:image/png;base64,QR');
const redirectToPaymentUrl = vi.fn();
@@ -313,6 +314,7 @@ const {
},
],
})),
mockWatchWechatRpgProfileRechargeOrder: vi.fn(async () => null),
};
});
@@ -379,6 +381,7 @@ vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
createRpgProfileRechargeOrder: mockCreateRpgProfileRechargeOrder,
confirmWechatRpgProfileRechargeOrder:
mockConfirmWechatRpgProfileRechargeOrder,
watchWechatRpgProfileRechargeOrder: mockWatchWechatRpgProfileRechargeOrder,
}));
vi.mock('../ResolvedAssetImage', () => ({
@@ -1407,7 +1410,7 @@ test('profile recharge modal posts virtual payment params in mini program web-vi
'requestId',
);
expect(requestId).toBeTruthy();
expect(screen.getByRole('dialog', { name: '正在支付' })).toBeTruthy();
expect(screen.queryByRole('dialog', { name: '正在支付' })).toBeNull();
act(() => {
window.location.hash = `wx_pay_result=${requestId}:success:order-wechat-1`;
window.dispatchEvent(new HashChangeEvent('hashchange'));
@@ -1928,6 +1931,234 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
});
test('profile recharge modal confirms virtual payment after returning without hash result', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
options.success?.();
});
window.wx = {
miniProgram: {
navigateTo,
},
};
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
order: {
orderId: 'order-wechat-no-hash-paid',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null as string | null,
providerTransactionId: null,
pointsDelta: 0,
membershipExpiresAt: null,
},
center: {
walletBalance: 0,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
},
wechatMiniProgramPayParams: {
mode: 'short_series_coin',
signData:
'{"offerId":"offer-1","buyQuantity":60,"env":0,"currencyType":"CNY","outTradeNo":"order-wechat-no-hash-paid","attach":"mud_points_60"}',
paySig: 'pay-sig',
signature: 'user-sig',
},
});
mockConfirmWechatRpgProfileRechargeOrder
.mockResolvedValueOnce({
order: {
orderId: 'order-wechat-no-hash-paid',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null,
providerTransactionId: null,
pointsDelta: 0,
membershipExpiresAt: null,
},
center: {
walletBalance: 0,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
},
})
.mockResolvedValueOnce({
order: {
orderId: 'order-wechat-no-hash-paid',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'paid' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: '2026-04-25T10:01:00Z',
providerTransactionId: 'wx-transaction-no-hash',
pointsDelta: 120,
membershipExpiresAt: null,
},
center: {
walletBalance: 120,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: true,
},
});
renderProfileView(onRechargeSuccess);
await openRechargeModal(user);
await user.click(await screen.findByRole('button', { name: /60/u }));
act(() => {
window.dispatchEvent(new PageTransitionEvent('pageshow'));
});
expect(screen.getByRole('dialog', { name: '正在确认支付' })).toBeTruthy();
await waitFor(() => {
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
'order-wechat-no-hash-paid',
);
});
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
});
test('profile recharge modal blocks tab navigation while virtual payment confirmation is pending', async () => {
const user = userEvent.setup();
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
options.success?.();
});
window.wx = {
miniProgram: {
navigateTo,
},
};
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
order: {
orderId: 'order-wechat-confirm-mask',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null as string | null,
providerTransactionId: null,
pointsDelta: 0,
membershipExpiresAt: null,
},
center: {
walletBalance: 0,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
},
wechatMiniProgramPayParams: {
mode: 'short_series_coin',
signData:
'{"offerId":"offer-1","buyQuantity":60,"env":0,"currencyType":"CNY","outTradeNo":"order-wechat-confirm-mask","attach":"mud_points_60"}',
paySig: 'pay-sig',
signature: 'user-sig',
},
});
mockConfirmWechatRpgProfileRechargeOrder.mockResolvedValueOnce({
order: {
orderId: 'order-wechat-confirm-mask',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp_virtual',
createdAt: '2026-04-25T10:00:00Z',
paidAt: null,
providerTransactionId: null,
pointsDelta: 0,
membershipExpiresAt: null,
},
center: {
walletBalance: 0,
membership: {
status: 'normal',
tier: 'normal',
startedAt: null,
expiresAt: null,
updatedAt: null,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
},
});
mockWatchWechatRpgProfileRechargeOrder.mockReturnValueOnce(new Promise(() => undefined));
renderProfileView();
await openRechargeModal(user);
await user.click(await screen.findByRole('button', { name: /60/u }));
act(() => {
window.dispatchEvent(new PageTransitionEvent('pageshow'));
});
expect(screen.getByRole('dialog', { name: '正在确认支付' })).toBeTruthy();
expect(
(screen.getByRole('button', { name: '创作' }) as HTMLButtonElement)
.disabled,
).toBe(true);
});
test('profile recharge modal loads wechat js sdk before mini program payment bridge', async () => {
const user = userEvent.setup();
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');