/**
|
* 微赞 H5 唤起小程序支付桥接。
|
*
|
* H5 下单时通过原生 JSBridge 调用 APP 暴露的钩子方法:
|
* - Android: window.android.jumpToMiniProgramPay(JSON.stringify(params))
|
* - iOS: window.webkit.messageHandlers.jumpToMiniProgramPay.postMessage(JSON.stringify(params))
|
*
|
* params 字段(由微赞 H5 透传):
|
* - orderId: 订单号(用于日志/埋点,不参与拉起小程序)
|
* - path: 小程序支付页路径
|
* - username: 小程序原始 ID(gh_ 开头)
|
* - miniprogramType:小程序类型,固定 0
|
*
|
* 由 APP 用 plus.share.launchMiniProgram 拉起微信小程序完成支付(仅易宝支付)。
|
* 微信 appId: wx074c1f0cf5a0ecf4(见 manifest.json app-plus.distribute.sdkConfigs)。
|
*/
|
|
const PAY_BRIDGE_NAME = 'jumpToMiniProgramPay'
|
const PAY_BRIDGE_ANDROID_KEY = 'android' // H5 调 window.android.xxx
|
const LOG_TAG = '[weizanPay]'
|
|
/**
|
* 给指定 webview 注册微赞支付 JSBridge。
|
* 仅在 APP-PLUS 环境生效;非 APP 环境(H5/小程序)静默 no-op。
|
*
|
* @param {Object} webviewInstance plus.webview 实例(plus.webview.currentWebview() 拿到)
|
* @param {Function} [onPay] 收到 H5 回调时透传 (params),可在此做埋点/校验,不影响唤起
|
*/
|
export function registerWeizanPayBridge(webviewInstance, onPay) {
|
// #ifdef APP-PLUS
|
if (!webviewInstance) {
|
return
|
}
|
const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase()
|
if (platform === 'android') {
|
registerAndroidPayBridge(webviewInstance, onPay)
|
} else if (platform === 'ios') {
|
registerIOSPayBridge(webviewInstance, onPay)
|
}
|
// #endif
|
}
|
|
/**
|
* 反注册微赞支付 JSBridge,避免 webview 复用导致的内存泄漏与重复回调。
|
*
|
* @param {Object} webviewInstance plus.webview 实例
|
*/
|
export function unregisterWeizanPayBridge(webviewInstance) {
|
// #ifdef APP-PLUS
|
if (!webviewInstance) {
|
return
|
}
|
try {
|
const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase()
|
if (platform === 'android') {
|
// Android 移除注入的 JS 接口(同样要作用在原生 WebView 上)
|
try {
|
const nativeWebView = getAndroidNativeWebView(webviewInstance)
|
if (nativeWebView) {
|
plus.android.invoke(nativeWebView, 'removeJavascriptInterface', PAY_BRIDGE_ANDROID_KEY)
|
}
|
} catch (e) {}
|
}
|
// iOS:WKUserContentController 的 messageHandler 通常随 webview 销毁自动回收,
|
// 显式 removeScriptMessageHandlerForName 在 plus.ios 上不一定支持,这里不强行调用
|
} catch (e) {
|
console.error('[weizanPay] unregister fail:', e)
|
}
|
// #endif
|
}
|
|
/**
|
* 钩子方法:唤起微信小程序完成支付。
|
* orderId / path / username / miniprogramType 由微赞 H5 回调时传入。
|
*
|
* @param {Object} params { orderId, path, username, miniprogramType }
|
*/
|
export function jumpToMiniProgramPay(params) {
|
// #ifdef APP-PLUS
|
console.log('[weizanPay] >>> jumpToMiniProgramPay 被调用, 原始入参:', params, '类型:', typeof params)
|
const safeParams = normalizePayParams(params)
|
console.log('[weizanPay] 归一化后参数:', JSON.stringify(safeParams))
|
if (!safeParams.username) {
|
console.warn('[weizanPay] username 缺失,终止唤起。safeParams=', JSON.stringify(safeParams))
|
uni.showToast({ title: '支付参数缺失', icon: 'none' })
|
return
|
}
|
console.log('[weizanPay] 参数校验通过,开始调用 plus.share.getServices 唤起小程序')
|
plus.share.getServices((services) => {
|
const wx = services && services.find((s) => s && s.id === 'weixin')
|
if (!wx) {
|
uni.showToast({ title: '当前环境不支持微信', icon: 'none' })
|
return
|
}
|
if (!wx.nativeClient) {
|
uni.showToast({ title: '请先安装微信客户端', icon: 'none' })
|
return
|
}
|
wx.launchMiniProgram({
|
id: safeParams.username,
|
path: safeParams.path || '',
|
type: Number(safeParams.miniprogramType) || 0,
|
}, () => {
|
console.log('[weizanPay] 唤起小程序成功, orderId=', safeParams.orderId)
|
}, (err) => {
|
console.error('[weizanPay] 唤起失败', err)
|
const errMsg = String(err && (err.message || err.errMsg) || '')
|
uni.showToast({
|
title: errMsg.indexOf('badprogram') >= 0 ? '请检查小程序原始ID及关联配置' : '唤起支付失败',
|
icon: 'none',
|
})
|
})
|
}, (err) => {
|
console.error('[weizanPay] 微信服务不可用', err)
|
uni.showToast({ title: '微信服务不可用', icon: 'none' })
|
})
|
// #endif
|
}
|
|
// ===== 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 {
|
// java.io.Serializable 一定存在、无强制方法,足以承载动态代理
|
// H5 调 window.android.xxx 时由 Proxy 拦截转发到 JS 函数
|
const JsInterface = plus.android.implements('java.io.Serializable', {
|
[PAY_BRIDGE_NAME]: function (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(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) {}
|
}
|
jumpToMiniProgramPay(params)
|
})
|
}
|
})
|
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.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(LOG_TAG, '===== [iOS] didReceiveScriptMessage 被调用 =====')
|
if (!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(LOG_TAG, '[iOS] message.body=', safeStringify(body))
|
if (body !== undefined && body !== null) {
|
raw = body
|
}
|
} 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(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(LOG_TAG, '[iOS] 未拿到 userContentController,注册中止')
|
return
|
}
|
plus.ios.invoke(userContentController, 'addScriptMessageHandler:name:', MessageHandler, PAY_BRIDGE_NAME)
|
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.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)。
|
*
|
* @param {Object|string} input
|
* @returns {{ orderId: string, path: string, username: string, miniprogramType: number }}
|
*/
|
function normalizePayParams(input) {
|
let params = {}
|
if (input && typeof input === 'object') {
|
params = input
|
} else if (typeof input === 'string') {
|
params = parseJsonSafe(input)
|
}
|
return {
|
orderId: String(params.orderId || params.order_id || ''),
|
path: String(params.path || ''),
|
username: String(params.username || params.userName || params.user_name || ''),
|
miniprogramType: Number.isFinite(Number(params.miniprogramType)) ? Number(params.miniprogramType) : 0,
|
}
|
}
|
|
/**
|
* 安全解析 JSON 字符串,失败返回空对象。
|
*/
|
function parseJsonSafe(text) {
|
if (typeof text !== 'string') {
|
return text || {}
|
}
|
try {
|
return JSON.parse(text)
|
} catch (e) {
|
return {}
|
}
|
}
|
|
/**
|
* 安全调用 plus.ios.invoke,失败返回 null。
|
*/
|
function safeIosInvoke(obj, selector, ...args) {
|
try {
|
return plus.ios.invoke(obj, selector, ...args)
|
} catch (e) {
|
return null
|
}
|
}
|