fix mini program payment bridge

This commit is contained in:
2026-05-14 21:12:37 +08:00
parent bca439726d
commit cf3dcc6195
4 changed files with 132 additions and 7 deletions

View File

@@ -117,6 +117,7 @@
2. 弹窗顶部标题为 `账户充值`,右上角关闭。
3. 默认打开 `泥点充值`,可切换到 `会员卡充值`
4. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。
5. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和状态反馈。
6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。

View File

@@ -859,6 +859,10 @@ afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
window.wx = undefined;
document
.querySelectorAll('script[src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"]')
.forEach((script) => script.remove());
mockGetRpgProfileReferralInviteCenter.mockResolvedValue(
mockBuildReferralCenter(),
);
@@ -1044,6 +1048,84 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
expect(await screen.findByText('支付已提交')).toBeTruthy();
});
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');
window.wx = undefined;
const navigateTo = vi.fn((options: { url: string }) => {
const url = new URL(`https://mini.test${options.url}`);
const requestId = url.searchParams.get('requestId');
window.location.hash = `wx_pay_result=${requestId}:success`;
window.dispatchEvent(new HashChangeEvent('hashchange'));
});
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
order: {
orderId: 'order-wechat-sdk-1',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'pending' as const,
paymentChannel: 'wechat_mp',
paidAt: null as string | null,
providerTransactionId: null,
createdAt: '2026-04-25T10:00:00Z',
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: {
timeStamp: '1777110165',
nonceStr: 'nonce',
package: 'prepay_id=wx-prepay',
signType: 'RSA',
paySign: 'signature',
},
});
renderProfileView();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
await user.click(
within(shortcutRegion).getByRole('button', { name: //u }),
);
await user.click(await screen.findByRole('button', { name: /60/u }));
await waitFor(() => {
const script = document.querySelector<HTMLScriptElement>(
'script[src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"]',
);
expect(script).toBeTruthy();
window.wx = {
miniProgram: {
navigateTo,
},
};
script?.dispatchEvent(new Event('load'));
});
await waitFor(() => {
expect(navigateTo).toHaveBeenCalledWith({
url: expect.stringContaining('/pages/wechat-pay/index?'),
fail: expect.any(Function),
});
});
expect(await screen.findByText('支付已提交')).toBeTruthy();
});
test('profile daily task shortcut opens task center and claims reward', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();

View File

@@ -211,6 +211,7 @@ const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
const WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL = 'wechat_mp';
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
type RechargeTab = 'points' | 'membership';
@@ -2341,16 +2342,56 @@ function clearWechatPayResultHash() {
window.history.replaceState(null, '', nextUrl);
}
function requestWechatMiniProgramPayment(
function loadWechatJsSdk() {
if (typeof window === 'undefined') {
return Promise.reject(new Error('请在微信小程序内完成支付'));
}
if (window.wx?.miniProgram?.navigateTo) {
return Promise.resolve(window.wx);
}
return new Promise<NonNullable<Window['wx']>>((resolve, reject) => {
const existingScript = document.querySelector<HTMLScriptElement>(
`script[src="${WECHAT_JS_SDK_URL}"]`,
);
const complete = () => {
if (window.wx?.miniProgram?.navigateTo) {
resolve(window.wx);
} else {
reject(new Error('请在微信小程序内完成支付'));
}
};
if (existingScript) {
existingScript.addEventListener('load', complete, { once: true });
existingScript.addEventListener(
'error',
() => reject(new Error('请在微信小程序内完成支付')),
{ once: true },
);
complete();
return;
}
const script = document.createElement('script');
script.src = WECHAT_JS_SDK_URL;
script.async = true;
script.onload = complete;
script.onerror = () => reject(new Error('请在微信小程序内完成支付'));
document.head.appendChild(script);
});
}
async function requestWechatMiniProgramPayment(
payload: WechatMiniProgramPayParams | null | undefined,
orderId: string,
) {
const miniProgram = window.wx?.miniProgram;
if (
!payload ||
!miniProgram ||
typeof miniProgram.navigateTo !== 'function'
) {
if (!payload) {
return Promise.reject(new Error('请在微信小程序内完成支付'));
}
const wxBridge = await loadWechatJsSdk();
const miniProgram = wxBridge.miniProgram;
if (!miniProgram || typeof miniProgram.navigateTo !== 'function') {
return Promise.reject(new Error('请在微信小程序内完成支付'));
}
const navigateTo = miniProgram.navigateTo;

1
src/vite-env.d.ts vendored
View File

@@ -14,4 +14,5 @@ interface Window {
postMessage?: (message: unknown) => void;
};
};
WeixinJSBridge?: unknown;
}