diff --git a/docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md b/docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md index 987b0df8..79af8322 100644 --- a/docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md +++ b/docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md @@ -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. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。 diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 45b84597..1617b4a2 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -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( + '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(); diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 764983a4..62b1b746 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -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>((resolve, reject) => { + const existingScript = document.querySelector( + `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; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index f9f39034..a419fc50 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -14,4 +14,5 @@ interface Window { postMessage?: (message: unknown) => void; }; }; + WeixinJSBridge?: unknown; }