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 <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 12:53:52 +08:00
parent 571959900b
commit 2a9493165b
4 changed files with 76 additions and 22 deletions
+22
View File
@@ -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 回跳后延迟再调 scanQRCodeiOS JSSDK 离线校验更慢) */
export function getPostAuthScanDelayMs(): number {
return isIosDevice() ? 1200 : 600;
+5
View File
@@ -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<ShopSessionPayload | null |
if (!isWechatEnv()) {
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
}
markScanWarmupAfterAuth();
const result = await weixinSdk.login();
if (result) return handleShopWechatLoginResult(result);
}
@@ -171,6 +173,8 @@ export async function bindShopWechatAfterSmsLogin(): Promise<void> {
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<WechatLoginResult | void> {
if (!isWechatEnv()) {
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
}
markScanWarmupAfterAuth();
return weixinSdk.login();
}
+8 -3
View File
@@ -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) {
+41 -19
View File
@@ -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<void> | null = null;
let configured = false;
let configuredUrl: string | null = null;
/**
* iOS 微信 WebViewJSSDK 签名校验用的是「本次 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<void> {
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<void> {
captureIosJssdkEntryUrl();
const pageUrl = options.url ?? getJssdkSignUrl();
const pageUrl = getJssdkSignUrl(options.url);
if (configuredUrl && signUrlChanged(configuredUrl, pageUrl)) {
resetJssdkConfig();
}