fix(partner): 修正微信选图 JSSDK 签名 URL 含 query 参数
录店页 ?step= 变化时重新 wx.config,微信内上传仅走 chooseImage 接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,10 +39,6 @@ function formatWechatUploadError(e: unknown): string {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldFallbackToNativePicker(msg: string): boolean {
|
|
||||||
return /permission|denied|接口未就绪|signature|JSSDK|签名校验|invalid signature|config:fail|未授权/i.test(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function acceptsImages(accept: string) {
|
function acceptsImages(accept: string) {
|
||||||
return accept.includes('image');
|
return accept.includes('image');
|
||||||
}
|
}
|
||||||
@@ -93,6 +89,7 @@ export default function OssUploadField({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useWechatPicker || needsAuth) return;
|
if (!useWechatPicker || needsAuth) return;
|
||||||
|
weixinSdk.reset();
|
||||||
void weixinSdk.init().catch(() => {
|
void weixinSdk.init().catch(() => {
|
||||||
/* 点击上传时会再次初始化 */
|
/* 点击上传时会再次初始化 */
|
||||||
});
|
});
|
||||||
@@ -135,10 +132,6 @@ export default function OssUploadField({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNativeFilePicker() {
|
|
||||||
inputRef.current?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pickWechatImage() {
|
async function pickWechatImage() {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -150,21 +143,9 @@ export default function OssUploadField({
|
|||||||
count: 1,
|
count: 1,
|
||||||
sourceType: ['album', 'camera'],
|
sourceType: ['album', 'camera'],
|
||||||
});
|
});
|
||||||
if (!files) {
|
if (!files?.[0]) return;
|
||||||
openNativeFilePicker();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!files[0]) return;
|
|
||||||
await uploadSelectedFile(files[0]);
|
await uploadSelectedFile(files[0]);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : '无法打开相册';
|
|
||||||
if (/cancel/i.test(msg)) return;
|
|
||||||
if (shouldFallbackToNativePicker(msg)) {
|
|
||||||
openNativeFilePicker();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
@@ -186,16 +167,12 @@ export default function OssUploadField({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : '无法打开相册';
|
const msg = e instanceof Error ? e.message : '无法打开相册';
|
||||||
if (/cancel/i.test(msg)) return;
|
if (/cancel/i.test(msg)) return;
|
||||||
if (shouldFallbackToNativePicker(msg)) {
|
|
||||||
openNativeFilePicker();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showUploadError(formatWechatUploadError(e));
|
showUploadError(formatWechatUploadError(e));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openNativeFilePicker();
|
inputRef.current?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isImage = mediaType === 'IMAGE' && value;
|
const isImage = mediaType === 'IMAGE' && value;
|
||||||
@@ -225,6 +202,7 @@ export default function OssUploadField({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!useWechatPicker && (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -236,6 +214,7 @@ export default function OssUploadField({
|
|||||||
if (file) void uploadSelectedFile(file);
|
if (file) void uploadSelectedFile(file);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{isImage ? (
|
{isImage ? (
|
||||||
<button {...triggerProps} className={`partner-upload-preview${wide ? ' partner-upload-preview--wide' : ''}`}>
|
<button {...triggerProps} className={`partner-upload-preview${wide ? ' partner-upload-preview--wide' : ''}`}>
|
||||||
<img src={value} alt={label ?? '已上传'} />
|
<img src={value} alt={label ?? '已上传'} />
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ async function reportChooseImageEvent(
|
|||||||
sourceType: payload.sourceType,
|
sourceType: payload.sourceType,
|
||||||
stage: payload.stage,
|
stage: payload.stage,
|
||||||
pageUrl: typeof window !== 'undefined' ? normalizeJssdkPageUrl(window.location.href) : undefined,
|
pageUrl: typeof window !== 'undefined' ? normalizeJssdkPageUrl(window.location.href) : undefined,
|
||||||
|
signUrl: typeof window !== 'undefined' ? getJssdkSignUrl() : undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,64 +1,66 @@
|
|||||||
import type { WechatJssdkConfig } from '@dukang/shared-types';
|
import type { WechatJssdkConfig } from '@dukang/shared-types';
|
||||||
import { isIosDevice, isWechatBrowser, isWechatDevTools } from './env';
|
import { isWechatBrowser, isWechatDevTools } from './env';
|
||||||
import { DEFAULT_JS_API_LIST } from './types';
|
import { DEFAULT_JS_API_LIST } from './types';
|
||||||
|
|
||||||
const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||||
const IOS_ENTRY_URL_KEY = 'dukang_wx_ios_entry_url';
|
const SIGN_URL_CACHE_KEY = 'dukang_wx_sign_url_v2';
|
||||||
|
|
||||||
let scriptPromise: Promise<void> | null = null;
|
let scriptPromise: Promise<void> | null = null;
|
||||||
let configured = false;
|
let configured = false;
|
||||||
let configuredUrl: string | null = null;
|
let configuredUrl: string | null = null;
|
||||||
|
|
||||||
function pathnameOf(url: string): string {
|
|
||||||
try {
|
|
||||||
return new URL(url).pathname;
|
|
||||||
} catch {
|
|
||||||
return url.split('?')[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清除 JSSDK 配置缓存(路由切换后须重新 wx.config) */
|
/** 清除 JSSDK 配置缓存(路由切换后须重新 wx.config) */
|
||||||
export function resetJssdkConfig(): void {
|
export function resetJssdkConfig(): void {
|
||||||
configured = false;
|
configured = false;
|
||||||
configuredUrl = null;
|
configuredUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 参与 JSSDK 签名的页面 URL(去掉 hash、query;SPA 步骤参数不参与签名) */
|
/** 参与 JSSDK 签名的页面 URL:与微信文档一致,取 location.href 去掉 # 后的部分;剔除 OAuth 回调参数 */
|
||||||
export function normalizeJssdkPageUrl(rawUrl: string): string {
|
export function normalizeJssdkPageUrl(rawUrl: string): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL(rawUrl);
|
const url = new URL(rawUrl);
|
||||||
return `${url.origin}${url.pathname}`;
|
url.hash = '';
|
||||||
|
url.searchParams.delete('code');
|
||||||
|
url.searchParams.delete('state');
|
||||||
|
const query = url.searchParams.toString();
|
||||||
|
return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
|
||||||
} catch {
|
} catch {
|
||||||
return rawUrl.split('#')[0].split('?')[0];
|
const noHash = rawUrl.split('#')[0];
|
||||||
|
try {
|
||||||
|
const url = new URL(noHash, typeof window !== 'undefined' ? window.location.origin : 'https://localhost');
|
||||||
|
url.searchParams.delete('code');
|
||||||
|
url.searchParams.delete('state');
|
||||||
|
const query = url.searchParams.toString();
|
||||||
|
return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
|
||||||
|
} catch {
|
||||||
|
return noHash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** iOS 微信内 SPA:同 pathname 内复用入口 URL;跨路由时更新入口并失效旧签名 */
|
function signUrlChanged(prev: string | null, current: string): boolean {
|
||||||
|
return prev !== current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录最近一次签名 URL;SPA 路由或 ?step= 变化时须重新 wx.config */
|
||||||
export function captureIosJssdkEntryUrl(): void {
|
export function captureIosJssdkEntryUrl(): void {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
if (!isIosDevice() || !isWechatBrowser() || isWechatDevTools()) return;
|
if (!isWechatBrowser() || isWechatDevTools()) return;
|
||||||
const current = normalizeJssdkPageUrl(window.location.href);
|
const current = normalizeJssdkPageUrl(window.location.href);
|
||||||
const existing = sessionStorage.getItem(IOS_ENTRY_URL_KEY);
|
const existing = sessionStorage.getItem(SIGN_URL_CACHE_KEY);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
sessionStorage.setItem(IOS_ENTRY_URL_KEY, current);
|
sessionStorage.setItem(SIGN_URL_CACHE_KEY, current);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pathnameOf(existing) !== pathnameOf(current)) {
|
if (signUrlChanged(existing, current)) {
|
||||||
sessionStorage.setItem(IOS_ENTRY_URL_KEY, current);
|
sessionStorage.setItem(SIGN_URL_CACHE_KEY, current);
|
||||||
resetJssdkConfig();
|
resetJssdkConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取参与 JSSDK 签名的 URL */
|
/** 获取参与 JSSDK 签名的 URL(始终为当前页完整 URL,含 query) */
|
||||||
export function getJssdkSignUrl(rawUrl?: string): string {
|
export function getJssdkSignUrl(rawUrl?: string): string {
|
||||||
const current = normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''));
|
return normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''));
|
||||||
if (typeof window !== 'undefined' && isIosDevice() && isWechatBrowser() && !isWechatDevTools()) {
|
|
||||||
const entry = sessionStorage.getItem(IOS_ENTRY_URL_KEY);
|
|
||||||
if (entry && pathnameOf(entry) === pathnameOf(current)) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isJssdkDebugEnabled(): boolean {
|
function isJssdkDebugEnabled(): boolean {
|
||||||
@@ -163,7 +165,7 @@ export async function ensureJssdkReady(options: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
captureIosJssdkEntryUrl();
|
captureIosJssdkEntryUrl();
|
||||||
const pageUrl = options.url ?? getJssdkSignUrl();
|
const pageUrl = options.url ?? getJssdkSignUrl();
|
||||||
if (configuredUrl && pathnameOf(configuredUrl) !== pathnameOf(pageUrl)) {
|
if (configuredUrl && signUrlChanged(configuredUrl, pageUrl)) {
|
||||||
resetJssdkConfig();
|
resetJssdkConfig();
|
||||||
}
|
}
|
||||||
if (!isJssdkReady()) {
|
if (!isJssdkReady()) {
|
||||||
|
|||||||
@@ -86,9 +86,13 @@ export class WechatController {
|
|||||||
private normalizeJssdkUrl(rawUrl: string): string {
|
private normalizeJssdkUrl(rawUrl: string): string {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(rawUrl);
|
const parsed = new URL(rawUrl);
|
||||||
return `${parsed.origin}${parsed.pathname}`;
|
parsed.hash = '';
|
||||||
|
parsed.searchParams.delete('code');
|
||||||
|
parsed.searchParams.delete('state');
|
||||||
|
const query = parsed.searchParams.toString();
|
||||||
|
return `${parsed.origin}${parsed.pathname}${query ? `?${query}` : ''}`;
|
||||||
} catch {
|
} catch {
|
||||||
return rawUrl.split('#')[0].split('?')[0];
|
return rawUrl.split('#')[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user