| | |
| | | |
| | | const PAY_BRIDGE_NAME = 'jumpToMiniProgramPay' |
| | | const PAY_BRIDGE_ANDROID_KEY = 'android' // H5 调 window.android.xxx |
| | | const LOG_TAG = '[weizanPay]' |
| | | |
| | | /** |
| | | * 给指定 webview 注册微赞支付 JSBridge。 |
| | |
| | | try { |
| | | const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase() |
| | | if (platform === 'android') { |
| | | // Android 移除注入的 JS 接口 |
| | | // Android 移除注入的 JS 接口(同样要作用在原生 WebView 上) |
| | | try { |
| | | plus.android.invoke(webviewInstance, 'removeJavascriptInterface', PAY_BRIDGE_ANDROID_KEY) |
| | | const nativeWebView = getAndroidNativeWebView(webviewInstance) |
| | | if (nativeWebView) { |
| | | plus.android.invoke(nativeWebView, 'removeJavascriptInterface', PAY_BRIDGE_ANDROID_KEY) |
| | | } |
| | | } catch (e) {} |
| | | } |
| | | // iOS:WKUserContentController 的 messageHandler 通常随 webview 销毁自动回收, |
| | |
| | | |
| | | // ===== Android 实现:注入 window.android.jumpToMiniProgramPay ===== |
| | | function registerAndroidPayBridge(webviewInstance, onPay) { |
| | | console.log(LOG_TAG, '[Android] 开始注册 JSBridge,注入 key=', PAY_BRIDGE_ANDROID_KEY, '方法名=', PAY_BRIDGE_NAME) |
| | | console.log(LOG_TAG, '[Android] webviewInstance=', webviewInstance ? '[Object]' : 'null') |
| | | // plus.webview.create 返回的是 JS 包装对象,不能直接反射; |
| | | // 必须先取底层原生 WebView(5+ ADWebView,是 android.webkit.WebView 的子类)再反射, |
| | | // 否则 getSettings / addJavascriptInterface 都会失败。 |
| | | const nativeWebView = getAndroidNativeWebView(webviewInstance) |
| | | console.log(LOG_TAG, '[Android] nativeInstanceObject=', nativeWebView ? '[Object]' : 'null') |
| | | // 反射验证是否是真原生 WebView(应暴露 getSettings) |
| | | const settings = safeAndroidInvoke(nativeWebView, 'getSettings') |
| | | console.log(LOG_TAG, '[Android] getSettings 反射结果=', settings ? '[Object]' : 'null') |
| | | if (!settings) { |
| | | console.error(LOG_TAG, '[Android] 未取到原生 WebView 或不暴露 getSettings,注册中止') |
| | | console.error(LOG_TAG, '[Android] 请确认调用方传入的是 plus.webview.create 创建的子 webview,而不是 currentWebview()') |
| | | return |
| | | } |
| | | try { |
| | | const JsInterface = plus.android.implements('io.dcloud.js.WeizanPayJsInterface', { |
| | | // java.io.Serializable 一定存在、无强制方法,足以承载动态代理 |
| | | // H5 调 window.android.xxx 时由 Proxy 拦截转发到 JS 函数 |
| | | const JsInterface = plus.android.implements('java.io.Serializable', { |
| | | [PAY_BRIDGE_NAME]: function (paramsJson) { |
| | | // 注意:此日志运行在原生子线程 |
| | | console.log('[weizanPay][Android] JS 接口被触发, 收到原始字符串:', paramsJson) |
| | | console.log(LOG_TAG, '===== [Android] H5 调用 window.android.' + PAY_BRIDGE_NAME + ' =====') |
| | | console.log(LOG_TAG, '[Android] 原始 paramsJson=', safeStringify(paramsJson)) |
| | | // JS 接口回调在原生子线程,必须切回 UI 线程才能拉起微信 SDK |
| | | plus.android.runOnUiThread(() => { |
| | | console.log('[weizanPay][Android] 已切回 UI 线程, 开始解析参数') |
| | | console.log(LOG_TAG, '[Android] 已进入 UI 线程,开始解析参数') |
| | | const params = normalizePayParams(parseJsonSafe(paramsJson)) |
| | | console.log(LOG_TAG, '[Android] 解析后 params=', safeStringify(params)) |
| | | if (typeof onPay === 'function') { |
| | | try { onPay(params) } catch (e) {} |
| | | } |
| | |
| | | }) |
| | | } |
| | | }) |
| | | plus.android.invoke(webviewInstance, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY) |
| | | console.log('[weizanPay][Android] JSBridge 已注册') |
| | | console.log(LOG_TAG, '[Android] JsInterface 已创建:', JsInterface ? '[Object]' : 'null') |
| | | // 注入到原生 WebView(而非 JS 包装对象),H5 端才能拿到 window.android.xxx |
| | | plus.android.invoke(nativeWebView, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY) |
| | | console.log(LOG_TAG, '[Android] addJavascriptInterface 完成,window.' + PAY_BRIDGE_ANDROID_KEY + ' 已注入') |
| | | // 通过 evalJS 验证 H5 端 window.android 是否真的能访问到 |
| | | try { |
| | | if (typeof webviewInstance.evalJS === 'function') { |
| | | webviewInstance.evalJS(` |
| | | try { |
| | | if (typeof window.android === 'undefined' || window.android === null) { |
| | | console.log('[weizanPay][H5] window.android 不存在'); |
| | | } else { |
| | | console.log('[weizanPay][H5] window.android 存在, keys=' + Object.keys(window.android).join(',')); |
| | | console.log('[weizanPay][H5] window.android.${PAY_BRIDGE_NAME} 类型=' + typeof window.android.${PAY_BRIDGE_NAME}); |
| | | } |
| | | } catch (e) { |
| | | console.error('[weizanPay][Android] 注册失败:', e) |
| | | console.log('[weizanPay][H5] 检查 window.android 异常: ' + e.message); |
| | | } |
| | | `) |
| | | } |
| | | } catch (e) { |
| | | console.warn(LOG_TAG, '[Android] evalJS 验证失败:', e && e.message) |
| | | } |
| | | } catch (e) { |
| | | console.error(LOG_TAG, '[Android] 注册 JSBridge 失败:', e) |
| | | console.error(LOG_TAG, '[Android] 异常 message=', e && e.message) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 取 plus.webview 实例底层的原生 android.webkit.WebView。 |
| | | * plus.webview.create 返回的是 JS 包装对象,直接反射 getSettings/addJavascriptInterface 会失败, |
| | | * 必须先取到原生 WebView。优先用官方 nativeInstanceObject(),失败再退回 obtainWebview()。 |
| | | */ |
| | | function getAndroidNativeWebView(webview) { |
| | | if (!webview) { |
| | | return null |
| | | } |
| | | // 1. 官方 API:WebviewObject.nativeInstanceObject() 返回原生 WebView |
| | | if (typeof webview.nativeInstanceObject === 'function') { |
| | | try { |
| | | const nativeObj = webview.nativeInstanceObject() |
| | | if (nativeObj) { |
| | | return nativeObj |
| | | } |
| | | } catch (e) { |
| | | console.warn(LOG_TAG, '[Android] nativeInstanceObject 调用失败:', e && e.message) |
| | | } |
| | | } |
| | | // 2. 兼容:部分运行时通过 obtainWebview 取原生 WebView |
| | | if (typeof webview.obtainWebview === 'function') { |
| | | try { |
| | | const obtained = webview.obtainWebview() |
| | | if (obtained) { |
| | | return obtained |
| | | } |
| | | } catch (e) {} |
| | | } |
| | | return null |
| | | } |
| | | |
| | | /** |
| | | * 安全调用 plus.android.invoke,失败返回 null。 |
| | | */ |
| | | function safeAndroidInvoke(obj, methodName, ...args) { |
| | | try { |
| | | return plus.android.invoke(obj, methodName, ...args) |
| | | } catch (e) { |
| | | return null |
| | | } |
| | | } |
| | | |
| | | // ===== iOS 实现:注册 window.webkit.messageHandlers.jumpToMiniProgramPay ===== |
| | | function registerIOSPayBridge(webviewInstance, onPay) { |
| | | console.log(LOG_TAG, '[iOS] 开始注册 JSBridge,handler name=', PAY_BRIDGE_NAME) |
| | | console.log(LOG_TAG, '[iOS] webviewInstance=', webviewInstance ? '[Object]' : 'null') |
| | | try { |
| | | // 实现 WKScriptMessageHandler 协议 |
| | | const MessageHandler = plus.ios.implements('NSObject<WKScriptMessageHandler>', { |
| | | 'userContentController:didReceiveScriptMessage:': function (controller, message) { |
| | | console.log('[weizanPay][iOS] messageHandler 被触发, message=', message) |
| | | console.log(LOG_TAG, '===== [iOS] didReceiveScriptMessage 被调用 =====') |
| | | if (!message) { |
| | | console.warn('[weizanPay][iOS] message 为空,终止') |
| | | console.warn(LOG_TAG, '[iOS] message 为空,跳过') |
| | | return |
| | | } |
| | | // message.name === 'jumpToMiniProgramPay',message.body 是 H5 传的参数 |
| | | let raw = message |
| | | try { |
| | | const body = plus.ios.invoke(message, 'body') |
| | | console.log('[weizanPay][iOS] message.body=', body, '类型:', typeof body) |
| | | console.log(LOG_TAG, '[iOS] message.body=', safeStringify(body)) |
| | | if (body !== undefined && body !== null) { |
| | | raw = body |
| | | } |
| | | } catch (e) { |
| | | console.error('[weizanPay][iOS] 取 message.body 失败:', e) |
| | | } |
| | | } catch (e) {} |
| | | console.log(LOG_TAG, '[iOS] 原始 raw=', safeStringify(raw)) |
| | | const params = normalizePayParams(parseJsonSafe(raw)) |
| | | console.log(LOG_TAG, '[iOS] 解析后 params=', safeStringify(params)) |
| | | if (typeof onPay === 'function') { |
| | | try { onPay(params) } catch (e) {} |
| | | } |
| | | jumpToMiniProgramPay(params) |
| | | } |
| | | }) |
| | | console.log(LOG_TAG, '[iOS] MessageHandler 已创建:', MessageHandler ? '[Object]' : 'null') |
| | | // 取底层 WKWebView:plus.webview 实例 → valueForKey('webView') 或直接复用 |
| | | console.log(LOG_TAG, '[iOS] 尝试 valueForKey:webView') |
| | | const wkWebView = safeIosInvoke(webviewInstance, 'valueForKey:', 'webView') || webviewInstance |
| | | console.log(LOG_TAG, '[iOS] wkWebView=', wkWebView ? '[Object]' : 'null') |
| | | const configuration = safeIosInvoke(wkWebView, 'configuration') |
| | | console.log(LOG_TAG, '[iOS] configuration=', configuration ? '[Object]' : 'null') |
| | | if (!configuration) { |
| | | console.error('[weizanPay][iOS] 未拿到 WKWebView configuration') |
| | | console.error(LOG_TAG, '[iOS] 未拿到 WKWebView configuration,注册中止') |
| | | console.error(LOG_TAG, '[iOS] 请确认传入的是 plus.webview.create 创建的子 webview') |
| | | return |
| | | } |
| | | const userContentController = safeIosInvoke(configuration, 'userContentController') |
| | | console.log(LOG_TAG, '[iOS] userContentController=', userContentController ? '[Object]' : 'null') |
| | | if (!userContentController) { |
| | | console.error('[weizanPay][iOS] 未拿到 userContentController') |
| | | console.error(LOG_TAG, '[iOS] 未拿到 userContentController,注册中止') |
| | | return |
| | | } |
| | | plus.ios.invoke(userContentController, 'addScriptMessageHandler:name:', MessageHandler, PAY_BRIDGE_NAME) |
| | | console.log('[weizanPay][iOS] JSBridge 已注册') |
| | | console.log(LOG_TAG, '[iOS] addScriptMessageHandler 完成,window.webkit.messageHandlers.' + PAY_BRIDGE_NAME + ' 已注册') |
| | | // 验证 H5 端是否能访问到 messageHandlers |
| | | try { |
| | | if (typeof webviewInstance.evalJS === 'function') { |
| | | webviewInstance.evalJS(` |
| | | try { |
| | | if (!window.webkit || !window.webkit.messageHandlers) { |
| | | console.log('[weizanPay][H5] window.webkit.messageHandlers 不存在'); |
| | | } else { |
| | | var names = Object.keys(window.webkit.messageHandlers); |
| | | console.log('[weizanPay][H5] messageHandlers names=' + names.join(',')); |
| | | var handler = window.webkit.messageHandlers.${PAY_BRIDGE_NAME}; |
| | | console.log('[weizanPay][H5] ${PAY_BRIDGE_NAME} handler 类型=' + typeof handler); |
| | | } |
| | | } catch (e) { |
| | | console.error('[weizanPay][iOS] 注册失败:', e) |
| | | console.log('[weizanPay][H5] 检查 messageHandlers 异常: ' + e.message); |
| | | } |
| | | `) |
| | | } |
| | | } catch (e) { |
| | | console.warn(LOG_TAG, '[iOS] evalJS 验证失败:', e && e.message) |
| | | } |
| | | } catch (e) { |
| | | console.error(LOG_TAG, '[iOS] 注册 JSBridge 失败:', e) |
| | | console.error(LOG_TAG, '[iOS] 异常 message=', e && e.message) |
| | | } |
| | | } |
| | | |
| | | // ===== 辅助函数 ===== |
| | | |
| | | /** |
| | | * 安全序列化任意值,用于日志打印。原生对象、循环引用、undefined 都不会抛错。 |
| | | */ |
| | | function safeStringify(value) { |
| | | try { |
| | | if (value === undefined) { |
| | | return 'undefined' |
| | | } |
| | | if (value === null) { |
| | | return 'null' |
| | | } |
| | | if (typeof value === 'string') { |
| | | return value |
| | | } |
| | | if (typeof value === 'number' || typeof value === 'boolean') { |
| | | return String(value) |
| | | } |
| | | if (typeof value === 'function') { |
| | | return '[Function]' |
| | | } |
| | | return JSON.stringify(value) |
| | | } catch (e) { |
| | | try { |
| | | return String(value) |
| | | } catch (e2) { |
| | | return '[Unserializable]' |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 把 H5 传过来的 params 归一化为标准结构。 |
| | | * 同时兼容下划线命名(order_id)与驼峰命名(orderId)。 |
| | | * |