修复小程序内充值设备识别

为 API 请求和刷新请求补充宿主运行时请求头。

在小程序充值设备身份异常时清理旧 token 并重新拉起小程序登录。

补充小程序充值回归测试并隔离测试 spy。
This commit is contained in:
kdletters
2026-07-09 16:27:43 +08:00
parent 67fee3718d
commit c022bbb8a0
4 changed files with 221 additions and 9 deletions
@@ -10,9 +10,16 @@ import {
type RedeemProfileRewardCodeResponse,
type WechatNativePayment,
} from '../../../packages/shared/src/contracts/runtime';
import { refreshStoredAccessToken } from '../../services/apiClient';
import {
clearStoredAccessToken,
refreshStoredAccessToken,
} from '../../services/apiClient';
import { startWechatBind, type AuthUser } from '../../services/authService';
import { requestHostPayment } from '../../services/host-bridge/hostBridge';
import {
getHostRuntime,
requestHostLogin,
requestHostPayment,
} from '../../services/host-bridge/hostBridge';
import {
resolveProfileRechargeProductPaymentChannel,
WECHAT_H5_PAYMENT_CHANNEL,
@@ -47,6 +54,7 @@ const WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS = [800, 1600] as const;
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
const WECHAT_PAY_RESULT_RECHECK_INTERVAL_MS = 250;
const WECHAT_PAY_RESULT_RECHECK_TIMEOUT_MS = 10000;
const WECHAT_RECHARGE_UNSUPPORTED_DEVICE_TEXT = '当前登录设备不支持充值';
const WECHAT_JSAPI_MISSING_IDENTITY_TEXT = '缺少微信';
export type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
@@ -87,6 +95,13 @@ function isWechatJsapiMissingIdentityError(error: unknown) {
);
}
function isWechatRechargeUnsupportedDeviceError(error: unknown) {
return (
error instanceof Error &&
error.message.includes(WECHAT_RECHARGE_UNSUPPORTED_DEVICE_TEXT)
);
}
type UsePlatformProfileCenterControllerArgs = {
activeTab: string;
isAuthenticated: boolean;
@@ -642,15 +657,16 @@ export function usePlatformProfileCenterController({
return;
}
if (paymentChannel === WECHAT_NATIVE_PAYMENT_CHANNEL) {
const codeUrl = response.wechatNativePayment?.codeUrl?.trim();
const expiresAt = response.wechatNativePayment?.expiresAt?.trim();
if (!codeUrl || !expiresAt) {
const wechatNativePayment = response.wechatNativePayment;
const codeUrl = wechatNativePayment?.codeUrl?.trim();
const expiresAt = wechatNativePayment?.expiresAt?.trim();
if (!wechatNativePayment || !codeUrl || !expiresAt) {
throw new Error('微信 Native 支付链接生成失败');
}
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
setRechargeCenter(response.center);
setNativeWechatPayment({
...response.wechatNativePayment,
...wechatNativePayment,
codeUrl,
expiresAt,
orderId: response.order.orderId,
@@ -667,6 +683,29 @@ export function usePlatformProfileCenterController({
.catch((error: unknown) => {
pendingWechatRechargeOrderIdRef.current = null;
setNativeWechatPayment(null);
if (
paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL &&
getHostRuntime().kind === 'wechat_mini_program' &&
isWechatRechargeUnsupportedDeviceError(error)
) {
clearStoredAccessToken();
setSubmittingRechargeProductId(null);
setRechargeError(null);
setRechargePaymentResult({
kind: 'pending',
title: '需要重新登录',
message: '正在打开微信小程序登录,请重新登录后再支付。',
});
void requestHostLogin().catch((miniProgramLoginError: unknown) => {
setRechargePaymentResult(null);
setRechargeError(
miniProgramLoginError instanceof Error
? miniProgramLoginError.message
: '请在微信小程序内重新登录后再支付',
);
});
return;
}
if (
paymentChannel === WECHAT_JSAPI_PAYMENT_CHANNEL &&
isWechatJsapiMissingIdentityError(error)
@@ -381,6 +381,7 @@ const {
const {
mockGetPublicAuthUserByCode,
mockGetPublicAuthUserById,
mockClearStoredAccessToken,
mockRefreshStoredAccessToken,
mockRequestJson,
mockUpdateAuthProfile,
@@ -403,6 +404,7 @@ const {
avatarUrl: null,
}),
),
mockClearStoredAccessToken: vi.fn(),
mockRefreshStoredAccessToken: vi.fn(async () => 'jwt-refreshed-token'),
mockRequestJson: vi.fn(async () => ({
read: {
@@ -418,6 +420,7 @@ const mockStartWechatBind = vi.hoisted(() => vi.fn(async () => undefined));
vi.mock('../../services/apiClient', () => ({
BACKGROUND_AUTH_REQUEST_OPTIONS: {},
clearStoredAccessToken: mockClearStoredAccessToken,
refreshStoredAccessToken: mockRefreshStoredAccessToken,
requestJson: mockRequestJson,
}));
@@ -1272,6 +1275,7 @@ function renderStatefulLoggedOutHomeView(
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
vi.clearAllMocks();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
@@ -1752,6 +1756,41 @@ test('profile recharge modal posts virtual payment params in mini program web-vi
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
});
test('profile recharge modal requests mini program login when stored token is not a mini program device', async () => {
const user = userEvent.setup();
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
window.wx = {
miniProgram: {
navigateTo: vi.fn((options: { success?: () => void }) => {
options.success?.();
}),
},
};
const requestHostLogin = vi
.spyOn(hostBridgeServices, 'requestHostLogin')
.mockResolvedValueOnce(true);
mockCreateRpgProfileRechargeOrder.mockRejectedValueOnce(
new Error('当前登录设备不支持充值,请在微信环境内登录后重试'),
);
renderProfileView();
await openRechargeModal(user);
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
await waitFor(() => {
expect(mockCreateRpgProfileRechargeOrder).toHaveBeenCalledWith(
'points_60',
'wechat_mp_virtual',
);
});
expect(mockClearStoredAccessToken).toHaveBeenCalledTimes(1);
expect(requestHostLogin).toHaveBeenCalledTimes(1);
expect(await screen.findByRole('dialog', { name: '需要重新登录' })).toBeTruthy();
expect(screen.getByText('正在打开微信小程序登录,请重新登录后再支付。')).toBeTruthy();
expect(mockRedirectToPaymentUrl).not.toHaveBeenCalled();
expect(mockRequestWechatJsapiPayment).not.toHaveBeenCalled();
});
test('profile recharge modal posts membership goods virtual payment params in mini program web-view', async () => {
const user = userEvent.setup();
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');