From 2a9493165bbeac81d4759eb4148005e89b945310 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Fri, 7 Aug 2026 12:53:52 +0800 Subject: [PATCH] fix(weixin-sdk): restore iOS JSSDK entry-URL signing for shop scan Phone re-login + OAuth on iOS left SPA on a different path than the WebView entry URL, so scanQRCode failed until a full reopen. Sign with the document entry URL again and warm up after auth. Co-authored-by: Cursor --- apps/h5-shop/src/lib/shop-scan-auth.ts | 22 ++++++++++ apps/h5-shop/src/lib/wechat-auth.ts | 5 +++ apps/h5-shop/src/pages/HomePage.tsx | 11 +++-- packages/weixin-sdk/src/jssdk.ts | 60 ++++++++++++++++++-------- 4 files changed, 76 insertions(+), 22 deletions(-) diff --git a/apps/h5-shop/src/lib/shop-scan-auth.ts b/apps/h5-shop/src/lib/shop-scan-auth.ts index 1dbc5da..ffbfd21 100644 --- a/apps/h5-shop/src/lib/shop-scan-auth.ts +++ b/apps/h5-shop/src/lib/shop-scan-auth.ts @@ -3,6 +3,9 @@ import { isIosDevice } from '@dukang/weixin-sdk'; /** 扫码前发起 OAuth 时标记,回跳后在首页续扫 */ export const SHOP_PENDING_SCAN_KEY = 'shop_pending_scan'; +/** 短信登录后绑定微信等 OAuth 回跳:下次手动扫码加长预热(不自动打开相机) */ +export const SHOP_SCAN_WARMUP_KEY = 'shop_scan_warmup'; + export function markPendingScanAfterAuth(): void { try { sessionStorage.setItem(SHOP_PENDING_SCAN_KEY, '1'); @@ -27,6 +30,25 @@ export function clearPendingScanAfterAuth(): void { } } +/** OAuth(含短信登录后绑微信)回跳后,标记下一次扫码需要加长预热 */ +export function markScanWarmupAfterAuth(): void { + try { + sessionStorage.setItem(SHOP_SCAN_WARMUP_KEY, '1'); + } catch { + /* ignore */ + } +} + +export function consumeScanWarmupAfterAuth(): boolean { + try { + if (sessionStorage.getItem(SHOP_SCAN_WARMUP_KEY) !== '1') return false; + sessionStorage.removeItem(SHOP_SCAN_WARMUP_KEY); + return true; + } catch { + return false; + } +} + /** OAuth 回跳后延迟再调 scanQRCode(iOS JSSDK 离线校验更慢) */ export function getPostAuthScanDelayMs(): number { return isIosDevice() ? 1200 : 600; diff --git a/apps/h5-shop/src/lib/wechat-auth.ts b/apps/h5-shop/src/lib/wechat-auth.ts index 6c4f320..d0acec9 100644 --- a/apps/h5-shop/src/lib/wechat-auth.ts +++ b/apps/h5-shop/src/lib/wechat-auth.ts @@ -2,6 +2,7 @@ import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-type import { isWxAuthorizeEnabled } from '@dukang/shared-types'; import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk'; import { isWechatEnv, weixinSdk } from './weixin'; +import { markScanWarmupAfterAuth } from './shop-scan-auth'; import { request, saveWechatSession, type ShopSessionPayload } from './api'; export type ShopAccountProfile = { @@ -162,6 +163,7 @@ export async function loginShopWithWechat(): Promise { const config = await fetchClientConfig(); if (!isWxAuthorizeEnabled(config)) return; if (!isWechatEnv()) return; + // OAuth 整页回跳后 iOS 需用入场 URL 重配 JSSDK;标记下次扫码加长预热 + markScanWarmupAfterAuth(); await weixinSdk.login(); } @@ -180,5 +184,6 @@ export async function authorizeShopWechat(): Promise { if (!isWechatEnv()) { throw new Error(WECHAT_INAPP_REQUIRED_MSG); } + markScanWarmupAfterAuth(); return weixinSdk.login(); } diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx index feeb3d2..54920a3 100644 --- a/apps/h5-shop/src/pages/HomePage.tsx +++ b/apps/h5-shop/src/pages/HomePage.tsx @@ -4,7 +4,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import PullToRefresh from '@dukang/shared-ui/PullToRefresh'; -import { isScanPermissionWarmupError } from '@dukang/weixin-sdk'; +import { isIosDevice, isScanPermissionWarmupError } from '@dukang/weixin-sdk'; import { useStoreSession } from '../contexts/StoreSessionContext'; @@ -28,6 +28,8 @@ import { clearPendingScanAfterAuth, + consumeScanWarmupAfterAuth, + getPostAuthScanDelayMs, markPendingScanAfterAuth, @@ -190,7 +192,8 @@ export default function HomePage() { try { - if (opts?.postAuthWarmup) { + // iOS / OAuth 回跳后须重新 wx.config(签名用入场 URL) + if (opts?.postAuthWarmup || isIosDevice()) { weixinSdk.reset(); @@ -308,7 +311,9 @@ export default function HomePage() { } - await runScan(); + const needWarmup = consumeScanWarmupAfterAuth(); + + await runScan(needWarmup ? { postAuthWarmup: true } : undefined); } catch (e) { diff --git a/packages/weixin-sdk/src/jssdk.ts b/packages/weixin-sdk/src/jssdk.ts index 17dd21c..2dce4b2 100644 --- a/packages/weixin-sdk/src/jssdk.ts +++ b/packages/weixin-sdk/src/jssdk.ts @@ -1,14 +1,22 @@ import type { WechatJssdkConfig } from '@dukang/shared-types'; -import { isWechatBrowser, isWechatDevTools } from './env'; +import { isIosDevice, isWechatBrowser, isWechatDevTools } from './env'; import { DEFAULT_JS_API_LIST } from './types'; const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'; -const SIGN_URL_CACHE_KEY = 'dukang_wx_sign_url_v2'; +/** 旧版错误地把 SPA 当前 URL 写入 session;清理以免干扰排查 */ +const LEGACY_SIGN_URL_CACHE_KEY = 'dukang_wx_sign_url_v2'; let scriptPromise: Promise | null = null; let configured = false; let configuredUrl: string | null = null; +/** + * iOS 微信 WebView:JSSDK 签名校验用的是「本次 document 加载」的入场 URL, + * SPA pushState/replaceState 后 location.href 会变,但微信仍按入场 URL 验签。 + * 使用模块级变量:整页刷新(含 OAuth 回跳)会重置;同页 SPA 路由保持不变。 + */ +let iosEntryUrl: string | null = null; + /** 清除 JSSDK 配置缓存(路由切换后须重新 wx.config) */ export function resetJssdkConfig(): void { configured = false; @@ -42,25 +50,36 @@ function signUrlChanged(prev: string | null, current: string): boolean { return prev !== current; } -/** 记录最近一次签名 URL;SPA 路由或 ?step= 变化时须重新 wx.config */ -export function captureIosJssdkEntryUrl(): void { - if (typeof window === 'undefined') return; - if (!isWechatBrowser() || isWechatDevTools()) return; - const current = normalizeJssdkPageUrl(window.location.href); - const existing = sessionStorage.getItem(SIGN_URL_CACHE_KEY); - if (!existing) { - sessionStorage.setItem(SIGN_URL_CACHE_KEY, current); - return; - } - if (signUrlChanged(existing, current)) { - sessionStorage.setItem(SIGN_URL_CACHE_KEY, current); - resetJssdkConfig(); +function clearLegacySignUrlCache(): void { + try { + sessionStorage.removeItem(LEGACY_SIGN_URL_CACHE_KEY); + } catch { + /* ignore */ } } -/** 获取参与 JSSDK 签名的 URL(始终为当前页完整 URL,含 query) */ +/** + * 捕获 iOS 微信入场 URL(每个 document 生命周期只记一次)。 + * Android / 非微信环境为 no-op。 + */ +export function captureIosJssdkEntryUrl(): void { + if (typeof window === 'undefined') return; + if (!isIosDevice() || !isWechatBrowser() || isWechatDevTools()) return; + clearLegacySignUrlCache(); + if (iosEntryUrl) return; + iosEntryUrl = normalizeJssdkPageUrl(window.location.href); +} + +/** 获取参与 JSSDK 签名的 URL;iOS 微信内固定为本次入场 URL */ export function getJssdkSignUrl(rawUrl?: string): string { - return normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : '')); + const current = normalizeJssdkPageUrl( + rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''), + ); + if (typeof window !== 'undefined' && isIosDevice() && isWechatBrowser() && !isWechatDevTools()) { + captureIosJssdkEntryUrl(); + if (iosEntryUrl) return iosEntryUrl; + } + return current; } function isJssdkDebugEnabled(): boolean { @@ -76,6 +95,8 @@ export function stripOAuthParamsFromLocation(): void { if (typeof window === 'undefined') return; const url = new URL(window.location.href); if (!url.searchParams.has('code') && !url.searchParams.has('state')) return; + // iOS:须先锁定入场 URL,再 replaceState;否则签名 URL 与微信内部 URL 不一致 + captureIosJssdkEntryUrl(); url.searchParams.delete('code'); url.searchParams.delete('state'); const query = url.searchParams.toString(); @@ -126,7 +147,8 @@ export async function initWechatJssdk(options: { }): Promise { captureIosJssdkEntryUrl(); const { apiBase, clientApp, getAccessToken } = options; - const pageUrl = options.url ?? getJssdkSignUrl(); + // iOS 忽略调用方传入的「当前页」URL,强制入场 URL,避免登录后 SPA 到首页签错名 + const pageUrl = getJssdkSignUrl(options.url); await loadScript(); if (!window.wx) throw new Error('微信 JSSDK 不可用'); @@ -164,7 +186,7 @@ export async function ensureJssdkReady(options: { jsApiList?: string[]; }): Promise { captureIosJssdkEntryUrl(); - const pageUrl = options.url ?? getJssdkSignUrl(); + const pageUrl = getJssdkSignUrl(options.url); if (configuredUrl && signUrlChanged(configuredUrl, pageUrl)) { resetJssdkConfig(); }