| New file |
| | |
| | | /** |
| | | * 微赞 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 |
| | | |
| | | /** |
| | | * 给指定 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 接口 |
| | | try { |
| | | plus.android.invoke(webviewInstance, '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 |
| | | const safeParams = normalizePayParams(params) |
| | | if (!safeParams.username) { |
| | | uni.showToast({ title: '支付参数缺失', icon: 'none' }) |
| | | return |
| | | } |
| | | 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) { |
| | | try { |
| | | const JsInterface = plus.android.implements('io.dcloud.js.WeizanPayJsInterface', { |
| | | [PAY_BRIDGE_NAME]: function (paramsJson) { |
| | | // JS 接口回调在原生子线程,必须切回 UI 线程才能拉起微信 SDK |
| | | plus.android.runOnUiThread(() => { |
| | | const params = normalizePayParams(parseJsonSafe(paramsJson)) |
| | | if (typeof onPay === 'function') { |
| | | try { onPay(params) } catch (e) {} |
| | | } |
| | | jumpToMiniProgramPay(params) |
| | | }) |
| | | } |
| | | }) |
| | | plus.android.invoke(webviewInstance, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY) |
| | | console.log('[weizanPay][Android] JSBridge 已注册') |
| | | } catch (e) { |
| | | console.error('[weizanPay][Android] 注册失败:', e) |
| | | } |
| | | } |
| | | |
| | | // ===== iOS 实现:注册 window.webkit.messageHandlers.jumpToMiniProgramPay ===== |
| | | function registerIOSPayBridge(webviewInstance, onPay) { |
| | | try { |
| | | // 实现 WKScriptMessageHandler 协议 |
| | | const MessageHandler = plus.ios.implements('NSObject<WKScriptMessageHandler>', { |
| | | 'userContentController:didReceiveScriptMessage:': function (controller, message) { |
| | | if (!message) { |
| | | return |
| | | } |
| | | // message.name === 'jumpToMiniProgramPay',message.body 是 H5 传的参数 |
| | | let raw = message |
| | | try { |
| | | const body = plus.ios.invoke(message, 'body') |
| | | if (body !== undefined && body !== null) { |
| | | raw = body |
| | | } |
| | | } catch (e) {} |
| | | const params = normalizePayParams(parseJsonSafe(raw)) |
| | | if (typeof onPay === 'function') { |
| | | try { onPay(params) } catch (e) {} |
| | | } |
| | | jumpToMiniProgramPay(params) |
| | | } |
| | | }) |
| | | // 取底层 WKWebView:plus.webview 实例 → valueForKey('webView') 或直接复用 |
| | | const wkWebView = safeIosInvoke(webviewInstance, 'valueForKey:', 'webView') || webviewInstance |
| | | const configuration = safeIosInvoke(wkWebView, 'configuration') |
| | | if (!configuration) { |
| | | console.error('[weizanPay][iOS] 未拿到 WKWebView configuration') |
| | | return |
| | | } |
| | | const userContentController = safeIosInvoke(configuration, 'userContentController') |
| | | if (!userContentController) { |
| | | console.error('[weizanPay][iOS] 未拿到 userContentController') |
| | | return |
| | | } |
| | | plus.ios.invoke(userContentController, 'addScriptMessageHandler:name:', MessageHandler, PAY_BRIDGE_NAME) |
| | | console.log('[weizanPay][iOS] JSBridge 已注册') |
| | | } catch (e) { |
| | | console.error('[weizanPay][iOS] 注册失败:', e) |
| | | } |
| | | } |
| | | |
| | | // ===== 辅助函数 ===== |
| | | |
| | | /** |
| | | * 把 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 |
| | | } |
| | | } |