Compare commits
33 Commits
23c91267a2
...
pay
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
689eec085f | ||
|
|
bee953617d | ||
|
|
23b97548cf | ||
|
|
378c7e4ba5 | ||
|
|
c2476b9f85 | ||
|
|
d574f08faf | ||
|
|
0b1b145b9c | ||
|
|
fb3a4b4e06 | ||
|
|
13b461050b | ||
|
|
b12497443c | ||
|
|
b438364656 | ||
|
|
229c8cb9e4 | ||
|
|
cdec1838ac | ||
|
|
c08cdaee68 | ||
|
|
f52b9f3518 | ||
|
|
6663fd6ab4 | ||
|
|
c56c17589a | ||
|
|
bc025f7f74 | ||
|
|
4afa5ec8cb | ||
|
|
69b0c4e27c | ||
|
|
6e91cc0ef4 | ||
|
|
1c02c72b3b | ||
|
|
548814fe38 | ||
|
|
ede67b61b4 | ||
|
|
1966dbbd51 | ||
|
|
45686d28fc | ||
|
|
09b80d6b78 | ||
|
|
96006164b5 | ||
|
|
36f1cbbf7e | ||
|
|
ed1ee040cc | ||
|
|
4e41a85e04 | ||
|
|
c0fba11103 | ||
|
|
dab2aa2d53 |
42
README.md
42
README.md
@@ -61,9 +61,6 @@ IScan.config({
|
|||||||
// 自动选择可用扫码方式:桥接 -> 微信 -> Web 摄像头 -> 图片识别
|
// 自动选择可用扫码方式:桥接 -> 微信 -> Web 摄像头 -> 图片识别
|
||||||
IScan.startScan();
|
IScan.startScan();
|
||||||
|
|
||||||
// 仅打开 Web 视频扫码
|
|
||||||
IScan.scanVideo();
|
|
||||||
|
|
||||||
// 仅选择图片识别
|
// 仅选择图片识别
|
||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
|
|
||||||
@@ -83,11 +80,17 @@ interface ScanConfigOptions {
|
|||||||
webScanEnabled?: boolean,
|
webScanEnabled?: boolean,
|
||||||
webScanCanvasEnabled?: boolean;
|
webScanCanvasEnabled?: boolean;
|
||||||
webScanCanvasStyle?: string;
|
webScanCanvasStyle?: string;
|
||||||
|
webScanCloseButtonStyle?: string;
|
||||||
|
webScanCanvasClass?: string;
|
||||||
|
webScanCloseButtonClass?: string;
|
||||||
webScanType?: ("qrCode" | "barCode")[];
|
webScanType?: ("qrCode" | "barCode")[];
|
||||||
webScanVideoMirror?: boolean;
|
webScanVideoMirror?: boolean;
|
||||||
webScanVideoMirrorVertical?: boolean;
|
webScanVideoMirrorVertical?: boolean;
|
||||||
webScanBeepAudio?: string;
|
webScanImageFallbackOnVideoError?: boolean;
|
||||||
webScanBeepEnabled?: boolean;
|
webScanVideoAccessTimeout?: number;
|
||||||
|
webScanVideoReadyTimeout?: number;
|
||||||
|
scanBeepAudio?: string;
|
||||||
|
scanBeepEnabled?: boolean;
|
||||||
initWechatJssdk?: {
|
initWechatJssdk?: {
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
sdkConfig?: {
|
sdkConfig?: {
|
||||||
@@ -110,12 +113,23 @@ interface ScanConfigOptions {
|
|||||||
| `bridgeName` | 挂载在 `window` 上的桥接对象名称 | `__bridge_client__` |
|
| `bridgeName` | 挂载在 `window` 上的桥接对象名称 | `__bridge_client__` |
|
||||||
| `webScanEnabled` | 是否支持 WebScan 扫码 | `true` |
|
| `webScanEnabled` | 是否支持 WebScan 扫码 | `true` |
|
||||||
| `webScanCanvasEnabled` | 是否显示 WebScan 扫码 canvas;关闭后仍会用隐藏 canvas 识别 | `true` |
|
| `webScanCanvasEnabled` | 是否显示 WebScan 扫码 canvas;关闭后仍会用隐藏 canvas 识别 | `true` |
|
||||||
| `webScanCanvasStyle` | WebScan 扫码 canvas 样式 | `position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;` |
|
| `webScanCanvasStyle` | WebScan 扫码 canvas 内联样式 | `position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;` |
|
||||||
|
| `webScanCloseButtonStyle` | WebScan 关闭按钮内联样式(在默认位置上追加);按钮内容为 SVG,`path` 使用 `currentColor`,可通过按钮的 `color` 改图标颜色 | 圆形半透明底、白图标,定位在 canvas 右上角 |
|
||||||
|
| `webScanCanvasClass` | WebScan canvas 根元素 `class`,便于用外部样式表配合 `webScanCanvasStyle` 定制 | 无 |
|
||||||
|
| `webScanCloseButtonClass` | WebScan 关闭按钮 `class`,便于用外部样式表配合 `webScanCloseButtonStyle` 定制 | 无 |
|
||||||
| `webScanType` | WebScan 扫码类型 | `["qrCode", "barCode"]` |
|
| `webScanType` | WebScan 扫码类型 | `["qrCode", "barCode"]` |
|
||||||
| `webScanVideoMirror` | WebScan 视频是否水平镜像;不配置时自动判断:前置/PC 镜像,后置不镜像 | 自动 |
|
| `webScanVideoMirror` | WebScan 视频是否水平镜像;不配置时自动判断:前置/PC 镜像,后置不镜像 | 自动 |
|
||||||
| `webScanVideoMirrorVertical` | WebScan 视频是否垂直镜像 | `false` |
|
| `webScanVideoMirrorVertical` | WebScan 视频是否垂直镜像 | `false` |
|
||||||
| `webScanBeepAudio` | WebScan 扫码成功提示音地址 | 内置提示音 |
|
| `webScanImageFallbackOnVideoError` | 摄像头不可用或打开失败时,是否自动弹出拍照/选图(适用于部分安卓内置浏览器) | `true` |
|
||||||
| `webScanBeepEnabled` | WebScan 扫码成功是否播放提示音 | `true` |
|
| `webScanVideoAccessTimeout` | 打开摄像头超时(毫秒),超时后走图片回退 | `10000` |
|
||||||
|
| `webScanVideoReadyTimeout` | 摄像头已开但无画面超时(毫秒),超时后走图片回退 | `8000` |
|
||||||
|
| `webScanCameraPermissionDialogEnabled` | `startScan` 走 Web 摄像头前是否先展示权限说明弹窗 | `true` |
|
||||||
|
| `webScanCameraPermissionTitle` | 权限说明弹窗标题 | `需要使用摄像头` |
|
||||||
|
| `webScanCameraPermissionMessage` | 权限说明弹窗正文 | 见类型定义默认值 |
|
||||||
|
| `webScanCameraPermissionConfirmText` | 确认按钮文案 | `继续` |
|
||||||
|
| `webScanCameraPermissionCancelText` | 取消按钮文案 | `取消` |
|
||||||
|
| `scanBeepAudio` | 扫码成功提示音地址(任意模式匹配成功时播放) | 内置提示音 |
|
||||||
|
| `scanBeepEnabled` | 扫码成功是否播放提示音 | `true` |
|
||||||
| `initWechatJssdk` | 微信 JSSDK 初始化配置,仅微信环境生效 | 无 |
|
| `initWechatJssdk` | 微信 JSSDK 初始化配置,仅微信环境生效 | 无 |
|
||||||
|
|
||||||
`initWechatJssdk` 子配置:
|
`initWechatJssdk` 子配置:
|
||||||
@@ -212,7 +226,9 @@ IScan.config({
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
IScan.config({
|
IScan.config({
|
||||||
webScanCanvasEnabled: true
|
webScanCanvasEnabled: true,
|
||||||
|
webScanCanvasClass: "my-webscan-canvas",
|
||||||
|
webScanCloseButtonClass: "my-webscan-close"
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -278,14 +294,6 @@ console.log(IScan.getStatus());
|
|||||||
IScan.startScan();
|
IScan.startScan();
|
||||||
```
|
```
|
||||||
|
|
||||||
### `scanVideo(): void`
|
|
||||||
|
|
||||||
直接开启 Web 摄像头扫码。扫码结果通过 `onScanListener` 回调。
|
|
||||||
|
|
||||||
```js
|
|
||||||
IScan.scanVideo();
|
|
||||||
```
|
|
||||||
|
|
||||||
### `scanImage(): void`
|
### `scanImage(): void`
|
||||||
|
|
||||||
直接选择图片进行识别。识别结果通过 `onScanListener` 回调。
|
直接选择图片进行识别。识别结果通过 `onScanListener` 回调。
|
||||||
|
|||||||
16
demo.html
16
demo.html
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
|
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
|
||||||
<title>IScan</title>
|
<title>IScan 使用示例</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var vconsole = new VConsole();
|
var vconsole = new VConsole();
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<main class="page">
|
<main class="page">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<h1>IScan 通用扫码 SDK</h1>
|
<h1>IScan 通用扫码SDK</h1>
|
||||||
<p>统一接入桥接扫码、微信小程序、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p>
|
<p>统一接入桥接扫码、微信小程序、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -207,7 +207,6 @@
|
|||||||
<p>点击开始后,会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p>
|
<p>点击开始后,会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button onclick="startScan()" class="btn">开始扫码</button>
|
<button onclick="startScan()" class="btn">开始扫码</button>
|
||||||
<button onclick="scanVideo()" class="btn secondary">开启视频扫码</button>
|
|
||||||
<button onclick="scanImage()" class="btn secondary">选择图片识别</button>
|
<button onclick="scanImage()" class="btn secondary">选择图片识别</button>
|
||||||
<button onclick="stopScan()" class="btn secondary">停止扫码</button>
|
<button onclick="stopScan()" class="btn secondary">停止扫码</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +228,8 @@
|
|||||||
<pre>IScan.config({
|
<pre>IScan.config({
|
||||||
webScanEnabled: true,
|
webScanEnabled: true,
|
||||||
webScanCanvasEnabled: true,
|
webScanCanvasEnabled: true,
|
||||||
webScanBeepEnabled: true,
|
webScanCloseButtonStyle: "background: rgba(27, 99, 244, 0.88);",
|
||||||
|
scanBeepEnabled: true,
|
||||||
initWechatJssdk: {
|
initWechatJssdk: {
|
||||||
apiUrl: "https://your-domain.com/wechat/jssdk-config"
|
apiUrl: "https://your-domain.com/wechat/jssdk-config"
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,6 @@
|
|||||||
|
|
||||||
IScan.startScan();
|
IScan.startScan();
|
||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
IScan.scanVideo();
|
|
||||||
IScan.stopScan();</pre>
|
IScan.stopScan();</pre>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -319,7 +318,8 @@ IScan.stopScan();</pre>
|
|||||||
var url = "https://vet.iqudoo.com/api?action=api.biz.wechat.JSSDKConfig";
|
var url = "https://vet.iqudoo.com/api?action=api.biz.wechat.JSSDKConfig";
|
||||||
initSDK({
|
initSDK({
|
||||||
webScanCanvasEnabled: true,
|
webScanCanvasEnabled: true,
|
||||||
webScanBeepEnabled: true,
|
webScanCloseButtonStyle: "background: rgba(27, 99, 244, 0.88);",
|
||||||
|
scanBeepEnabled: true,
|
||||||
initWechatJssdk: { apiUrl: url }
|
initWechatJssdk: { apiUrl: url }
|
||||||
}, function () {
|
}, function () {
|
||||||
setStatus(IScan.getStatus());
|
setStatus(IScan.getStatus());
|
||||||
@@ -347,10 +347,6 @@ IScan.stopScan();</pre>
|
|||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanVideo() {
|
|
||||||
IScan.scanVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
ready();
|
ready();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
399
dist/index.d.ts
vendored
399
dist/index.d.ts
vendored
@@ -6,6 +6,26 @@ interface ScanConfigOptions {
|
|||||||
* 扫码重启延迟,单位:毫秒,默认500ms
|
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||||
*/
|
*/
|
||||||
scanRestartDelay?: number,
|
scanRestartDelay?: number,
|
||||||
|
/**
|
||||||
|
* iframe 场景下是否将 API 调用转发到父页面同名 SDK(postMessage)。
|
||||||
|
* - `'auto'`(默认):处于子 frame 时 `startScan` 等由父页执行;`onScanListener` 仅注册在 iframe 内,识别结果由父页 `postMessage` 回传
|
||||||
|
* - `true` / `'on'` / `'parent'`:存在父 window 时强制转发
|
||||||
|
* - `false` / `'off'` / `'local'`:始终在本页执行(子页自己要跑扫码时用)
|
||||||
|
*/
|
||||||
|
embedProxyMode?: 'auto' | boolean | 'on' | 'off' | 'local' | 'parent',
|
||||||
|
/**
|
||||||
|
* 请求微信 JS-SDK 签名时使用的页面 URL(不含 hash)。
|
||||||
|
* 跨域 iframe 无法读取父页地址时需手动设为当前微信内打开的页面链接。
|
||||||
|
*/
|
||||||
|
wxJssdkSignatureUrl?: string,
|
||||||
|
/**
|
||||||
|
* 页面 URL 变化后是否自动重新初始化微信 JSSDK(默认 true,避免扫一扫失效)
|
||||||
|
*/
|
||||||
|
wxJssdkAutoReinitOnUrlChange?: boolean,
|
||||||
|
/**
|
||||||
|
* URL 轮询检测间隔(毫秒),用于捕获未走 history API 的 SPA 路由;默认 1000,设为 0 关闭
|
||||||
|
*/
|
||||||
|
wxJssdkUrlPollInterval?: number,
|
||||||
/**
|
/**
|
||||||
* 桥接是否启用,默认启用
|
* 桥接是否启用,默认启用
|
||||||
*/
|
*/
|
||||||
@@ -33,6 +53,50 @@ interface ScanConfigOptions {
|
|||||||
* 2. 结束扫码的方法名称为:stopScan
|
* 2. 结束扫码的方法名称为:stopScan
|
||||||
*/
|
*/
|
||||||
bridgeName?: string,
|
bridgeName?: string,
|
||||||
|
/**
|
||||||
|
* 桥接扫码超时(毫秒),超时后回退 Web/图片识别,默认 5000
|
||||||
|
*/
|
||||||
|
bridgeScanTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 是否允许 H5 摄像头扫码:true 强制开启(仍需有媒体 API),false 强制关闭
|
||||||
|
*/
|
||||||
|
webScanCameraEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* startScan 走 Web 摄像头前是否展示权限说明弹窗,默认 true
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionDialogEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗标题
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionTitle?: string,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗正文
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionMessage?: string,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗确认按钮文案
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionConfirmText?: string,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗取消按钮文案
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionCancelText?: string,
|
||||||
|
/**
|
||||||
|
* 选图后延迟读取 file 对象(毫秒),微信/安卓 WebView 默认 100
|
||||||
|
*/
|
||||||
|
webScanFileReadDelay?: number,
|
||||||
|
/**
|
||||||
|
* 强制使用 WASM ponyfill 识别(微信/部分 WebView 建议开启,默认微信内自动开启)
|
||||||
|
*/
|
||||||
|
webScanPreferPonyfill?: boolean,
|
||||||
|
/**
|
||||||
|
* WASM 文件完整 URL;未配置时相对 SDK 脚本地址解析 lib/reader.wasm
|
||||||
|
*/
|
||||||
|
webScanWasmUrl?: string,
|
||||||
|
/**
|
||||||
|
* WASM 基准路径(SDK 脚本 URL 或目录);用于 async/defer 加载时修正 reader.wasm 路径
|
||||||
|
*/
|
||||||
|
webScanWasmBaseUrl?: string,
|
||||||
/**
|
/**
|
||||||
* webScan是否启用,默认启用
|
* webScan是否启用,默认启用
|
||||||
*/
|
*/
|
||||||
@@ -42,9 +106,29 @@ interface ScanConfigOptions {
|
|||||||
*/
|
*/
|
||||||
webScanCanvasEnabled?: boolean,
|
webScanCanvasEnabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* 网页扫码canvas样式,默认:position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;
|
* 网页扫码canvas样式,默认:PC 为左上角固定 300×300;移动端为 min(视口宽, 视口高) 的正方形,固定在左上角;z-index: 9999
|
||||||
*/
|
*/
|
||||||
webScanCanvasStyle?: string,
|
webScanCanvasStyle?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码canvas关闭按钮样式,默认显示在canvas右上角
|
||||||
|
*/
|
||||||
|
webScanCloseButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码 canvas 根元素的 class,便于配合外部样式表定制布局与外观
|
||||||
|
*/
|
||||||
|
webScanCanvasClass?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码关闭按钮的 class
|
||||||
|
*/
|
||||||
|
webScanCloseButtonClass?: string,
|
||||||
|
/**
|
||||||
|
* Canvas 开启且支持图片识别时,关闭按钮下方的「选图」按钮样式(fixed 定位基准由 SDK 计算,可与关闭按钮一致覆盖)
|
||||||
|
*/
|
||||||
|
webScanPickImageButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码选图按钮的 class
|
||||||
|
*/
|
||||||
|
webScanPickImageButtonClass?: string,
|
||||||
/**
|
/**
|
||||||
* 网页扫码类型,默认支持二维码和条码
|
* 网页扫码类型,默认支持二维码和条码
|
||||||
*/
|
*/
|
||||||
@@ -58,13 +142,160 @@ interface ScanConfigOptions {
|
|||||||
*/
|
*/
|
||||||
webScanVideoMirrorVertical?: boolean,
|
webScanVideoMirrorVertical?: boolean,
|
||||||
/**
|
/**
|
||||||
* 网页扫码成功提示音地址,默认使用内置提示音
|
* 摄像头不可用(如部分安卓内置浏览器禁用 getUserMedia)时是否自动回退为拍照/选图识别,默认启用
|
||||||
*/
|
*/
|
||||||
webScanBeepAudio?: string,
|
webScanImageFallbackOnVideoError?: boolean,
|
||||||
/**
|
/**
|
||||||
* 网页扫码成功提示音是否启用,默认启用
|
* 打开摄像头超时时间(毫秒),超时后触发图片回退,默认 10000
|
||||||
*/
|
*/
|
||||||
webScanBeepEnabled?: boolean,
|
webScanVideoAccessTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 摄像头已打开但长时间无画面时触发图片回退(毫秒),默认 8000
|
||||||
|
*/
|
||||||
|
webScanVideoReadyTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 图片/回退识别时 detect 超时(毫秒),默认 15000
|
||||||
|
*/
|
||||||
|
webScanDetectTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 图片识别是否优先使用 ZXing ponyfill(原生 detect 在部分 WebView 可能卡住),默认 true
|
||||||
|
*/
|
||||||
|
webScanImagePreferPonyfill?: boolean,
|
||||||
|
/**
|
||||||
|
* 移动端图片回退是否使用 capture 拍照(false 为相册选图,兼容性更好),默认 false
|
||||||
|
*/
|
||||||
|
webScanImagePreferCapture?: boolean,
|
||||||
|
/**
|
||||||
|
* 图片识别是否优先 canvas 解码(安卓 WebView 建议开启),默认在安卓/微信内自动开启
|
||||||
|
*/
|
||||||
|
webScanImageDetectPreferCanvas?: boolean,
|
||||||
|
/**
|
||||||
|
* ZXing WASM 加载超时(毫秒),默认 20000
|
||||||
|
*/
|
||||||
|
webScanPrepareTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 选图等待超时(毫秒),默认 120000
|
||||||
|
*/
|
||||||
|
webScanChooseImageTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 选图方式:button=显示「选择图片」按钮(安卓/微信默认,需用户点击);auto=自动弹出系统选图
|
||||||
|
*/
|
||||||
|
webScanImagePickerMode?: 'auto' | 'button',
|
||||||
|
webScanImagePickerTitle?: string,
|
||||||
|
webScanImagePickerButtonText?: string,
|
||||||
|
webScanImagePickerCancelText?: string,
|
||||||
|
/**
|
||||||
|
* 单次扫码会话超时(毫秒),超时后状态恢复 ready,默认 90000
|
||||||
|
*/
|
||||||
|
scanSessionTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 扫码成功提示音地址,默认使用内置提示音;任意识别模式匹配成功时播放
|
||||||
|
*/
|
||||||
|
scanBeepAudio?: string,
|
||||||
|
/**
|
||||||
|
* 扫码成功是否播放提示音,默认启用;任意识别模式匹配成功时生效
|
||||||
|
*/
|
||||||
|
scanBeepEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* PC 微信 Native 扫码支付是否展示内置二维码弹层,默认 true
|
||||||
|
*/
|
||||||
|
paymentNativeQrEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层标题
|
||||||
|
*/
|
||||||
|
paymentNativeQrTitle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层说明文案
|
||||||
|
*/
|
||||||
|
paymentNativeQrMessage?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码底部提示
|
||||||
|
*/
|
||||||
|
paymentNativeQrHint?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码边长(像素),默认 220
|
||||||
|
*/
|
||||||
|
paymentNativeQrSize?: number,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层遮罩样式
|
||||||
|
*/
|
||||||
|
paymentNativeQrOverlayStyle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层面板样式
|
||||||
|
*/
|
||||||
|
paymentNativeQrPanelStyle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码关闭按钮样式
|
||||||
|
*/
|
||||||
|
paymentNativeQrCloseButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层遮罩 class
|
||||||
|
*/
|
||||||
|
paymentNativeQrOverlayClass?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层面板 class
|
||||||
|
*/
|
||||||
|
paymentNativeQrPanelClass?: string,
|
||||||
|
/**
|
||||||
|
* PC 支付宝表单是否使用弹层 + iframe,默认 true;false 时整页跳转
|
||||||
|
*/
|
||||||
|
paymentAlipayFormEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层标题
|
||||||
|
*/
|
||||||
|
paymentAlipayFormTitle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层说明文案
|
||||||
|
*/
|
||||||
|
paymentAlipayFormMessage?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单 iframe 高度(像素),默认 520
|
||||||
|
*/
|
||||||
|
paymentAlipayFormIframeHeight?: number,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层遮罩样式(未配置时回退 paymentNativeQrOverlayStyle)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormOverlayStyle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层面板样式(未配置时回退 paymentNativeQrPanelStyle)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormPanelStyle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层关闭按钮样式(未配置时回退 paymentNativeQrCloseButtonStyle)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormCloseButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层遮罩 class(未配置时回退 paymentNativeQrOverlayClass)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormOverlayClass?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层面板 class(未配置时回退 paymentNativeQrPanelClass)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormPanelClass?: string,
|
||||||
|
/**
|
||||||
|
* 微信支付 OAuth 配置(paymentType 为 wechat 且微信内 JSAPI 时由 requestPayment 自动处理)
|
||||||
|
*/
|
||||||
|
initWechatPayment?: {
|
||||||
|
/**
|
||||||
|
* 用 code 换 openid,对应 api.biz.wechat.oauth;
|
||||||
|
* 未配置时默认 /api?action=api.biz.wechat.oauth(若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
|
||||||
|
*/
|
||||||
|
oauthApiUrl?: string,
|
||||||
|
/**
|
||||||
|
* 获取 OAuth 授权信息(含 appId、authorizeUrl),对应 api.biz.wechat.oauthAuthorizeUrl;
|
||||||
|
* 未配置时默认 /api?action=api.biz.wechat.oauthAuthorizeUrl(若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
|
||||||
|
*/
|
||||||
|
oauthAuthorizeUrlApiUrl?: string,
|
||||||
|
/**
|
||||||
|
* openid 缓存 key,默认 iscan_wechat_openid_${appId}(appId 来自授权接口响应)
|
||||||
|
*/
|
||||||
|
storageKey?: string,
|
||||||
|
/**
|
||||||
|
* OAuth 授权回跳完整 URL(redirectUri),须与当前页域名一致且已在公众号登记;
|
||||||
|
* 未配置时默认使用当前页 location.href(不含 hash)
|
||||||
|
*/
|
||||||
|
redirectUri?: string,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 微信JSSDK配置,微信环境才会生效,配置后会自动初始化微信JSSDK
|
* 微信JSSDK配置,微信环境才会生效,配置后会自动初始化微信JSSDK
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +348,16 @@ interface ScanResult {
|
|||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码错误
|
||||||
|
*/
|
||||||
|
interface ScanErrorInfo {
|
||||||
|
error: string,
|
||||||
|
key: string,
|
||||||
|
source?: string,
|
||||||
|
cancel?: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听key
|
* 监听key
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +374,10 @@ interface ScanListenerInfo {
|
|||||||
* 监听级别
|
* 监听级别
|
||||||
*/
|
*/
|
||||||
level?: number;
|
level?: number;
|
||||||
|
/**
|
||||||
|
* 为 true 时仅接收同 key 主动扫码(startScan/scanImage 等)的结果,忽略扫码枪
|
||||||
|
*/
|
||||||
|
selfKeyOnly?: boolean;
|
||||||
/**
|
/**
|
||||||
* 监听回调
|
* 监听回调
|
||||||
*/
|
*/
|
||||||
@@ -153,11 +398,108 @@ type ScanStatus = "scanning" | "ready";
|
|||||||
*/
|
*/
|
||||||
type ScanResultCallback = (result: ScanResult) => any;
|
type ScanResultCallback = (result: ScanResult) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听扫码错误回调
|
||||||
|
*/
|
||||||
|
type ScanErrorCallback = (error: ScanErrorInfo) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码错误监听信息
|
||||||
|
*/
|
||||||
|
interface ScanErrorListenerInfo {
|
||||||
|
key?: string;
|
||||||
|
match?: string;
|
||||||
|
level?: number;
|
||||||
|
listener: ScanErrorCallback;
|
||||||
|
cancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听状态回调
|
* 监听状态回调
|
||||||
*/
|
*/
|
||||||
type ScanStatusCallback = (status: ScanStatus) => any;
|
type ScanStatusCallback = (status: ScanStatus) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付渠道
|
||||||
|
*/
|
||||||
|
type PaymentType = 'wechat' | 'alipay';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSAPI 收银台交互结果(仅表示用户在收银台的操作,非支付到账结果)
|
||||||
|
*/
|
||||||
|
type PaymentCashierResult = 'ok' | 'cancel' | 'fail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付场景
|
||||||
|
*/
|
||||||
|
type PayScene = 'jsapi' | 'h5' | 'native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SDK 传给业务支付函数的参数(OAuth、环境判断由 SDK 自动完成)
|
||||||
|
*/
|
||||||
|
interface PaymentPrepareParams {
|
||||||
|
/** 微信 JSAPI 场景下的 openid,其他场景为空字符串 */
|
||||||
|
openid: string;
|
||||||
|
/** 当前环境对应的微信支付场景 */
|
||||||
|
payScene: PayScene;
|
||||||
|
/** navigator.userAgent */
|
||||||
|
userAgent: string;
|
||||||
|
/** 用户 IP,默认空字符串 */
|
||||||
|
clientIp: string;
|
||||||
|
/** 支付完成回跳地址 */
|
||||||
|
returnUrl: string;
|
||||||
|
/** 支付渠道 */
|
||||||
|
paymentType: PaymentType;
|
||||||
|
/** 支付金额(元),由 requestPayment 传入 */
|
||||||
|
amount?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务支付函数:调用自己的支付接口,返回 paymentFormData
|
||||||
|
*/
|
||||||
|
type PaymentFormDataProvider = (
|
||||||
|
params: PaymentPrepareParams
|
||||||
|
) => string | Record<string, any> | Promise<string | Record<string, any>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SDK 发起支付后的返回(不代表支付成功,业务须自行查单)
|
||||||
|
*/
|
||||||
|
interface PaymentInvokeResult {
|
||||||
|
payType?: 'jsapi' | 'h5' | 'native' | 'alipay';
|
||||||
|
/** SDK 是否已发起支付流程 */
|
||||||
|
invoked: boolean;
|
||||||
|
/** 微信 OAuth 授权跳转中(页面将刷新,业务函数尚未执行) */
|
||||||
|
oauthRedirecting?: boolean;
|
||||||
|
/** 支付未发起(含微信 OAuth 失败) */
|
||||||
|
failed?: boolean;
|
||||||
|
/** 微信 OAuth / openid 获取失败 */
|
||||||
|
oauthFailed?: boolean;
|
||||||
|
/** OAuth 或调起失败时的错误信息 */
|
||||||
|
error?: string;
|
||||||
|
/** H5 / 移动端支付宝是否已跳转整页 */
|
||||||
|
redirected?: boolean;
|
||||||
|
/** Native 场景是否展示了二维码弹层 */
|
||||||
|
qrShown?: boolean;
|
||||||
|
/** PC 支付宝是否展示了表单弹层 */
|
||||||
|
dialogShown?: boolean;
|
||||||
|
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
|
||||||
|
cashierResult?: PaymentCashierResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付请求选项
|
||||||
|
*/
|
||||||
|
interface RequestPaymentOptions {
|
||||||
|
/** 支付金额(元),展示在支付弹窗;也可由业务返回 amount / totalAmount / payAmount */
|
||||||
|
amount?: number | string;
|
||||||
|
/** 货币符号,默认 CNY(¥) */
|
||||||
|
currency?: string;
|
||||||
|
/** 支付完成回跳地址,未传时使用当前页 URL */
|
||||||
|
returnUrl?: string;
|
||||||
|
/** OAuth 授权回跳完整 URL(redirectUri),覆盖 initWechatPayment.redirectUri */
|
||||||
|
redirectUri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** IScan */
|
/** IScan */
|
||||||
interface IScan {
|
interface IScan {
|
||||||
/**
|
/**
|
||||||
@@ -176,13 +518,27 @@ interface IScan {
|
|||||||
* @param key 监听key
|
* @param key 监听key
|
||||||
* @param match 监听匹配
|
* @param match 监听匹配
|
||||||
* @param level 监听级别
|
* @param level 监听级别
|
||||||
|
* @param selfKeyOnly 为 true 时仅接收同 key 主动扫码结果,忽略扫码枪
|
||||||
*/
|
*/
|
||||||
onScanListener(callback: ScanResultCallback, key: string, match?: string, level?: number): ScanListenerInfo;
|
onScanListener(callback: ScanResultCallback, key: string, match?: string, level?: number, selfKeyOnly?: boolean): ScanListenerInfo;
|
||||||
/**
|
/**
|
||||||
* 取消监听扫码结果
|
* 取消监听扫码结果
|
||||||
* @param callback 监听回调,或监听key
|
* @param callback 监听回调,或监听key
|
||||||
*/
|
*/
|
||||||
offScanListener(callback: ScanResultCallback | string): void;
|
offScanListener(callback: ScanResultCallback | string): void;
|
||||||
|
/**
|
||||||
|
* 添加监听扫码错误(如图片识别失败)
|
||||||
|
* @param callback 错误回调
|
||||||
|
* @param key 监听 key
|
||||||
|
* @param match 可选正则,匹配 error 文本后回调
|
||||||
|
* @param level 优先级
|
||||||
|
*/
|
||||||
|
onScanErrorListener(callback: ScanErrorCallback, key: string, match?: string, level?: number): ScanErrorListenerInfo;
|
||||||
|
/**
|
||||||
|
* 取消监听扫码错误
|
||||||
|
* @param callback 监听回调,或监听 key
|
||||||
|
*/
|
||||||
|
offScanErrorListener(callback: ScanErrorCallback | string): void;
|
||||||
/**
|
/**
|
||||||
* 获取扫码状态
|
* 获取扫码状态
|
||||||
* @returns ScanStatus
|
* @returns ScanStatus
|
||||||
@@ -193,21 +549,38 @@ interface IScan {
|
|||||||
*/
|
*/
|
||||||
stopScan(): void;
|
stopScan(): void;
|
||||||
/**
|
/**
|
||||||
* 开启扫码
|
* 开启扫码(摄像头 / 桥接 / 微信等)
|
||||||
|
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
|
||||||
*/
|
*/
|
||||||
startScan(): void;
|
startScan(key?: string): void;
|
||||||
/**
|
|
||||||
* 开启视频扫码
|
|
||||||
*/
|
|
||||||
scanVideo(): void;
|
|
||||||
/**
|
/**
|
||||||
* 选择图片进行识别
|
* 选择图片进行识别
|
||||||
|
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
|
||||||
*/
|
*/
|
||||||
scanImage(): void;
|
scanImage(key?: string): void;
|
||||||
|
/**
|
||||||
|
* 由业务/原生传入已选图片 File 识别(WebView input.files 异常时使用)
|
||||||
|
* @param file 已选图片 File
|
||||||
|
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
|
||||||
|
*/
|
||||||
|
scanImageFromFile(file: File | Blob, key?: string): void;
|
||||||
/**
|
/**
|
||||||
* 清除全部监听
|
* 清除全部监听
|
||||||
*/
|
*/
|
||||||
clear(): void;
|
clear(): void;
|
||||||
|
/**
|
||||||
|
* 发起支付:按 paymentType 决定是否走微信 OAuth,再将参数传给业务函数获取 paymentFormData 并调起支付。
|
||||||
|
* 不代表支付成功,业务须自行查单。
|
||||||
|
*/
|
||||||
|
requestPayment(
|
||||||
|
paymentType: PaymentType,
|
||||||
|
fetchPaymentFormData: PaymentFormDataProvider,
|
||||||
|
options?: RequestPaymentOptions
|
||||||
|
): Promise<PaymentInvokeResult>;
|
||||||
|
/**
|
||||||
|
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层、支付宝表单弹层)
|
||||||
|
*/
|
||||||
|
closePaymentDialog(): void;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
dist/index.html
vendored
15
dist/index.html
vendored
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,minimum-scale=1,maximum-scale=1,user-scalable=no"/><title>IScan</title><script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script><script>var vconsole = new VConsole();</script><style>* {
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,minimum-scale=1,maximum-scale=1,user-scalable=no"/><title>IScan 使用示例</title><script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script><script>var vconsole = new VConsole();</script><style>* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,10 +161,11 @@
|
|||||||
|
|
||||||
section {
|
section {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}</style></head><body><main class="page"><section class="hero"><h1>IScan 通用扫码 SDK</h1><p>统一接入桥接扫码、微信小程序、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p></section><section class="grid"><div class="card"><h2>功能说明</h2><ul class="feature-list"><li>支持桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</li><li>浏览器环境使用WASM库识别二维码和条形码。</li><li>扫码结果由监听规则统一回调,支持正则匹配和优先级排序。</li></ul></div><div class="card"><h2>当前状态</h2><p>SDK 状态:<span id="status" class="status">loading</span></p><p>运行环境:</p><pre id="output" class="panel"></pre></div></section><section class="card"><h2>操作</h2><p>点击开始后,会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p><div class="actions"><button onclick="startScan()" class="btn">开始扫码</button> <button onclick="scanVideo()" class="btn secondary">开启视频扫码</button> <button onclick="scanImage()" class="btn secondary">选择图片识别</button> <button onclick="stopScan()" class="btn secondary">停止扫码</button></div></section><section class="card"><h2>扫码结果</h2><pre id="result" class="panel result"></pre></section><section class="card"><h2>错误信息</h2><pre id="error" class="panel error"></pre><p style="color: #919191">错误信息可能来源于扫码结果、扫码过程、扫码初始化等。</p></section><section class="card code"><h2>接入方式</h2><pre>IScan.config({
|
}</style></head><body><main class="page"><section class="hero"><h1>IScan 通用扫码SDK</h1><p>统一接入桥接扫码、微信小程序、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p></section><section class="grid"><div class="card"><h2>功能说明</h2><ul class="feature-list"><li>支持桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</li><li>浏览器环境使用WASM库识别二维码和条形码。</li><li>扫码结果由监听规则统一回调,支持正则匹配和优先级排序。</li></ul></div><div class="card"><h2>当前状态</h2><p>SDK 状态:<span id="status" class="status">loading</span></p><p>运行环境:</p><pre id="output" class="panel"></pre></div></section><section class="card"><h2>操作</h2><p>点击开始后,会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p><div class="actions"><button onclick="startScan()" class="btn">开始扫码</button> <button onclick="scanImage()" class="btn secondary">选择图片识别</button> <button onclick="stopScan()" class="btn secondary">停止扫码</button></div></section><section class="card"><h2>扫码结果</h2><pre id="result" class="panel result"></pre></section><section class="card"><h2>错误信息</h2><pre id="error" class="panel error"></pre><p style="color: #919191">错误信息可能来源于扫码结果、扫码过程、扫码初始化等。</p></section><section class="card code"><h2>接入方式</h2><pre>IScan.config({
|
||||||
webScanEnabled: true,
|
webScanEnabled: true,
|
||||||
webScanCanvasEnabled: true,
|
webScanCanvasEnabled: true,
|
||||||
webScanBeepEnabled: true,
|
webScanCloseButtonStyle: "background: rgba(27, 99, 244, 0.88);",
|
||||||
|
scanBeepEnabled: true,
|
||||||
initWechatJssdk: {
|
initWechatJssdk: {
|
||||||
apiUrl: "https://your-domain.com/wechat/jssdk-config"
|
apiUrl: "https://your-domain.com/wechat/jssdk-config"
|
||||||
}
|
}
|
||||||
@@ -181,7 +182,6 @@
|
|||||||
|
|
||||||
IScan.startScan();
|
IScan.startScan();
|
||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
IScan.scanVideo();
|
|
||||||
IScan.stopScan();</pre></section></main><script>(function () {
|
IScan.stopScan();</pre></section></main><script>(function () {
|
||||||
output(window.navigator.userAgent);
|
output(window.navigator.userAgent);
|
||||||
window.onerror = function (message, source, lineno, colno, err) {
|
window.onerror = function (message, source, lineno, colno, err) {
|
||||||
@@ -249,7 +249,8 @@ IScan.stopScan();</pre></section></main><script>(function () {
|
|||||||
var url = "https://vet.iqudoo.com/api?action=api.biz.wechat.JSSDKConfig";
|
var url = "https://vet.iqudoo.com/api?action=api.biz.wechat.JSSDKConfig";
|
||||||
initSDK({
|
initSDK({
|
||||||
webScanCanvasEnabled: true,
|
webScanCanvasEnabled: true,
|
||||||
webScanBeepEnabled: true,
|
webScanCloseButtonStyle: "background: rgba(27, 99, 244, 0.88);",
|
||||||
|
scanBeepEnabled: true,
|
||||||
initWechatJssdk: { apiUrl: url }
|
initWechatJssdk: { apiUrl: url }
|
||||||
}, function () {
|
}, function () {
|
||||||
setStatus(IScan.getStatus());
|
setStatus(IScan.getStatus());
|
||||||
@@ -277,8 +278,4 @@ IScan.stopScan();</pre></section></main><script>(function () {
|
|||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanVideo() {
|
|
||||||
IScan.scanVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
ready();</script><script src="index.js"></script></body></html>
|
ready();</script><script src="index.js"></script></body></html>
|
||||||
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
342
dist/index.md
vendored
Normal file
342
dist/index.md
vendored
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
# scan-code-jssdk
|
||||||
|
|
||||||
|
统一扫码 JSSDK,支持桥接扫码、微信小程序、微信 JSSDK 扫码、Web 摄像头扫码、选择图片识别和扫码枪输入。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- App 桥接环境:优先调用原生桥接 `startScan` / `stopScan`。
|
||||||
|
- 微信浏览器:初始化微信 JSSDK 后调用 `wx.scanQRCode`。
|
||||||
|
- Web 浏览器:使用 `BarcodeDetector` 识别二维码和条形码。
|
||||||
|
- 图片识别:选择本地图片后识别二维码/条形码。
|
||||||
|
- 扫码枪:监听键盘快速输入并统一走扫码监听回调。
|
||||||
|
- 监听规则:支持按 `match` 正则和 `level` 优先级分发扫码结果。
|
||||||
|
|
||||||
|
## 引入
|
||||||
|
|
||||||
|
构建后使用 `dist/index.js`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="./dist/index.js"></script>
|
||||||
|
<script>
|
||||||
|
IScan.config().then(function () {
|
||||||
|
console.log("IScan ready");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
如果脚本是后加载的,可以监听 `IScanReady`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function init() {
|
||||||
|
IScan.config().then(function () {
|
||||||
|
// ready
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.IScan) {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("IScanReady", init, { once: true });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.config({
|
||||||
|
initWechatJssdk: {
|
||||||
|
apiUrl: "https://your-domain.com/wechat/jssdk-config"
|
||||||
|
}
|
||||||
|
}).then(function () {
|
||||||
|
// 监听识别状态
|
||||||
|
IScan.setStatusListener(function (status) {
|
||||||
|
console.log("status:", status);
|
||||||
|
});
|
||||||
|
// 监听识别结果
|
||||||
|
IScan.onScanListener(function (res) {
|
||||||
|
console.log("scan result:", res.result, res.key);
|
||||||
|
}, "scan", null, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动选择可用扫码方式:桥接 -> 微信 -> Web 摄像头 -> 图片识别
|
||||||
|
IScan.startScan();
|
||||||
|
|
||||||
|
// 仅选择图片识别
|
||||||
|
IScan.scanImage();
|
||||||
|
|
||||||
|
// 停止当前扫码
|
||||||
|
IScan.stopScan();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
通过 `IScan.config(options)` 配置。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ScanConfigOptions {
|
||||||
|
scanRestartDelay?: number,
|
||||||
|
bridgeEnabled?: boolean;
|
||||||
|
bridgeName?: string;
|
||||||
|
webScanEnabled?: boolean,
|
||||||
|
webScanCanvasEnabled?: boolean;
|
||||||
|
webScanCanvasStyle?: string;
|
||||||
|
webScanCloseButtonStyle?: string;
|
||||||
|
webScanCanvasClass?: string;
|
||||||
|
webScanCloseButtonClass?: string;
|
||||||
|
webScanType?: ("qrCode" | "barCode")[];
|
||||||
|
webScanVideoMirror?: boolean;
|
||||||
|
webScanVideoMirrorVertical?: boolean;
|
||||||
|
webScanImageFallbackOnVideoError?: boolean;
|
||||||
|
webScanVideoAccessTimeout?: number;
|
||||||
|
webScanVideoReadyTimeout?: number;
|
||||||
|
scanBeepAudio?: string;
|
||||||
|
scanBeepEnabled?: boolean;
|
||||||
|
initWechatJssdk?: {
|
||||||
|
apiUrl?: string;
|
||||||
|
sdkConfig?: {
|
||||||
|
debug?: boolean;
|
||||||
|
appId: string;
|
||||||
|
timestamp: number;
|
||||||
|
nonceStr: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
sdkUrl?: string;
|
||||||
|
jsApiList?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 配置 | 说明 | 默认值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `scanRestartDelay` | 扫码重启延迟,单位:毫秒 | `500ms` |
|
||||||
|
| `bridgeEnabled` | 是否启用桥接扫码 | `true` |
|
||||||
|
| `bridgeName` | 挂载在 `window` 上的桥接对象名称 | `__bridge_client__` |
|
||||||
|
| `webScanEnabled` | 是否支持 WebScan 扫码 | `true` |
|
||||||
|
| `webScanCanvasEnabled` | 是否显示 WebScan 扫码 canvas;关闭后仍会用隐藏 canvas 识别 | `true` |
|
||||||
|
| `webScanCanvasStyle` | WebScan 扫码 canvas 内联样式 | `position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;` |
|
||||||
|
| `webScanCloseButtonStyle` | WebScan 关闭按钮内联样式(在默认位置上追加);按钮内容为 SVG,`path` 使用 `currentColor`,可通过按钮的 `color` 改图标颜色 | 圆形半透明底、白图标,定位在 canvas 右上角 |
|
||||||
|
| `webScanCanvasClass` | WebScan canvas 根元素 `class`,便于用外部样式表配合 `webScanCanvasStyle` 定制 | 无 |
|
||||||
|
| `webScanCloseButtonClass` | WebScan 关闭按钮 `class`,便于用外部样式表配合 `webScanCloseButtonStyle` 定制 | 无 |
|
||||||
|
| `webScanType` | WebScan 扫码类型 | `["qrCode", "barCode"]` |
|
||||||
|
| `webScanVideoMirror` | WebScan 视频是否水平镜像;不配置时自动判断:前置/PC 镜像,后置不镜像 | 自动 |
|
||||||
|
| `webScanVideoMirrorVertical` | WebScan 视频是否垂直镜像 | `false` |
|
||||||
|
| `webScanImageFallbackOnVideoError` | 摄像头不可用或打开失败时,是否自动弹出拍照/选图(适用于部分安卓内置浏览器) | `true` |
|
||||||
|
| `webScanVideoAccessTimeout` | 打开摄像头超时(毫秒),超时后走图片回退 | `10000` |
|
||||||
|
| `webScanVideoReadyTimeout` | 摄像头已开但无画面超时(毫秒),超时后走图片回退 | `8000` |
|
||||||
|
| `webScanCameraPermissionDialogEnabled` | `startScan` 走 Web 摄像头前是否先展示权限说明弹窗 | `true` |
|
||||||
|
| `webScanCameraPermissionTitle` | 权限说明弹窗标题 | `需要使用摄像头` |
|
||||||
|
| `webScanCameraPermissionMessage` | 权限说明弹窗正文 | 见类型定义默认值 |
|
||||||
|
| `webScanCameraPermissionConfirmText` | 确认按钮文案 | `继续` |
|
||||||
|
| `webScanCameraPermissionCancelText` | 取消按钮文案 | `取消` |
|
||||||
|
| `scanBeepAudio` | 扫码成功提示音地址(任意模式匹配成功时播放) | 内置提示音 |
|
||||||
|
| `scanBeepEnabled` | 扫码成功是否播放提示音 | `true` |
|
||||||
|
| `initWechatJssdk` | 微信 JSSDK 初始化配置,仅微信环境生效 | 无 |
|
||||||
|
|
||||||
|
`initWechatJssdk` 子配置:
|
||||||
|
|
||||||
|
| 配置 | 说明 | 默认值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `apiUrl` | 微信 JSSDK 签名配置接口地址;未传 `sdkConfig` 时会请求该接口,并自动携带当前页面 URL 参数 | 无 |
|
||||||
|
| `sdkConfig` | 直接传入微信 JSSDK 签名配置;配置后不再请求 `apiUrl` | 无 |
|
||||||
|
| `sdkConfig.debug` | 是否开启微信 JSSDK 调试模式 | `false` |
|
||||||
|
| `sdkConfig.appId` | 微信公众平台应用 ID | 必填 |
|
||||||
|
| `sdkConfig.timestamp` | 签名时间戳 | 必填 |
|
||||||
|
| `sdkConfig.nonceStr` | 签名随机字符串 | 必填 |
|
||||||
|
| `sdkConfig.signature` | 微信 JSSDK 签名 | 必填 |
|
||||||
|
| `sdkUrl` | 微信 JSSDK 脚本地址 | `https://res.wx.qq.com/open/js/jweixin-1.6.0.js` |
|
||||||
|
| `jsApiList` | 微信 JSSDK JS-API 列表,SDK 会自动追加 `scanQRCode` | `["scanQRCode"]` |
|
||||||
|
|
||||||
|
## 桥接接入
|
||||||
|
|
||||||
|
桥接对象需要挂载到 `window[bridgeName]`,并实现 `call(method, data)`。
|
||||||
|
|
||||||
|
SDK 会调用:
|
||||||
|
|
||||||
|
- `startScan`
|
||||||
|
- `stopScan`
|
||||||
|
|
||||||
|
异步回调方法名为 `${bridgeName}_handle_callback`。
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.__bridge_client__ = {
|
||||||
|
call: function (method, data) {
|
||||||
|
var requestId = data.request_id;
|
||||||
|
|
||||||
|
if (method === "startScan") {
|
||||||
|
// 调用原生扫码后回调
|
||||||
|
window.__bridge_client___handle_callback({
|
||||||
|
code: 0,
|
||||||
|
method: method,
|
||||||
|
request_id: requestId,
|
||||||
|
payload: {
|
||||||
|
result: "https://example.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "stopScan") {
|
||||||
|
window.__bridge_client___handle_callback({
|
||||||
|
code: 0,
|
||||||
|
method: method,
|
||||||
|
request_id: requestId,
|
||||||
|
payload: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 微信 JSSDK 接入
|
||||||
|
|
||||||
|
配置接口方式:
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.config({
|
||||||
|
initWechatJssdk: {
|
||||||
|
apiUrl: "https://your-domain.com/wechat/jssdk-config"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
接口会收到当前页面 URL 参数:`url=location.href.split("#")[0]`。
|
||||||
|
|
||||||
|
也可以直接传入微信签名配置:
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.config({
|
||||||
|
initWechatJssdk: {
|
||||||
|
apiUrl: "",
|
||||||
|
sdkConfig: {
|
||||||
|
appId: "wx_app_id",
|
||||||
|
timestamp: 123456,
|
||||||
|
nonceStr: "nonce",
|
||||||
|
signature: "signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`jsApiList` 会默认追加 `scanQRCode`。
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `config(options?): Promise<any>`
|
||||||
|
|
||||||
|
配置并初始化 SDK。
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.config({
|
||||||
|
webScanCanvasEnabled: true,
|
||||||
|
webScanCanvasClass: "my-webscan-canvas",
|
||||||
|
webScanCloseButtonClass: "my-webscan-close"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `setStatusListener(callback): void`
|
||||||
|
|
||||||
|
监听扫码状态,状态为:
|
||||||
|
|
||||||
|
- `scanning`
|
||||||
|
- `closed`
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.setStatusListener(function (status) {
|
||||||
|
console.log(status);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `onScanListener(callback, key, match?, level?): ScanListenerInfo`
|
||||||
|
|
||||||
|
添加扫码结果监听。
|
||||||
|
|
||||||
|
```js
|
||||||
|
var listener = IScan.onScanListener(function (res) {
|
||||||
|
console.log(res.result, res.key);
|
||||||
|
}, "order", "^https://", 100);
|
||||||
|
```
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
|
||||||
|
- `callback`:扫码结果回调。
|
||||||
|
- `key`:监听 key,同 key 会覆盖旧监听。
|
||||||
|
- `match`:可选正则字符串;扫码结果匹配后才回调。
|
||||||
|
- `level`:优先级,数值越大越先匹配。
|
||||||
|
|
||||||
|
### `offScanListener(callbackOrKey): void`
|
||||||
|
|
||||||
|
移除监听,可传 callback 或 key。
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.offScanListener("order");
|
||||||
|
listener.cancel();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `getStatus(): "scanning" | "ready"`
|
||||||
|
|
||||||
|
获取当前扫码状态。
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log(IScan.getStatus());
|
||||||
|
```
|
||||||
|
|
||||||
|
### `startScan(): void`
|
||||||
|
|
||||||
|
开启扫码。SDK 会按以下顺序选择可用能力:
|
||||||
|
|
||||||
|
1. 桥接扫码
|
||||||
|
2. 微信扫码
|
||||||
|
3. Web 摄像头扫码
|
||||||
|
4. 图片识别
|
||||||
|
|
||||||
|
扫码结果通过 `onScanListener` 回调。
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.startScan();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scanImage(): void`
|
||||||
|
|
||||||
|
直接选择图片进行识别。识别结果通过 `onScanListener` 回调。
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.scanImage();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `stopScan(): void`
|
||||||
|
|
||||||
|
停止当前扫码。
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.stopScan();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `clear(): void`
|
||||||
|
|
||||||
|
清空全部扫码监听。
|
||||||
|
|
||||||
|
```js
|
||||||
|
IScan.clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类型
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ScanResult {
|
||||||
|
result: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScanStatus = "scanning" | "ready";
|
||||||
|
|
||||||
|
type ScanResultCallback = (result: ScanResult) => any;
|
||||||
|
type ScanStatusCallback = (status: ScanStatus) => any;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物输出到 `dist` 目录。
|
||||||
@@ -51,5 +51,8 @@
|
|||||||
},
|
},
|
||||||
"repository": {},
|
"repository": {},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
154
pnpm-lock.yaml
generated
154
pnpm-lock.yaml
generated
@@ -7,6 +7,10 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
qrcode:
|
||||||
|
specifier: ^1.5.4
|
||||||
|
version: 1.5.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^7.2.3
|
specifier: ^7.2.3
|
||||||
@@ -243,6 +247,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
|
resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
ansi-styles@2.2.1:
|
ansi-styles@2.2.1:
|
||||||
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
|
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -251,6 +259,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
anymatch@2.0.0:
|
anymatch@2.0.0:
|
||||||
resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==}
|
resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==}
|
||||||
|
|
||||||
@@ -762,6 +774,9 @@ packages:
|
|||||||
cliui@5.0.0:
|
cliui@5.0.0:
|
||||||
resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==}
|
resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==}
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||||
|
|
||||||
clone@1.0.4:
|
clone@1.0.4:
|
||||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -781,6 +796,10 @@ packages:
|
|||||||
color-convert@1.9.3:
|
color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
color-name@1.1.3:
|
color-name@1.1.3:
|
||||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||||
|
|
||||||
@@ -1051,6 +1070,9 @@ packages:
|
|||||||
diffie-hellman@5.0.3:
|
diffie-hellman@5.0.3:
|
||||||
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
|
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3:
|
||||||
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
|
||||||
dom-converter@0.2.0:
|
dom-converter@0.2.0:
|
||||||
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
|
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
|
||||||
|
|
||||||
@@ -1108,6 +1130,9 @@ packages:
|
|||||||
emoji-regex@7.0.3:
|
emoji-regex@7.0.3:
|
||||||
resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
|
resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
emojis-list@3.0.0:
|
emojis-list@3.0.0:
|
||||||
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
|
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -1305,6 +1330,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
|
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
findup-sync@3.0.0:
|
findup-sync@3.0.0:
|
||||||
resolution: {integrity: sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==}
|
resolution: {integrity: sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -1776,6 +1805,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
|
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-generator-function@1.1.2:
|
is-generator-function@1.1.2:
|
||||||
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
|
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1989,6 +2022,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
|
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
lodash.camelcase@4.3.0:
|
lodash.camelcase@4.3.0:
|
||||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||||
|
|
||||||
@@ -2276,6 +2313,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
p-map@2.1.0:
|
p-map@2.1.0:
|
||||||
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
|
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2333,6 +2374,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
path-exists@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
path-is-absolute@1.0.1:
|
path-is-absolute@1.0.1:
|
||||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2397,6 +2442,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
pngjs@5.0.0:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
portfinder@1.0.38:
|
portfinder@1.0.38:
|
||||||
resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==}
|
resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==}
|
||||||
engines: {node: '>= 10.12'}
|
engines: {node: '>= 10.12'}
|
||||||
@@ -2587,6 +2636,11 @@ packages:
|
|||||||
|
|
||||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
qs@2.3.3:
|
qs@2.3.3:
|
||||||
resolution: {integrity: sha512-f5M0HQqZWkzU8GELTY8LyMrGkr3bPjKoFtTkwUEqJQbcljbeK8M7mliP9Ia2xoOI6oMerp+QPS7oYJtpGmWe/A==}
|
resolution: {integrity: sha512-f5M0HQqZWkzU8GELTY8LyMrGkr3bPjKoFtTkwUEqJQbcljbeK8M7mliP9Ia2xoOI6oMerp+QPS7oYJtpGmWe/A==}
|
||||||
|
|
||||||
@@ -2985,6 +3039,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
|
resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
string.prototype.trim@1.2.10:
|
string.prototype.trim@1.2.10:
|
||||||
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
|
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3011,6 +3069,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
|
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
strip-bom@3.0.0:
|
strip-bom@3.0.0:
|
||||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3352,6 +3414,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
|
resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
wrap-ansi@6.2.0:
|
||||||
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
@@ -3382,9 +3448,17 @@ packages:
|
|||||||
yargs-parser@13.1.2:
|
yargs-parser@13.1.2:
|
||||||
resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
|
resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
|
||||||
|
|
||||||
|
yargs-parser@18.1.3:
|
||||||
|
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
yargs@13.3.2:
|
yargs@13.3.2:
|
||||||
resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==}
|
resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==}
|
||||||
|
|
||||||
|
yargs@15.4.1:
|
||||||
|
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
zxing-wasm@3.0.2:
|
zxing-wasm@3.0.2:
|
||||||
resolution: {integrity: sha512-2YMAriaYHX9wrBY2k7H0epSo+dyCaCZg/vOtt+nEDXM9ul480gkXz/9SkwpOeHcD2H5qqDG8lWDSBwpTcZpa6w==}
|
resolution: {integrity: sha512-2YMAriaYHX9wrBY2k7H0epSo+dyCaCZg/vOtt+nEDXM9ul480gkXz/9SkwpOeHcD2H5qqDG8lWDSBwpTcZpa6w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3566,12 +3640,18 @@ snapshots:
|
|||||||
|
|
||||||
ansi-regex@4.1.1: {}
|
ansi-regex@4.1.1: {}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
ansi-styles@2.2.1: {}
|
ansi-styles@2.2.1: {}
|
||||||
|
|
||||||
ansi-styles@3.2.1:
|
ansi-styles@3.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 1.9.3
|
color-convert: 1.9.3
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
anymatch@2.0.0:
|
anymatch@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
micromatch: 3.1.10(supports-color@6.1.0)
|
micromatch: 3.1.10(supports-color@6.1.0)
|
||||||
@@ -4473,6 +4553,12 @@ snapshots:
|
|||||||
strip-ansi: 5.2.0
|
strip-ansi: 5.2.0
|
||||||
wrap-ansi: 5.1.0
|
wrap-ansi: 5.1.0
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 6.2.0
|
||||||
|
|
||||||
clone@1.0.4: {}
|
clone@1.0.4: {}
|
||||||
|
|
||||||
co@4.6.0: {}
|
co@4.6.0: {}
|
||||||
@@ -4490,6 +4576,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.3
|
color-name: 1.1.3
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
color-name@1.1.3: {}
|
color-name@1.1.3: {}
|
||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
@@ -4826,6 +4916,8 @@ snapshots:
|
|||||||
miller-rabin: 4.0.1
|
miller-rabin: 4.0.1
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3: {}
|
||||||
|
|
||||||
dom-converter@0.2.0:
|
dom-converter@0.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
utila: 0.4.0
|
utila: 0.4.0
|
||||||
@@ -4900,6 +4992,8 @@ snapshots:
|
|||||||
|
|
||||||
emoji-regex@7.0.3: {}
|
emoji-regex@7.0.3: {}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
emojis-list@3.0.0: {}
|
emojis-list@3.0.0: {}
|
||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
@@ -5188,6 +5282,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
locate-path: 3.0.0
|
locate-path: 3.0.0
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 5.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
findup-sync@3.0.0(supports-color@6.1.0):
|
findup-sync@3.0.0(supports-color@6.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-file: 1.0.0
|
detect-file: 1.0.0
|
||||||
@@ -5712,6 +5811,8 @@ snapshots:
|
|||||||
|
|
||||||
is-fullwidth-code-point@2.0.0: {}
|
is-fullwidth-code-point@2.0.0: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
is-generator-function@1.1.2:
|
is-generator-function@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -5906,6 +6007,10 @@ snapshots:
|
|||||||
p-locate: 3.0.0
|
p-locate: 3.0.0
|
||||||
path-exists: 3.0.0
|
path-exists: 3.0.0
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 4.1.0
|
||||||
|
|
||||||
lodash.camelcase@4.3.0: {}
|
lodash.camelcase@4.3.0: {}
|
||||||
|
|
||||||
lodash.memoize@4.1.2: {}
|
lodash.memoize@4.1.2: {}
|
||||||
@@ -6256,6 +6361,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 2.3.0
|
p-limit: 2.3.0
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 2.3.0
|
||||||
|
|
||||||
p-map@2.1.0: {}
|
p-map@2.1.0: {}
|
||||||
|
|
||||||
p-try@1.0.0: {}
|
p-try@1.0.0: {}
|
||||||
@@ -6310,6 +6419,8 @@ snapshots:
|
|||||||
|
|
||||||
path-exists@3.0.0: {}
|
path-exists@3.0.0: {}
|
||||||
|
|
||||||
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-is-absolute@1.0.1: {}
|
path-is-absolute@1.0.1: {}
|
||||||
|
|
||||||
path-is-inside@1.0.2: {}
|
path-is-inside@1.0.2: {}
|
||||||
@@ -6359,6 +6470,8 @@ snapshots:
|
|||||||
|
|
||||||
pngjs@3.4.0: {}
|
pngjs@3.4.0: {}
|
||||||
|
|
||||||
|
pngjs@5.0.0: {}
|
||||||
|
|
||||||
portfinder@1.0.38:
|
portfinder@1.0.38:
|
||||||
dependencies:
|
dependencies:
|
||||||
async: 3.2.6
|
async: 3.2.6
|
||||||
@@ -6620,6 +6733,12 @@ snapshots:
|
|||||||
|
|
||||||
q@1.5.1: {}
|
q@1.5.1: {}
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs: 1.0.3
|
||||||
|
pngjs: 5.0.0
|
||||||
|
yargs: 15.4.1
|
||||||
|
|
||||||
qs@2.3.3: {}
|
qs@2.3.3: {}
|
||||||
|
|
||||||
qs@6.14.2:
|
qs@6.14.2:
|
||||||
@@ -7137,6 +7256,12 @@ snapshots:
|
|||||||
is-fullwidth-code-point: 2.0.0
|
is-fullwidth-code-point: 2.0.0
|
||||||
strip-ansi: 5.2.0
|
strip-ansi: 5.2.0
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
string.prototype.trim@1.2.10:
|
string.prototype.trim@1.2.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.9
|
call-bind: 1.0.9
|
||||||
@@ -7176,6 +7301,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 4.1.1
|
ansi-regex: 4.1.1
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
strip-bom@3.0.0: {}
|
strip-bom@3.0.0: {}
|
||||||
|
|
||||||
style-loader@0.18.2:
|
style-loader@0.18.2:
|
||||||
@@ -7612,6 +7741,12 @@ snapshots:
|
|||||||
string-width: 3.1.0
|
string-width: 3.1.0
|
||||||
strip-ansi: 5.2.0
|
strip-ansi: 5.2.0
|
||||||
|
|
||||||
|
wrap-ansi@6.2.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
ws@6.2.3:
|
ws@6.2.3:
|
||||||
@@ -7631,6 +7766,11 @@ snapshots:
|
|||||||
camelcase: 5.3.1
|
camelcase: 5.3.1
|
||||||
decamelize: 1.2.0
|
decamelize: 1.2.0
|
||||||
|
|
||||||
|
yargs-parser@18.1.3:
|
||||||
|
dependencies:
|
||||||
|
camelcase: 5.3.1
|
||||||
|
decamelize: 1.2.0
|
||||||
|
|
||||||
yargs@13.3.2:
|
yargs@13.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 5.0.0
|
cliui: 5.0.0
|
||||||
@@ -7644,6 +7784,20 @@ snapshots:
|
|||||||
y18n: 4.0.3
|
y18n: 4.0.3
|
||||||
yargs-parser: 13.1.2
|
yargs-parser: 13.1.2
|
||||||
|
|
||||||
|
yargs@15.4.1:
|
||||||
|
dependencies:
|
||||||
|
cliui: 6.0.0
|
||||||
|
decamelize: 1.2.0
|
||||||
|
find-up: 4.1.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
require-main-filename: 2.0.0
|
||||||
|
set-blocking: 2.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
which-module: 2.0.1
|
||||||
|
y18n: 4.0.3
|
||||||
|
yargs-parser: 18.1.3
|
||||||
|
|
||||||
zxing-wasm@3.0.2(@types/emscripten@1.41.5):
|
zxing-wasm@3.0.2(@types/emscripten@1.41.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/emscripten': 1.41.5
|
'@types/emscripten': 1.41.5
|
||||||
|
|||||||
98
src/_core.js
98
src/_core.js
@@ -1,11 +1,27 @@
|
|||||||
import './polyfill';
|
import './polyfill';
|
||||||
import {
|
import {
|
||||||
onScanListener, offScanListener, setStatusListener, getStatus,
|
supportList,
|
||||||
startScan, stopScan, scanVideo, scanImage, clear
|
onScanListener, offScanListener,
|
||||||
|
onScanErrorListener, offScanErrorListener,
|
||||||
|
setStatusListener, getStatus,
|
||||||
|
startScan, stopScan, scanImage, scanImageFromFile, clear
|
||||||
} from './services/provider/scan';
|
} from './services/provider/scan';
|
||||||
import { setConfig, getVersion } from './services/config';
|
import { setConfig, getVersion } from './services/config';
|
||||||
import { initWxJssdk } from './services/wx';
|
import { initWxJssdk, installWxJssdkUrlWatcher, resetWxJssdkState } from './services/wx';
|
||||||
import { printDebug } from './utils/logger';
|
import {
|
||||||
|
isSupportWebScan,
|
||||||
|
prepareWebScanBarcodeDetector,
|
||||||
|
isSupportImageScan
|
||||||
|
} from './services/web';
|
||||||
|
import { printDebug, printWarn } from './utils/logger';
|
||||||
|
import { requestPayment, closePaymentDialog } from './services/payment';
|
||||||
|
import {
|
||||||
|
getSdkRuntimeUrl,
|
||||||
|
setSdkRuntimeUrlBaseline,
|
||||||
|
isSdkRuntimeUrlChanged,
|
||||||
|
setSdkRuntimeUrlChangeHandler,
|
||||||
|
bindSdkConfigReadyPromise,
|
||||||
|
} from './services/sdkLifecycle';
|
||||||
|
|
||||||
let _readyPromise = null;
|
let _readyPromise = null;
|
||||||
let _calledReady = false;
|
let _calledReady = false;
|
||||||
@@ -14,37 +30,81 @@ export function isReadyCalled() {
|
|||||||
return _calledReady;
|
return _calledReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidateSdkReadyState() {
|
||||||
|
_readyPromise = null;
|
||||||
|
_calledReady = false;
|
||||||
|
setSdkRuntimeUrlBaseline("");
|
||||||
|
bindSdkConfigReadyPromise(null);
|
||||||
|
resetWxJssdkState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSdkConfigInit() {
|
||||||
|
const urlAtStart = getSdkRuntimeUrl();
|
||||||
|
setSdkRuntimeUrlBaseline(urlAtStart);
|
||||||
|
_readyPromise = Promise.resolve().then(() => {
|
||||||
|
installWxJssdkUrlWatcher();
|
||||||
|
return initWxJssdk().catch(err => {
|
||||||
|
printDebug('init wx jssdk failed:', err && err.message ? err.message : err);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
printDebug('-------------------------------------');
|
||||||
|
printDebug('sdk_version:', getVersion());
|
||||||
|
printDebug('sdk_runtime_url:', getSdkRuntimeUrl());
|
||||||
|
printDebug('support_list:', supportList.map(item => item.name + ':' + item.support).join(', '));
|
||||||
|
printDebug('-------------------------------------');
|
||||||
|
if (isSupportWebScan() || isSupportImageScan()) {
|
||||||
|
return prepareWebScanBarcodeDetector().catch(err => {
|
||||||
|
printWarn('prepare barcode detector failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
_calledReady = true;
|
||||||
|
setSdkRuntimeUrlBaseline(getSdkRuntimeUrl());
|
||||||
|
}).catch(err => {
|
||||||
|
invalidateSdkReadyState();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
bindSdkConfigReadyPromise(_readyPromise);
|
||||||
|
return _readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
function config(config) {
|
function config(config) {
|
||||||
if (config) {
|
if (config) {
|
||||||
setConfig(config);
|
setConfig(config);
|
||||||
}
|
}
|
||||||
|
if (isSdkRuntimeUrlChanged()) {
|
||||||
|
invalidateSdkReadyState();
|
||||||
|
}
|
||||||
if (_readyPromise) {
|
if (_readyPromise) {
|
||||||
return _readyPromise;
|
return _readyPromise;
|
||||||
}
|
}
|
||||||
_readyPromise = Promise.resolve().then(() => {
|
return runSdkConfigInit();
|
||||||
printDebug('-------------------------------------');
|
|
||||||
printDebug('sdk_version:', getVersion());
|
|
||||||
printDebug('-------------------------------------');
|
|
||||||
initWxJssdk();
|
|
||||||
return Promise.resolve().then(() => {
|
|
||||||
_calledReady = true;
|
|
||||||
}).catch(err => {
|
|
||||||
_readyPromise = null;
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
return _readyPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSdkRuntimeUrlChangeHandler(() => {
|
||||||
|
if (!isSdkRuntimeUrlChanged()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printDebug('sdk runtime url changed, reinitializing config:', getSdkRuntimeUrl());
|
||||||
|
invalidateSdkReadyState();
|
||||||
|
runSdkConfigInit().catch((err) => {
|
||||||
|
printDebug('sdk reconfig failed:', err && err.message ? err.message : err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export default Object.assign({}, {
|
export default Object.assign({}, {
|
||||||
config,
|
config,
|
||||||
onScanListener,
|
onScanListener,
|
||||||
offScanListener,
|
offScanListener,
|
||||||
|
onScanErrorListener,
|
||||||
|
offScanErrorListener,
|
||||||
setStatusListener,
|
setStatusListener,
|
||||||
getStatus,
|
getStatus,
|
||||||
startScan,
|
startScan,
|
||||||
stopScan,
|
stopScan,
|
||||||
scanVideo,
|
|
||||||
scanImage,
|
scanImage,
|
||||||
|
scanImageFromFile,
|
||||||
clear,
|
clear,
|
||||||
});
|
requestPayment,
|
||||||
|
closePaymentDialog,
|
||||||
|
});
|
||||||
|
|||||||
525
src/_export.js
525
src/_export.js
@@ -1,4 +1,486 @@
|
|||||||
import _core, { isReadyCalled } from "./_core";
|
import { isReadyCalled } from "./_core";
|
||||||
|
import { getConfig } from "./services/config";
|
||||||
|
import {
|
||||||
|
readWxLikeEnvFromWindow,
|
||||||
|
setParentWxEnvReport,
|
||||||
|
getParentWxEnvReport,
|
||||||
|
} from "./services/embedEnvProbe";
|
||||||
|
import {
|
||||||
|
hasDistinctParentWindow,
|
||||||
|
resolveUseParentProxy,
|
||||||
|
} from "./services/embedProxy";
|
||||||
|
import { createUUID } from "./utils/uuid";
|
||||||
|
import {
|
||||||
|
dispatchEmbedScanResult,
|
||||||
|
acknowledgeEmbedScanConsumed,
|
||||||
|
dispatchEmbedScanError,
|
||||||
|
setEmbedScanHostEnabled,
|
||||||
|
} from "./services/provider/scan";
|
||||||
|
import {
|
||||||
|
setEmbedScanResultForwarder,
|
||||||
|
setEmbedScanErrorForwarder,
|
||||||
|
} from "./services/embedScanBridge";
|
||||||
|
import { unlockScanBeep, installScanBeepGestureUnlock } from "./services/web";
|
||||||
|
import {
|
||||||
|
setEmbedPaymentDialogTarget,
|
||||||
|
clearEmbedPaymentDialogTarget,
|
||||||
|
resolveDelegatedPaymentDialog,
|
||||||
|
handleEmbedPaymentDialogOnChild,
|
||||||
|
} from "./services/payment/embedPaymentDialog";
|
||||||
|
|
||||||
|
const EMBED_SOURCE = "IScanEmbed";
|
||||||
|
const EMBED_V = 1;
|
||||||
|
|
||||||
|
/** 已向父页发起过 invoke 的嵌入子 frame(用于父页识别结果回传) */
|
||||||
|
const embedChildSources = new Set();
|
||||||
|
|
||||||
|
const EMBED_LISTENER_METHODS = new Set([
|
||||||
|
"onScanListener",
|
||||||
|
"offScanListener",
|
||||||
|
"onScanErrorListener",
|
||||||
|
"offScanErrorListener",
|
||||||
|
"clear",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 嵌入 iframe 转发到父页时,须在子页用用户手势解锁提示音 */
|
||||||
|
const EMBED_SCAN_GESTURE_METHODS = new Set([
|
||||||
|
"startScan",
|
||||||
|
"scanImage",
|
||||||
|
"scanImageFromFile",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isEmbedMessage(data) {
|
||||||
|
return data && data.source === EMBED_SOURCE && data.v === EMBED_V;
|
||||||
|
}
|
||||||
|
|
||||||
|
let embedHostInstalled = false;
|
||||||
|
let embedChildInstalled = false;
|
||||||
|
const pendingInvokes = Object.create(null);
|
||||||
|
const pendingCallbackInvokes = Object.create(null);
|
||||||
|
const childCallbackFns = Object.create(null);
|
||||||
|
|
||||||
|
function cloneArgsReplacingFunctions(val, registry, seen) {
|
||||||
|
if (typeof val === "function") {
|
||||||
|
const id = createUUID();
|
||||||
|
registry[id] = val;
|
||||||
|
return { __IScanEmbedCb__: id };
|
||||||
|
}
|
||||||
|
if (val === null || typeof val !== "object") {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
if (!seen) {
|
||||||
|
seen = new WeakMap();
|
||||||
|
}
|
||||||
|
if (seen.has(val)) {
|
||||||
|
throw new Error("[IScan embed]: circular reference in arguments");
|
||||||
|
}
|
||||||
|
seen.set(val, true);
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val.map((item) => cloneArgsReplacingFunctions(item, registry, seen));
|
||||||
|
}
|
||||||
|
const out = {};
|
||||||
|
Object.keys(val).forEach((k) => {
|
||||||
|
out[k] = cloneArgsReplacingFunctions(val[k], registry, seen);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeEmbedParams(params) {
|
||||||
|
const serialized = [];
|
||||||
|
const registry = {};
|
||||||
|
for (let i = 0; i < params.length; i++) {
|
||||||
|
serialized.push(cloneArgsReplacingFunctions(params[i], registry));
|
||||||
|
}
|
||||||
|
return { serialized, registry };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateEmbedParams(params, messageSource, targetOrigin) {
|
||||||
|
function walk(val) {
|
||||||
|
if (val && typeof val === "object" && val.__IScanEmbedCb__) {
|
||||||
|
const cbId = val.__IScanEmbedCb__;
|
||||||
|
return function embedCbProxy() {
|
||||||
|
const args = Array.prototype.slice.call(arguments);
|
||||||
|
const callbackInvokeId = createUUID();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingCallbackInvokes[callbackInvokeId] = { resolve, reject };
|
||||||
|
messageSource.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "callback",
|
||||||
|
cbId,
|
||||||
|
callbackInvokeId,
|
||||||
|
args,
|
||||||
|
},
|
||||||
|
targetOrigin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (val === null || typeof val !== "object") {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val.map(walk);
|
||||||
|
}
|
||||||
|
const out = {};
|
||||||
|
Object.keys(val).forEach((k) => {
|
||||||
|
out[k] = walk(val[k]);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return params.map(walk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastScanErrorToEmbedChildren(error, meta) {
|
||||||
|
if (embedChildSources.size === 0 || error == null || error === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedChildSources.forEach((source) => {
|
||||||
|
try {
|
||||||
|
source.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "forwardScanError",
|
||||||
|
error,
|
||||||
|
scanKey: meta && meta.scanKey,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastScanResultToEmbedChildren(result, meta) {
|
||||||
|
if (embedChildSources.size === 0 || result == null || result === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedChildSources.forEach((source) => {
|
||||||
|
try {
|
||||||
|
source.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "forwardScanResult",
|
||||||
|
result,
|
||||||
|
scanSource: meta && meta.source,
|
||||||
|
scanKey: meta && meta.scanKey,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let embedWxProbeScheduled = false;
|
||||||
|
let embedWxProbeAttempts = 0;
|
||||||
|
const EMBED_WX_PROBE_MAX = 4;
|
||||||
|
|
||||||
|
function shouldScheduleParentWxProbe() {
|
||||||
|
if (typeof window === "undefined" || !hasDistinctParentWindow()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mode = getConfig("embedProxyMode");
|
||||||
|
if (mode === false || mode === "local" || mode === "off") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向父页询问是否微信环境(子 iframe 跨域时本地 UA 可能不可靠) */
|
||||||
|
function scheduleEmbedWxEnvProbeIfNeeded() {
|
||||||
|
if (!shouldScheduleParentWxProbe()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (embedWxProbeScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (embedWxProbeAttempts >= EMBED_WX_PROBE_MAX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedWxProbeScheduled = true;
|
||||||
|
embedWxProbeAttempts++;
|
||||||
|
ensureEmbedChildListener();
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "probeWxEnv",
|
||||||
|
id: createUUID(),
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
embedWxProbeScheduled = false;
|
||||||
|
if (getParentWxEnvReport() !== null || !shouldScheduleParentWxProbe()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleEmbedWxEnvProbeIfNeeded();
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function embedChildOnMessage(ev) {
|
||||||
|
const data = ev.data;
|
||||||
|
if (!isEmbedMessage(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "probeWxEnvResult") {
|
||||||
|
setParentWxEnvReport(!!data.wx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "forwardScanResult") {
|
||||||
|
if (typeof data.result === "string") {
|
||||||
|
const consumed = dispatchEmbedScanResult(data.result, {
|
||||||
|
source: data.scanSource,
|
||||||
|
scanKey: data.scanKey,
|
||||||
|
});
|
||||||
|
if (consumed && resolveUseParentProxy()) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "scanResultConsumed",
|
||||||
|
result: data.result,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "forwardScanError") {
|
||||||
|
if (typeof data.error === "string") {
|
||||||
|
dispatchEmbedScanError(data.error, {
|
||||||
|
scanKey: data.scanKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "paymentDialog") {
|
||||||
|
handleEmbedPaymentDialogOnChild(data, (ok, result, error) => {
|
||||||
|
window.parent.postMessage({
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "paymentDialogResult",
|
||||||
|
id: data.id,
|
||||||
|
ok,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
}, "*");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "invokeResult") {
|
||||||
|
const pending = pendingInvokes[data.id];
|
||||||
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete pendingInvokes[data.id];
|
||||||
|
if (data.ok) {
|
||||||
|
pending.resolve(data.result);
|
||||||
|
} else {
|
||||||
|
pending.reject(new Error(data.error || "[IScan embed]: invoke failed"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "callback") {
|
||||||
|
const fn = childCallbackFns[data.cbId];
|
||||||
|
const replyCallbackResult = (ok, result, error) => {
|
||||||
|
if (!data.callbackInvokeId || !ev.source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ev.source.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "callbackResult",
|
||||||
|
callbackInvokeId: data.callbackInvokeId,
|
||||||
|
ok,
|
||||||
|
result: ok ? result : undefined,
|
||||||
|
error: ok ? undefined : error,
|
||||||
|
},
|
||||||
|
ev.origin
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (typeof fn !== "function") {
|
||||||
|
replyCallbackResult(false, null, "[IScan embed]: callback not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.resolve(fn.apply(null, data.args || []))
|
||||||
|
.then((result) => {
|
||||||
|
replyCallbackResult(true, result);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
replyCallbackResult(false, null, String((err && err.message) || err));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureEmbedChildListener() {
|
||||||
|
if (embedChildInstalled || typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedChildInstalled = true;
|
||||||
|
installScanBeepGestureUnlock();
|
||||||
|
window.addEventListener("message", embedChildOnMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function embedInvoke(methodKey, params) {
|
||||||
|
ensureEmbedChildListener();
|
||||||
|
scheduleEmbedWxEnvProbeIfNeeded();
|
||||||
|
const id = createUUID();
|
||||||
|
const { serialized, registry } = serializeEmbedParams(params);
|
||||||
|
Object.keys(registry).forEach((cbId) => {
|
||||||
|
childCallbackFns[cbId] = registry[cbId];
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingInvokes[id] = { resolve, reject };
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "invoke",
|
||||||
|
id,
|
||||||
|
methodKey,
|
||||||
|
params: serialized,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEmbedHostInvoke(lib, ev) {
|
||||||
|
const data = ev.data;
|
||||||
|
const { id, methodKey, params } = data;
|
||||||
|
const shouldDelegatePaymentDialog = methodKey === "requestPayment" || methodKey === "closePaymentDialog";
|
||||||
|
if (shouldDelegatePaymentDialog && ev.source) {
|
||||||
|
setEmbedPaymentDialogTarget(ev.source);
|
||||||
|
}
|
||||||
|
const hydrated = hydrateEmbedParams(params || [], ev.source, ev.origin);
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => _exec(lib, methodKey, ...hydrated))
|
||||||
|
.then((result) => {
|
||||||
|
let out = result;
|
||||||
|
if (out && typeof out.then === "function") {
|
||||||
|
return out.then((r) => {
|
||||||
|
out = r;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
ev.source.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "invokeResult",
|
||||||
|
id,
|
||||||
|
methodKey,
|
||||||
|
ok: true,
|
||||||
|
result,
|
||||||
|
},
|
||||||
|
ev.origin
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
ev.source.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "invokeResult",
|
||||||
|
id,
|
||||||
|
methodKey,
|
||||||
|
ok: false,
|
||||||
|
error: typeof err === "string" ? err : String((err && err.message) || err),
|
||||||
|
},
|
||||||
|
ev.origin
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (shouldDelegatePaymentDialog) {
|
||||||
|
clearEmbedPaymentDialogTarget();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在顶层页面注册 iframe 嵌入代理:子页面(嵌入模式)通过 postMessage 调用同一套 SDK。
|
||||||
|
* 需在加载 SDK 后调用一次,并传入与 exportSDK 相同的 lib(通常为 ./_core 默认导出)。
|
||||||
|
*/
|
||||||
|
export function installEmbedHost(lib) {
|
||||||
|
if (embedHostInstalled || typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedHostInstalled = true;
|
||||||
|
setEmbedScanResultForwarder(broadcastScanResultToEmbedChildren);
|
||||||
|
setEmbedScanErrorForwarder(broadcastScanErrorToEmbedChildren);
|
||||||
|
setEmbedScanHostEnabled(true);
|
||||||
|
window.addEventListener("message", (ev) => {
|
||||||
|
const data = ev.data;
|
||||||
|
if (!isEmbedMessage(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "probeWxEnv") {
|
||||||
|
if (!ev.source || ev.source === window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wx = readWxLikeEnvFromWindow(window);
|
||||||
|
ev.source.postMessage(
|
||||||
|
{
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "probeWxEnvResult",
|
||||||
|
id: data.id,
|
||||||
|
wx,
|
||||||
|
},
|
||||||
|
ev.origin
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "scanResultConsumed") {
|
||||||
|
if (!ev.source || ev.source === window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof data.result === "string") {
|
||||||
|
acknowledgeEmbedScanConsumed(data.result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "callbackResult") {
|
||||||
|
const pending = pendingCallbackInvokes[data.callbackInvokeId];
|
||||||
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete pendingCallbackInvokes[data.callbackInvokeId];
|
||||||
|
if (data.ok) {
|
||||||
|
pending.resolve(data.result);
|
||||||
|
} else {
|
||||||
|
pending.reject(new Error(data.error || "[IScan embed]: callback failed"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind === "paymentDialogResult") {
|
||||||
|
resolveDelegatedPaymentDialog(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.kind !== "invoke") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.id || !data.methodKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ev.source || ev.source === window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
embedChildSources.add(ev.source);
|
||||||
|
handleEmbedHostInvoke(lib, ev);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function _exec(target, func, ...params) {
|
function _exec(target, func, ...params) {
|
||||||
let instant = target;
|
let instant = target;
|
||||||
@@ -52,6 +534,39 @@ function freezeObj(obj) {
|
|||||||
Object.freeze(obj);
|
Object.freeze(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一调用代理:根据 embedProxyMode + 环境决定走父页面转发或本地 _exec。
|
||||||
|
*/
|
||||||
|
function createInvokeTransport(lib, method, methodName, initNames) {
|
||||||
|
return function IScanInvokeProxy(...params) {
|
||||||
|
if (resolveUseParentProxy()) {
|
||||||
|
if (EMBED_LISTENER_METHODS.has(methodName)) {
|
||||||
|
if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) {
|
||||||
|
throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}`
|
||||||
|
}
|
||||||
|
return _exec(lib, methodName, ...params);
|
||||||
|
}
|
||||||
|
if (EMBED_SCAN_GESTURE_METHODS.has(method)) {
|
||||||
|
unlockScanBeep();
|
||||||
|
}
|
||||||
|
return embedInvoke(methodName, params);
|
||||||
|
}
|
||||||
|
if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) {
|
||||||
|
throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}`
|
||||||
|
}
|
||||||
|
return _exec(lib, methodName, ...params);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试:当前解析到的嵌入转发开关(每次读取最新 config)。
|
||||||
|
*/
|
||||||
|
export function getEmbedProxyResolved() {
|
||||||
|
return resolveUseParentProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { resolveUseParentProxy } from "./services/embedProxy";
|
||||||
|
|
||||||
export function exportSDK(lib, funcs, ...initNames) {
|
export function exportSDK(lib, funcs, ...initNames) {
|
||||||
let methods = {};
|
let methods = {};
|
||||||
if (funcs && typeof funcs === 'object') {
|
if (funcs && typeof funcs === 'object') {
|
||||||
@@ -63,13 +578,9 @@ export function exportSDK(lib, funcs, ...initNames) {
|
|||||||
Object.keys(methods).forEach(method => {
|
Object.keys(methods).forEach(method => {
|
||||||
let methodItem = methods[method];
|
let methodItem = methods[method];
|
||||||
let methodName = methodItem && methodItem.method || method;
|
let methodName = methodItem && methodItem.method || method;
|
||||||
hook(library, method, (...params) => {
|
hook(library, method, createInvokeTransport(lib, method, methodName, initNames));
|
||||||
if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) {
|
|
||||||
throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}`
|
|
||||||
}
|
|
||||||
return _exec(lib, methodName, ...params);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
scheduleEmbedWxEnvProbeIfNeeded();
|
||||||
freezeObj(library);
|
freezeObj(library);
|
||||||
return library;
|
return library;
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/index.js
13
src/index.js
@@ -1,9 +1,18 @@
|
|||||||
|
import './polyfill';
|
||||||
import core from "./_core";
|
import core from "./_core";
|
||||||
import _global from './polyfill/_global';
|
import _global from './polyfill/_global';
|
||||||
import { exportSDK } from './_export';
|
import { exportSDK, installEmbedHost } from './_export';
|
||||||
|
import { setSdkScriptSrc } from './services/web';
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) {
|
||||||
|
setSdkScriptSrc(document.currentScript.src);
|
||||||
|
}
|
||||||
|
|
||||||
const IScan = exportSDK(core, null, "config", "setStatusListener", "onScanListener",
|
const IScan = exportSDK(core, null, "config", "setStatusListener", "onScanListener",
|
||||||
"offScanListener", "stopScan", "startScan", "scanImage", "clear");
|
"offScanListener", "onScanErrorListener", "offScanErrorListener", "stopScan", "startScan", "scanImage", "scanImageFromFile", "clear",
|
||||||
|
"requestPayment", "closePaymentDialog");
|
||||||
|
|
||||||
|
installEmbedHost(core);
|
||||||
|
|
||||||
function dispatchIScanReady() {
|
function dispatchIScanReady() {
|
||||||
_global.__IScanReady__ && _global.__IScanReady__();
|
_global.__IScanReady__ && _global.__IScanReady__();
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { polyfill } from 'es6-promise';
|
import { polyfill } from 'es6-promise';
|
||||||
|
|
||||||
|
// Object.hasOwn (ES2022) — barcode-detector / ZXing 依赖,旧版 WebView 无此方法
|
||||||
|
if (typeof Object.hasOwn !== 'function') {
|
||||||
|
Object.hasOwn = function hasOwn(object, key) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, key);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Object.assign
|
// Object.assign
|
||||||
if (typeof Object.assign != 'function') {
|
if (typeof Object.assign != 'function') {
|
||||||
// Must be writable: true, enumerable: false, configurable: true
|
// Must be writable: true, enumerable: false, configurable: true
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ let _defineConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _defConfig = {
|
let _defConfig = {
|
||||||
|
/**
|
||||||
|
* iframe / 嵌入场景下是否把调用转发到父页面的同名 SDK(postMessage)。
|
||||||
|
* - 'auto'(默认):处于子 frame 时 startScan 等走父页;onScanListener 仅注册在 iframe 内,识别结果由父页回传
|
||||||
|
* - true | 'on' | 'parent':在存在父 window 时强制转发
|
||||||
|
* - false | 'off' | 'local':始终在本页执行(子页自己要跑扫码时用)
|
||||||
|
*/
|
||||||
|
embedProxyMode: 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
let _customConfig = {
|
let _customConfig = {
|
||||||
|
|||||||
40
src/services/embedEnvProbe.js
Normal file
40
src/services/embedEnvProbe.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 嵌入场景下由父页探测得到的微信环境结论(子页跨域时无法读 top UA)
|
||||||
|
*/
|
||||||
|
let parentWxEnvReport = null;
|
||||||
|
|
||||||
|
export function setParentWxEnvReport(wx) {
|
||||||
|
parentWxEnvReport = wx ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getParentWxEnvReport() {
|
||||||
|
return parentWxEnvReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在给定 window 上探测微信系 WebView(公众号 / 部分企业微信)
|
||||||
|
* 与外层页面是否为同一套 SDK无关:依赖 UA / WeixinJSBridge
|
||||||
|
*/
|
||||||
|
export function readWxLikeEnvFromWindow(win) {
|
||||||
|
if (!win) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const nav = win.navigator;
|
||||||
|
if (nav) {
|
||||||
|
const ua = nav.userAgent || "";
|
||||||
|
if (/micromessenger/i.test(ua)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/wxwork/i.test(ua)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (win.WeixinJSBridge) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
37
src/services/embedProxy.js
Normal file
37
src/services/embedProxy.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { getConfig } from "./config";
|
||||||
|
|
||||||
|
/** 是否存在可与 postMessage 交互的父 browsing context(自身不是顶层 opener/parent) */
|
||||||
|
export function hasDistinctParentWindow() {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return window.parent != null && window.parent !== window;
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否通过父页面 SDK 代理执行(每次调用重新读取 config)
|
||||||
|
*
|
||||||
|
* embedProxyMode 为 auto(默认)时:只要 window.parent !== window(处于子 frame),
|
||||||
|
* 所有对外 API(含 startScan)均走 postMessage 由父页同一套 SDK 执行,避免子页重复跑扫码逻辑。
|
||||||
|
*/
|
||||||
|
export function resolveUseParentProxy() {
|
||||||
|
const mode = getConfig("embedProxyMode");
|
||||||
|
if (mode === false || mode === "local" || mode === "off") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const parentOk = hasDistinctParentWindow();
|
||||||
|
if (!parentOk) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mode === true || mode === "on" || mode === "parent") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (mode === "auto" || mode === undefined || mode === null) {
|
||||||
|
return parentOk;
|
||||||
|
}
|
||||||
|
return !!mode;
|
||||||
|
}
|
||||||
24
src/services/embedScanBridge.js
Normal file
24
src/services/embedScanBridge.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/** 父页识别到扫码结果时,向嵌入 iframe 转发的回调(由 installEmbedHost 注册) */
|
||||||
|
let embedScanResultForwarder = null;
|
||||||
|
/** 父页图片识别失败时,向嵌入 iframe 转发的回调 */
|
||||||
|
let embedScanErrorForwarder = null;
|
||||||
|
|
||||||
|
export function setEmbedScanResultForwarder(fn) {
|
||||||
|
embedScanResultForwarder = typeof fn === "function" ? fn : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEmbedScanErrorForwarder(fn) {
|
||||||
|
embedScanErrorForwarder = typeof fn === "function" ? fn : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forwardEmbedScanResultIfNeeded(result, meta) {
|
||||||
|
if (embedScanResultForwarder && result != null && result !== "") {
|
||||||
|
embedScanResultForwarder(result, meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forwardEmbedScanErrorIfNeeded(error, meta) {
|
||||||
|
if (embedScanErrorForwarder && error != null && error !== "") {
|
||||||
|
embedScanErrorForwarder(error, meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/services/payment/alipayFormView.js
Normal file
118
src/services/payment/alipayFormView.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { getConfig } from "../config";
|
||||||
|
import { createPaymentDialogHost } from "./paymentDialogHost";
|
||||||
|
import { appendPaymentAmount } from "./paymentAmount";
|
||||||
|
import { hasEmbedPaymentDialogTarget, delegatePaymentDialog } from "./embedPaymentDialog";
|
||||||
|
|
||||||
|
const OVERLAY_ID = "__iscan_payment_alipay_form__";
|
||||||
|
const dialogHost = createPaymentDialogHost(OVERLAY_ID);
|
||||||
|
|
||||||
|
function getStyleConfig(alipayKey, nativeKey, fallback) {
|
||||||
|
const value = getConfig(alipayKey);
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const nativeValue = getConfig(nativeKey);
|
||||||
|
if (nativeValue) {
|
||||||
|
return nativeValue;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIframeHeight() {
|
||||||
|
const height = getConfig("paymentAlipayFormIframeHeight");
|
||||||
|
if (typeof height === "number" && height > 0) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
return 520;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeHtmlToIframe(iframe, html) {
|
||||||
|
const iframeWin = iframe.contentWindow;
|
||||||
|
const iframeDoc = iframe.contentDocument || (iframeWin && iframeWin.document);
|
||||||
|
if (!iframeDoc) {
|
||||||
|
throw new Error("alipay payment iframe document is not available");
|
||||||
|
}
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(html);
|
||||||
|
iframeDoc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeAlipayPaymentForm() {
|
||||||
|
dialogHost.removeOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showAlipayPaymentFormLocal(html, amount, currency) {
|
||||||
|
if (!html) {
|
||||||
|
return Promise.reject(new Error("alipay payment form html is required"));
|
||||||
|
}
|
||||||
|
if (getConfig("paymentAlipayFormEnabled") === false) {
|
||||||
|
return Promise.resolve({ shown: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
let paymentIframe;
|
||||||
|
try {
|
||||||
|
dialogHost.mountOverlay({
|
||||||
|
overlayClass: getConfig("paymentAlipayFormOverlayClass") || getConfig("paymentNativeQrOverlayClass"),
|
||||||
|
panelClass: getConfig("paymentAlipayFormPanelClass") || getConfig("paymentNativeQrPanelClass"),
|
||||||
|
overlayStyle: getStyleConfig(
|
||||||
|
"paymentAlipayFormOverlayStyle",
|
||||||
|
"paymentNativeQrOverlayStyle",
|
||||||
|
undefined
|
||||||
|
),
|
||||||
|
panelStyle: getStyleConfig(
|
||||||
|
"paymentAlipayFormPanelStyle",
|
||||||
|
"paymentNativeQrPanelStyle",
|
||||||
|
"position:relative;width:100%;max-width:480px;background:#fff;border-radius:12px;padding:24px 20px 20px;box-shadow:0 8px 28px rgba(0,0,0,0.22);"
|
||||||
|
),
|
||||||
|
closeButtonStyle: getStyleConfig(
|
||||||
|
"paymentAlipayFormCloseButtonStyle",
|
||||||
|
"paymentNativeQrCloseButtonStyle",
|
||||||
|
undefined
|
||||||
|
),
|
||||||
|
fillPanel(panel) {
|
||||||
|
const doc = panel.ownerDocument;
|
||||||
|
|
||||||
|
const title = doc.createElement("h3");
|
||||||
|
title.textContent = getConfig("paymentAlipayFormTitle") || "支付宝支付";
|
||||||
|
title.style.cssText = "margin:0 0 8px;font-size:18px;font-weight:600;color:#222;line-height:1.35;text-align:center;";
|
||||||
|
|
||||||
|
const message = doc.createElement("p");
|
||||||
|
message.textContent = getConfig("paymentAlipayFormMessage") || "请在下方页面完成支付";
|
||||||
|
message.style.cssText = "margin:0 0 12px;font-size:14px;color:#666;line-height:1.5;text-align:center;";
|
||||||
|
|
||||||
|
const iframeWrap = doc.createElement("div");
|
||||||
|
iframeWrap.style.cssText = "width:100%;overflow:hidden;border:1px solid #eee;border-radius:8px;background:#fff;";
|
||||||
|
|
||||||
|
paymentIframe = doc.createElement("iframe");
|
||||||
|
paymentIframe.setAttribute("title", "支付宝支付");
|
||||||
|
paymentIframe.style.cssText = `display:block;width:100%;height:${getIframeHeight()}px;border:0;background:#fff;`;
|
||||||
|
|
||||||
|
iframeWrap.appendChild(paymentIframe);
|
||||||
|
panel.appendChild(title);
|
||||||
|
appendPaymentAmount(panel, doc, amount, currency);
|
||||||
|
panel.appendChild(message);
|
||||||
|
panel.appendChild(iframeWrap);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeHtmlToIframe(paymentIframe, html);
|
||||||
|
return Promise.resolve({ shown: true });
|
||||||
|
} catch (err) {
|
||||||
|
dialogHost.removeOverlay();
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showAlipayPaymentForm(html, amount, currency) {
|
||||||
|
if (hasEmbedPaymentDialogTarget()) {
|
||||||
|
const delegated = delegatePaymentDialog("showAlipayPaymentForm", { html, amount, currency });
|
||||||
|
if (delegated) {
|
||||||
|
return delegated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showAlipayPaymentFormLocal(html, amount, currency);
|
||||||
|
}
|
||||||
58
src/services/payment/config.js
Normal file
58
src/services/payment/config.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { getConfig } from "../config";
|
||||||
|
import { toAny } from "../../utils/toany";
|
||||||
|
|
||||||
|
const DEFAULT_OAUTH_ACTION = "api.biz.wechat.oauth";
|
||||||
|
const DEFAULT_OAUTH_AUTHORIZE_URL_ACTION = "api.biz.wechat.oauthAuthorizeUrl";
|
||||||
|
const DEFAULT_API_PATH = "/api";
|
||||||
|
|
||||||
|
function buildApiUrlByAction(action) {
|
||||||
|
if (typeof window === "undefined" || !window.location) {
|
||||||
|
return `${DEFAULT_API_PATH}?action=${action}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(DEFAULT_API_PATH, window.location.origin);
|
||||||
|
url.searchParams.set("action", action);
|
||||||
|
return url.href;
|
||||||
|
} catch (e) {
|
||||||
|
return `${DEFAULT_API_PATH}?action=${action}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveApiUrlFromJssdk(jssdkApiUrl, action) {
|
||||||
|
if (!jssdkApiUrl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(String(jssdkApiUrl), typeof window !== "undefined" ? window.location.origin : undefined);
|
||||||
|
url.searchParams.set("action", action);
|
||||||
|
return url.href;
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOauthApiUrl(cfg, jssdkApiUrl) {
|
||||||
|
if (cfg.oauthApiUrl) {
|
||||||
|
return String(cfg.oauthApiUrl);
|
||||||
|
}
|
||||||
|
return deriveApiUrlFromJssdk(jssdkApiUrl, DEFAULT_OAUTH_ACTION) || buildApiUrlByAction(DEFAULT_OAUTH_ACTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOauthAuthorizeUrlApiUrl(cfg, jssdkApiUrl) {
|
||||||
|
if (cfg.oauthAuthorizeUrlApiUrl) {
|
||||||
|
return String(cfg.oauthAuthorizeUrlApiUrl);
|
||||||
|
}
|
||||||
|
return deriveApiUrlFromJssdk(jssdkApiUrl, DEFAULT_OAUTH_AUTHORIZE_URL_ACTION)
|
||||||
|
|| buildApiUrlByAction(DEFAULT_OAUTH_AUTHORIZE_URL_ACTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPaymentConfig() {
|
||||||
|
const cfg = toAny(getConfig("initWechatPayment"), {});
|
||||||
|
const jssdkApiUrl = toAny(getConfig("initWechatJssdk"), {}).apiUrl;
|
||||||
|
return {
|
||||||
|
oauthApiUrl: resolveOauthApiUrl(cfg, jssdkApiUrl),
|
||||||
|
oauthAuthorizeUrlApiUrl: resolveOauthAuthorizeUrlApiUrl(cfg, jssdkApiUrl),
|
||||||
|
storageKey: cfg.storageKey,
|
||||||
|
redirectUri: cfg.redirectUri,
|
||||||
|
};
|
||||||
|
}
|
||||||
92
src/services/payment/embedPaymentDialog.js
Normal file
92
src/services/payment/embedPaymentDialog.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { createUUID } from "../../utils/uuid";
|
||||||
|
|
||||||
|
const EMBED_SOURCE = "IScanEmbed";
|
||||||
|
const EMBED_V = 1;
|
||||||
|
const DELEGATE_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
|
let embedDialogTarget = null;
|
||||||
|
let lastPaymentEmbedSource = null;
|
||||||
|
const pendingDialogOps = Object.create(null);
|
||||||
|
let childActionRunner = null;
|
||||||
|
|
||||||
|
export function setEmbedPaymentDialogTarget(source) {
|
||||||
|
embedDialogTarget = source || null;
|
||||||
|
if (source) {
|
||||||
|
lastPaymentEmbedSource = source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearEmbedPaymentDialogTarget() {
|
||||||
|
embedDialogTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLastPaymentEmbedSource() {
|
||||||
|
lastPaymentEmbedSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaymentDialogDelegateTarget() {
|
||||||
|
return embedDialogTarget || lastPaymentEmbedSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasEmbedPaymentDialogTarget() {
|
||||||
|
return !!getPaymentDialogDelegateTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPaymentDialogChildRunner(fn) {
|
||||||
|
childActionRunner = typeof fn === "function" ? fn : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delegatePaymentDialog(action, payload) {
|
||||||
|
const target = getPaymentDialogDelegateTarget();
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const id = createUUID();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
delete pendingDialogOps[id];
|
||||||
|
reject(new Error("payment dialog delegate timeout"));
|
||||||
|
}, DELEGATE_TIMEOUT_MS);
|
||||||
|
pendingDialogOps[id] = { resolve, reject, timer };
|
||||||
|
try {
|
||||||
|
target.postMessage({
|
||||||
|
source: EMBED_SOURCE,
|
||||||
|
v: EMBED_V,
|
||||||
|
kind: "paymentDialog",
|
||||||
|
id,
|
||||||
|
action,
|
||||||
|
payload: payload || {},
|
||||||
|
}, "*");
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
delete pendingDialogOps[id];
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDelegatedPaymentDialog(data) {
|
||||||
|
const pending = pendingDialogOps[data.id];
|
||||||
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(pending.timer);
|
||||||
|
delete pendingDialogOps[data.id];
|
||||||
|
if (data.ok) {
|
||||||
|
pending.resolve(data.result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pending.reject(new Error(data.error || "payment dialog delegate failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleEmbedPaymentDialogOnChild(data, reply) {
|
||||||
|
if (typeof childActionRunner !== "function") {
|
||||||
|
reply(false, null, "payment dialog child runner is not ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.resolve(childActionRunner(data.action, data.payload || {}))
|
||||||
|
.then((result) => reply(true, result))
|
||||||
|
.catch((err) => {
|
||||||
|
reply(false, null, err && err.message ? err.message : String(err || "payment dialog failed"));
|
||||||
|
});
|
||||||
|
}
|
||||||
41
src/services/payment/env.js
Normal file
41
src/services/payment/env.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { readWxLikeEnvFromWindow, getParentWxEnvReport } from "../embedEnvProbe";
|
||||||
|
import { resolveUseParentProxy } from "../embedProxy";
|
||||||
|
|
||||||
|
export function getTopWindow() {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return window.top || window;
|
||||||
|
} catch (e) {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWechatBrowser() {
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (readWxLikeEnvFromWindow(topWin)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined" && readWxLikeEnvFromWindow(window)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (resolveUseParentProxy() && getParentWxEnvReport() === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 UA 判断微信支付场景,与后端 payScene=auto 规则一致。
|
||||||
|
*/
|
||||||
|
export function detectPayScene(userAgent) {
|
||||||
|
const ua = userAgent || (typeof navigator !== "undefined" ? navigator.userAgent : "");
|
||||||
|
if (/MicroMessenger/i.test(ua)) {
|
||||||
|
return "jsapi";
|
||||||
|
}
|
||||||
|
if (/Mobile|Android|iPhone|iPad|iPod/i.test(ua)) {
|
||||||
|
return "h5";
|
||||||
|
}
|
||||||
|
return "native";
|
||||||
|
}
|
||||||
329
src/services/payment/index.js
Normal file
329
src/services/payment/index.js
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import { toAny } from "../../utils/toany";
|
||||||
|
import { getConfig } from "../config";
|
||||||
|
import { getTopWindow, detectPayScene } from "./env";
|
||||||
|
import { showPaymentNativeQrCode, closePaymentNativeQrCode } from "./nativeQrView";
|
||||||
|
import { showAlipayPaymentForm, closeAlipayPaymentForm } from "./alipayFormView";
|
||||||
|
import {
|
||||||
|
hasEmbedPaymentDialogTarget,
|
||||||
|
delegatePaymentDialog,
|
||||||
|
registerPaymentDialogChildRunner,
|
||||||
|
clearLastPaymentEmbedSource,
|
||||||
|
} from "./embedPaymentDialog";
|
||||||
|
import { runPaymentDialogChildAction } from "./paymentDialogChild";
|
||||||
|
|
||||||
|
registerPaymentDialogChildRunner(runPaymentDialogChildAction);
|
||||||
|
import { resolvePaymentAmount } from "./paymentAmount";
|
||||||
|
import { ensureWechatOpenid } from "./oauth";
|
||||||
|
|
||||||
|
function buildInvokeResult(payType, extra) {
|
||||||
|
return Object.assign({ payType, invoked: true }, extra || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOAuthRedirectResult() {
|
||||||
|
return { invoked: false, oauthRedirecting: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPaymentFailureResult(error) {
|
||||||
|
const message = error && error.message ? error.message : String(error || "payment failed");
|
||||||
|
return {
|
||||||
|
invoked: false,
|
||||||
|
failed: true,
|
||||||
|
oauthFailed: true,
|
||||||
|
error: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultReturnUrl() {
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin || !topWin.location) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return topWin.location.href.split("#")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePaymentType(paymentType) {
|
||||||
|
const type = String(paymentType || "").toLowerCase();
|
||||||
|
if (type !== "wechat" && type !== "alipay") {
|
||||||
|
throw new Error("paymentType must be wechat or alipay");
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPaymentPrepareParams(paymentType, openid, options) {
|
||||||
|
const opts = toAny(options, {});
|
||||||
|
const payScene = detectPayScene();
|
||||||
|
return {
|
||||||
|
openid: paymentType === "wechat" && payScene === "jsapi" ? (openid || "") : "",
|
||||||
|
payScene,
|
||||||
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
||||||
|
clientIp: "",
|
||||||
|
returnUrl: opts.returnUrl || buildDefaultReturnUrl(),
|
||||||
|
paymentType,
|
||||||
|
amount: opts.amount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapPaymentFormData(result) {
|
||||||
|
if (result == null || result === "") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (typeof result === "string") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (typeof result === "object") {
|
||||||
|
if (result.paymentFormData != null && result.paymentFormData !== "") {
|
||||||
|
return result.paymentFormData;
|
||||||
|
}
|
||||||
|
if (result.data != null) {
|
||||||
|
if (typeof result.data === "string" && result.data !== "") {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
if (result.data.paymentFormData != null && result.data.paymentFormData !== "") {
|
||||||
|
return result.data.paymentFormData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.payType || isAlipayFormHtml(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldEnsureWechatOAuth(paymentType) {
|
||||||
|
return paymentType === "wechat" && detectPayScene() === "jsapi";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureOAuthBeforePayment(paymentType, oauthOptions) {
|
||||||
|
if (!shouldEnsureWechatOAuth(paymentType)) {
|
||||||
|
return Promise.resolve("");
|
||||||
|
}
|
||||||
|
return ensureWechatOpenid(oauthOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAlipayFormHtml(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return /<form[\s>]/i.test(trimmed) || /<input[\s>]/i.test(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePaymentFormData(paymentFormData, paymentType) {
|
||||||
|
if (paymentType === "alipay" || isAlipayFormHtml(paymentFormData)) {
|
||||||
|
return {
|
||||||
|
type: "alipay",
|
||||||
|
data: typeof paymentFormData === "string" ? paymentFormData : String(paymentFormData || ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData = paymentFormData;
|
||||||
|
if (typeof formData === "string") {
|
||||||
|
try {
|
||||||
|
formData = JSON.parse(formData);
|
||||||
|
} catch (e) {
|
||||||
|
if (isAlipayFormHtml(formData)) {
|
||||||
|
return { type: "alipay", data: formData };
|
||||||
|
}
|
||||||
|
throw new Error("paymentFormData is not valid JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formData = toAny(formData, {});
|
||||||
|
const payType = formData.payType;
|
||||||
|
if (!payType) {
|
||||||
|
throw new Error("paymentFormData.payType is required");
|
||||||
|
}
|
||||||
|
return { type: String(payType).toLowerCase(), data: formData };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPcBrowser() {
|
||||||
|
return detectPayScene() === "native";
|
||||||
|
}
|
||||||
|
|
||||||
|
function execAlipayPay(html, amount, currency) {
|
||||||
|
if (isPcBrowser() && getConfig("paymentAlipayFormEnabled") !== false) {
|
||||||
|
return showAlipayPaymentForm(html, amount, currency).then((result) => {
|
||||||
|
return buildInvokeResult("alipay", {
|
||||||
|
dialogShown: !!(result && result.shown),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin || !topWin.document) {
|
||||||
|
return Promise.reject(new Error("top window document is not available"));
|
||||||
|
}
|
||||||
|
const doc = topWin.document;
|
||||||
|
doc.open();
|
||||||
|
doc.write(html);
|
||||||
|
doc.close();
|
||||||
|
return Promise.resolve(buildInvokeResult("alipay", { redirected: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function execWechatH5Pay(formData) {
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin) {
|
||||||
|
return Promise.reject(new Error("top window is not available"));
|
||||||
|
}
|
||||||
|
const h5Url = formData.h5Url;
|
||||||
|
if (!h5Url) {
|
||||||
|
return Promise.reject(new Error("paymentFormData.h5Url is required"));
|
||||||
|
}
|
||||||
|
topWin.location.href = h5Url;
|
||||||
|
return Promise.resolve(buildInvokeResult("h5", { redirected: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function execWechatNativePay(formData, amount, currency) {
|
||||||
|
const codeUrl = formData.codeUrl;
|
||||||
|
if (!codeUrl) {
|
||||||
|
return Promise.reject(new Error("paymentFormData.codeUrl is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const showBuiltinQr = getConfig("paymentNativeQrEnabled") !== false;
|
||||||
|
const showPromise = showBuiltinQr
|
||||||
|
? showPaymentNativeQrCode(codeUrl, amount, currency)
|
||||||
|
: Promise.resolve({ shown: false });
|
||||||
|
|
||||||
|
return showPromise.then((qrResult) => {
|
||||||
|
return buildInvokeResult("native", {
|
||||||
|
qrShown: !!(qrResult && qrResult.shown),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function execWechatJsapiPay(formData) {
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin) {
|
||||||
|
return Promise.reject(new Error("top window is not available"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const payParams = {
|
||||||
|
appId: formData.appId,
|
||||||
|
timeStamp: formData.timeStamp,
|
||||||
|
nonceStr: formData.nonceStr,
|
||||||
|
package: formData.package,
|
||||||
|
signType: formData.signType,
|
||||||
|
paySign: formData.paySign,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requiredKeys = ["appId", "timeStamp", "nonceStr", "package", "signType", "paySign"];
|
||||||
|
for (let i = 0; i < requiredKeys.length; i++) {
|
||||||
|
const key = requiredKeys[i];
|
||||||
|
if (!payParams[key]) {
|
||||||
|
return Promise.reject(new Error(`paymentFormData.${key} is required for jsapi pay`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
function onBridgeReady() {
|
||||||
|
const bridge = topWin.WeixinJSBridge;
|
||||||
|
if (!bridge || !bridge.invoke) {
|
||||||
|
reject(new Error("WeixinJSBridge is not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bridge.invoke("getBrandWCPayRequest", payParams, (res) => {
|
||||||
|
const errMsg = res && res.err_msg ? res.err_msg : "";
|
||||||
|
if (errMsg === "get_brand_wcpay_request:ok") {
|
||||||
|
resolve(buildInvokeResult("jsapi", { cashierResult: "ok" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errMsg === "get_brand_wcpay_request:cancel") {
|
||||||
|
resolve(buildInvokeResult("jsapi", { cashierResult: "cancel" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(buildInvokeResult("jsapi", { cashierResult: "fail" }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof topWin.WeixinJSBridge === "undefined") {
|
||||||
|
topWin.document.addEventListener("WeixinJSBridgeReady", onBridgeReady, false);
|
||||||
|
} else {
|
||||||
|
onBridgeReady();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokePaymentFormData(paymentFormData, paymentType, amount, currency) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = normalizePaymentFormData(paymentFormData, paymentType);
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (parsed.type) {
|
||||||
|
case "alipay":
|
||||||
|
return execAlipayPay(parsed.data, amount, currency);
|
||||||
|
case "jsapi":
|
||||||
|
return execWechatJsapiPay(parsed.data);
|
||||||
|
case "h5":
|
||||||
|
return execWechatH5Pay(parsed.data);
|
||||||
|
case "native":
|
||||||
|
return execWechatNativePay(parsed.data, amount, currency);
|
||||||
|
default:
|
||||||
|
return Promise.reject(new Error(`unknown pay type: ${parsed.type}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层、支付宝表单弹层)。
|
||||||
|
*/
|
||||||
|
export function closePaymentDialog() {
|
||||||
|
if (hasEmbedPaymentDialogTarget()) {
|
||||||
|
const delegated = delegatePaymentDialog("closePaymentDialog", {});
|
||||||
|
if (delegated) {
|
||||||
|
return delegated.then((result) => {
|
||||||
|
clearLastPaymentEmbedSource();
|
||||||
|
return result;
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closePaymentNativeQrCode();
|
||||||
|
closeAlipayPaymentForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起支付:按 paymentType 决定是否走微信 OAuth,再将参数传给业务函数获取 paymentFormData 并调起支付。
|
||||||
|
*/
|
||||||
|
export function requestPayment(paymentType, fetchPaymentFormData, options) {
|
||||||
|
let normalizedPaymentType;
|
||||||
|
try {
|
||||||
|
normalizedPaymentType = normalizePaymentType(paymentType);
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
if (typeof fetchPaymentFormData !== "function") {
|
||||||
|
return Promise.reject(new Error("requestPayment requires a function"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = toAny(options, {});
|
||||||
|
const oauthOptions = {};
|
||||||
|
if (opts.redirectUri) {
|
||||||
|
oauthOptions.redirectUri = opts.redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ensureOAuthBeforePayment(normalizedPaymentType, oauthOptions).then(
|
||||||
|
(openid) => {
|
||||||
|
if (openid === null) {
|
||||||
|
return buildOAuthRedirectResult();
|
||||||
|
}
|
||||||
|
if (shouldEnsureWechatOAuth(normalizedPaymentType) && !openid) {
|
||||||
|
return buildPaymentFailureResult(new Error("wechat openid is empty after oauth"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareParams = buildPaymentPrepareParams(normalizedPaymentType, openid, opts);
|
||||||
|
return Promise.resolve(fetchPaymentFormData(prepareParams)).then((rawResult) => {
|
||||||
|
const paymentFormData = unwrapPaymentFormData(rawResult);
|
||||||
|
if (paymentFormData == null || paymentFormData === "") {
|
||||||
|
return Promise.reject(new Error(
|
||||||
|
`paymentFormData is empty (payScene=${prepareParams.payScene}, check business payment api response)`
|
||||||
|
));
|
||||||
|
}
|
||||||
|
const amount = resolvePaymentAmount(opts, rawResult, paymentFormData);
|
||||||
|
const currency = opts.currency;
|
||||||
|
return invokePaymentFormData(paymentFormData, normalizedPaymentType, amount, currency);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => buildPaymentFailureResult(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
100
src/services/payment/nativeQrView.js
Normal file
100
src/services/payment/nativeQrView.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import QRCode from "qrcode";
|
||||||
|
import { getConfig } from "../config";
|
||||||
|
import { createPaymentDialogHost } from "./paymentDialogHost";
|
||||||
|
import { appendPaymentAmount } from "./paymentAmount";
|
||||||
|
import { hasEmbedPaymentDialogTarget, delegatePaymentDialog } from "./embedPaymentDialog";
|
||||||
|
|
||||||
|
const OVERLAY_ID = "__iscan_payment_native_qr__";
|
||||||
|
const dialogHost = createPaymentDialogHost(OVERLAY_ID);
|
||||||
|
|
||||||
|
function getQrSize() {
|
||||||
|
const size = getConfig("paymentNativeQrSize");
|
||||||
|
if (typeof size === "number" && size > 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
return 220;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQrToCanvas(canvas, codeUrl) {
|
||||||
|
const size = getQrSize();
|
||||||
|
return QRCode.toCanvas(canvas, codeUrl, {
|
||||||
|
width: size,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: "#000000",
|
||||||
|
light: "#ffffff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closePaymentNativeQrCode() {
|
||||||
|
dialogHost.removeOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showPaymentNativeQrCodeLocal(codeUrl, amount, currency) {
|
||||||
|
if (!codeUrl) {
|
||||||
|
return Promise.reject(new Error("codeUrl is required"));
|
||||||
|
}
|
||||||
|
if (getConfig("paymentNativeQrEnabled") === false) {
|
||||||
|
return Promise.resolve({ shown: false, codeUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas;
|
||||||
|
try {
|
||||||
|
dialogHost.mountOverlay({
|
||||||
|
overlayClass: getConfig("paymentNativeQrOverlayClass"),
|
||||||
|
panelClass: getConfig("paymentNativeQrPanelClass"),
|
||||||
|
overlayStyle: getConfig("paymentNativeQrOverlayStyle") || undefined,
|
||||||
|
panelStyle: getConfig("paymentNativeQrPanelStyle")
|
||||||
|
|| "position:relative;width:100%;max-width:320px;background:#fff;border-radius:12px;padding:24px 20px 20px;box-shadow:0 8px 28px rgba(0,0,0,0.22);text-align:center;",
|
||||||
|
closeButtonStyle: getConfig("paymentNativeQrCloseButtonStyle") || undefined,
|
||||||
|
fillPanel(panel) {
|
||||||
|
const doc = panel.ownerDocument;
|
||||||
|
|
||||||
|
const title = doc.createElement("h3");
|
||||||
|
title.textContent = getConfig("paymentNativeQrTitle") || "微信扫码支付";
|
||||||
|
title.style.cssText = "margin:0 0 8px;font-size:18px;font-weight:600;color:#222;line-height:1.35;";
|
||||||
|
|
||||||
|
const message = doc.createElement("p");
|
||||||
|
message.textContent = getConfig("paymentNativeQrMessage") || "请使用微信扫一扫,扫描二维码完成支付";
|
||||||
|
message.style.cssText = "margin:0 0 16px;font-size:14px;color:#666;line-height:1.5;";
|
||||||
|
|
||||||
|
const qrWrap = doc.createElement("div");
|
||||||
|
qrWrap.style.cssText = "display:flex;align-items:center;justify-content:center;padding:8px;background:#fff;border:1px solid #eee;border-radius:8px;";
|
||||||
|
|
||||||
|
canvas = doc.createElement("canvas");
|
||||||
|
qrWrap.appendChild(canvas);
|
||||||
|
|
||||||
|
const hint = doc.createElement("p");
|
||||||
|
hint.textContent = getConfig("paymentNativeQrHint") || "支付完成后请稍候,系统将自动确认结果";
|
||||||
|
hint.style.cssText = "margin:14px 0 0;font-size:12px;color:#999;line-height:1.45;";
|
||||||
|
|
||||||
|
panel.appendChild(title);
|
||||||
|
appendPaymentAmount(panel, doc, amount, currency);
|
||||||
|
panel.appendChild(message);
|
||||||
|
panel.appendChild(qrWrap);
|
||||||
|
panel.appendChild(hint);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderQrToCanvas(canvas, codeUrl).then(() => ({
|
||||||
|
shown: true,
|
||||||
|
codeUrl,
|
||||||
|
})).catch((err) => {
|
||||||
|
dialogHost.removeOverlay();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showPaymentNativeQrCode(codeUrl, amount, currency) {
|
||||||
|
if (hasEmbedPaymentDialogTarget()) {
|
||||||
|
const delegated = delegatePaymentDialog("showPaymentNativeQr", { codeUrl, amount, currency });
|
||||||
|
if (delegated) {
|
||||||
|
return delegated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showPaymentNativeQrCodeLocal(codeUrl, amount, currency);
|
||||||
|
}
|
||||||
299
src/services/payment/oauth.js
Normal file
299
src/services/payment/oauth.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { getConfig } from "../config";
|
||||||
|
import { request } from "../../utils/request";
|
||||||
|
import { toAny } from "../../utils/toany";
|
||||||
|
import { getTopWindow } from "./env";
|
||||||
|
import { getPaymentConfig } from "./config";
|
||||||
|
|
||||||
|
const DEFAULT_STORAGE_KEY_PREFIX = "iscan_wechat_openid";
|
||||||
|
|
||||||
|
let resolvedOAuthAppId = "";
|
||||||
|
|
||||||
|
function getOAuthConfig() {
|
||||||
|
return getPaymentConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setResolvedOAuthAppId(appId) {
|
||||||
|
if (appId) {
|
||||||
|
resolvedOAuthAppId = String(appId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResolvedOAuthAppId() {
|
||||||
|
return resolvedOAuthAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStorageKey(appId) {
|
||||||
|
const cfg = getOAuthConfig();
|
||||||
|
if (cfg.storageKey) {
|
||||||
|
return cfg.storageKey;
|
||||||
|
}
|
||||||
|
const id = appId || getResolvedOAuthAppId();
|
||||||
|
if (id) {
|
||||||
|
return `${DEFAULT_STORAGE_KEY_PREFIX}_${id}`;
|
||||||
|
}
|
||||||
|
return DEFAULT_STORAGE_KEY_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionStorage() {
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return topWin.sessionStorage;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCachedOpenid(appId) {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return storage.getItem(buildStorageKey(appId)) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCachedOpenid(appId, openid) {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage || !openid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storage.setItem(buildStorageKey(appId), openid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCachedOpenid(appId) {
|
||||||
|
const storage = getSessionStorage();
|
||||||
|
if (!storage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = buildStorageKey(appId);
|
||||||
|
if (key) {
|
||||||
|
storage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseWechatOAuth(appId) {
|
||||||
|
clearCachedOpenid(appId || getResolvedOAuthAppId());
|
||||||
|
if (!appId || appId === getResolvedOAuthAppId()) {
|
||||||
|
resolvedOAuthAppId = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodeFromLocation(win) {
|
||||||
|
if (!win || !win.location) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(win.location.search || "");
|
||||||
|
return params.get("code") || "";
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripOAuthQueryFromUrl(win) {
|
||||||
|
if (!win || !win.history || !win.location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(win.location.href);
|
||||||
|
const keys = ["code", "state", "error", "error_description"];
|
||||||
|
let changed = false;
|
||||||
|
keys.forEach((key) => {
|
||||||
|
if (url.searchParams.has(key)) {
|
||||||
|
url.searchParams.delete(key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = url.pathname + url.search + url.hash;
|
||||||
|
win.history.replaceState(win.history.state, "", next);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOAuthErrorFromLocation(win) {
|
||||||
|
if (!win || !win.location) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(win.location.search || "");
|
||||||
|
const error = params.get("error");
|
||||||
|
if (!error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return params.get("error_description") || error;
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOAuthResponse(res) {
|
||||||
|
const body = toAny(res && res.data, {});
|
||||||
|
if (!body) {
|
||||||
|
throw new Error("wechat oauth response is empty");
|
||||||
|
}
|
||||||
|
if (body.code !== undefined && body.code !== 0) {
|
||||||
|
throw new Error(body.msg || body.message || "wechat oauth failed");
|
||||||
|
}
|
||||||
|
const data = body.data != null ? body.data : body;
|
||||||
|
const appId = data && data.appId;
|
||||||
|
const openid = data && data.openid;
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("wechat oauth appId is empty");
|
||||||
|
}
|
||||||
|
if (!openid) {
|
||||||
|
throw new Error("wechat oauth openid is empty");
|
||||||
|
}
|
||||||
|
setResolvedOAuthAppId(appId);
|
||||||
|
return {
|
||||||
|
appId: String(appId),
|
||||||
|
openid: String(openid),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAuthorizeUrlResponse(res) {
|
||||||
|
const body = toAny(res && res.data, {});
|
||||||
|
if (!body) {
|
||||||
|
throw new Error("wechat oauth authorize url response is empty");
|
||||||
|
}
|
||||||
|
if (body.code !== undefined && body.code !== 0) {
|
||||||
|
throw new Error(body.msg || body.message || "wechat oauth authorize url failed");
|
||||||
|
}
|
||||||
|
const data = body.data != null ? body.data : body;
|
||||||
|
const appId = data && data.appId;
|
||||||
|
const authorizeUrl = data && (data.authorizeUrl || data.authorize_url);
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("wechat oauth authorizeUrl appId is empty");
|
||||||
|
}
|
||||||
|
if (!authorizeUrl) {
|
||||||
|
throw new Error("wechat oauth authorizeUrl is empty");
|
||||||
|
}
|
||||||
|
setResolvedOAuthAppId(appId);
|
||||||
|
return {
|
||||||
|
appId: String(appId),
|
||||||
|
authorizeUrl: String(authorizeUrl),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultRedirectUri(win) {
|
||||||
|
if (!win || !win.location) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(win.location.href);
|
||||||
|
url.searchParams.delete("code");
|
||||||
|
url.searchParams.delete("state");
|
||||||
|
url.searchParams.delete("error");
|
||||||
|
url.searchParams.delete("error_description");
|
||||||
|
url.hash = "";
|
||||||
|
return url.href.split("#")[0];
|
||||||
|
} catch (e) {
|
||||||
|
return win.location.href.split("#")[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRedirectUri(options, win) {
|
||||||
|
const opts = toAny(options, {});
|
||||||
|
const cfg = getOAuthConfig();
|
||||||
|
if (opts.redirectUri) {
|
||||||
|
return String(opts.redirectUri).split("#")[0];
|
||||||
|
}
|
||||||
|
if (cfg.redirectUri) {
|
||||||
|
return String(cfg.redirectUri).split("#")[0];
|
||||||
|
}
|
||||||
|
return buildDefaultRedirectUri(win);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exchangeCodeForOpenid(code) {
|
||||||
|
const cfg = getOAuthConfig();
|
||||||
|
return request({
|
||||||
|
url: cfg.oauthApiUrl,
|
||||||
|
method: "POST",
|
||||||
|
json: true,
|
||||||
|
data: { code },
|
||||||
|
}).then(parseOAuthResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchAuthorizeInfo(redirectUri) {
|
||||||
|
const cfg = getOAuthConfig();
|
||||||
|
if (!redirectUri) {
|
||||||
|
return Promise.reject(new Error("redirectUri is required for wechat oauth authorize url"));
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: cfg.oauthAuthorizeUrlApiUrl,
|
||||||
|
method: "POST",
|
||||||
|
json: true,
|
||||||
|
data: { redirectUri },
|
||||||
|
}).then(parseAuthorizeUrlResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToAuthorize(authorizeUrl) {
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
topWin.location.href = authorizeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectWechatOAuth(topWin, appId, error) {
|
||||||
|
releaseWechatOAuth(appId);
|
||||||
|
if (topWin) {
|
||||||
|
stripOAuthQueryFromUrl(topWin);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付 JSAPI:先拉取授权信息获取 appId,再查缓存 / 换 code / 跳转授权。
|
||||||
|
* 需要跳转授权时返回 null;失败时释放 openid 缓存并 reject。
|
||||||
|
*/
|
||||||
|
export function ensureWechatOpenid(options) {
|
||||||
|
const opts = toAny(options, {});
|
||||||
|
const topWin = getTopWindow();
|
||||||
|
if (!topWin) {
|
||||||
|
return Promise.reject(new Error("window is not available"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthError = getOAuthErrorFromLocation(topWin);
|
||||||
|
if (oauthError) {
|
||||||
|
return rejectWechatOAuth(topWin, getResolvedOAuthAppId(), new Error(`wechat oauth failed: ${oauthError}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUri = resolveRedirectUri(opts, topWin);
|
||||||
|
const code = getCodeFromLocation(topWin);
|
||||||
|
|
||||||
|
return fetchAuthorizeInfo(redirectUri).then((authInfo) => {
|
||||||
|
const appId = authInfo.appId;
|
||||||
|
|
||||||
|
if (!opts.force) {
|
||||||
|
const cached = readCachedOpenid(appId);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
return exchangeCodeForOpenid(code).then((result) => {
|
||||||
|
writeCachedOpenid(result.appId, result.openid);
|
||||||
|
stripOAuthQueryFromUrl(topWin);
|
||||||
|
return result.openid;
|
||||||
|
}).catch((err) => rejectWechatOAuth(topWin, appId, err));
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectToAuthorize(authInfo.authorizeUrl);
|
||||||
|
return null;
|
||||||
|
}).catch((err) => rejectWechatOAuth(topWin, getResolvedOAuthAppId(), err));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearWechatOpenidCache(appId) {
|
||||||
|
clearCachedOpenid(appId || getResolvedOAuthAppId());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedWechatOpenid(appId) {
|
||||||
|
return readCachedOpenid(appId || getResolvedOAuthAppId());
|
||||||
|
}
|
||||||
67
src/services/payment/paymentAmount.js
Normal file
67
src/services/payment/paymentAmount.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { toAny } from "../../utils/toany";
|
||||||
|
|
||||||
|
export function formatPaymentAmountDisplay(amount, currency) {
|
||||||
|
if (amount == null || amount === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof amount === "string" && /[¥¥$]/.test(amount)) {
|
||||||
|
return amount.trim();
|
||||||
|
}
|
||||||
|
const num = Number(amount);
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return String(amount);
|
||||||
|
}
|
||||||
|
const symbol = !currency || currency === "CNY" ? "¥" : String(currency);
|
||||||
|
return `${symbol}${num.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePaymentAmount(options, rawResult, paymentFormData) {
|
||||||
|
const opts = toAny(options, {});
|
||||||
|
if (opts.amount != null && opts.amount !== "") {
|
||||||
|
return opts.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = toAny(rawResult, {});
|
||||||
|
if (raw.amount != null && raw.amount !== "") {
|
||||||
|
return raw.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = raw.data != null ? toAny(raw.data, {}) : {};
|
||||||
|
if (data.amount != null && data.amount !== "") {
|
||||||
|
return data.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData = paymentFormData;
|
||||||
|
if (typeof formData === "string") {
|
||||||
|
try {
|
||||||
|
formData = JSON.parse(formData);
|
||||||
|
} catch (e) {
|
||||||
|
formData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (formData && typeof formData === "object") {
|
||||||
|
const form = toAny(formData, {});
|
||||||
|
if (form.amount != null && form.amount !== "") {
|
||||||
|
return form.amount;
|
||||||
|
}
|
||||||
|
if (form.totalAmount != null && form.totalAmount !== "") {
|
||||||
|
return form.totalAmount;
|
||||||
|
}
|
||||||
|
if (form.payAmount != null && form.payAmount !== "") {
|
||||||
|
return form.payAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendPaymentAmount(panel, doc, amount, currency) {
|
||||||
|
const text = formatPaymentAmountDisplay(amount, currency);
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const amountEl = doc.createElement("p");
|
||||||
|
amountEl.textContent = text;
|
||||||
|
amountEl.style.cssText = "margin:0 0 12px;font-size:28px;font-weight:700;color:#e64340;line-height:1.2;text-align:center;";
|
||||||
|
panel.appendChild(amountEl);
|
||||||
|
}
|
||||||
18
src/services/payment/paymentDialogChild.js
Normal file
18
src/services/payment/paymentDialogChild.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { showPaymentNativeQrCodeLocal, closePaymentNativeQrCode } from "./nativeQrView";
|
||||||
|
import { showAlipayPaymentFormLocal, closeAlipayPaymentForm } from "./alipayFormView";
|
||||||
|
|
||||||
|
export function runPaymentDialogChildAction(action, payload) {
|
||||||
|
const data = payload || {};
|
||||||
|
if (action === "showPaymentNativeQr") {
|
||||||
|
return showPaymentNativeQrCodeLocal(data.codeUrl, data.amount, data.currency);
|
||||||
|
}
|
||||||
|
if (action === "showAlipayPaymentForm") {
|
||||||
|
return showAlipayPaymentFormLocal(data.html, data.amount, data.currency);
|
||||||
|
}
|
||||||
|
if (action === "closePaymentDialog") {
|
||||||
|
closePaymentNativeQrCode();
|
||||||
|
closeAlipayPaymentForm();
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`unknown payment dialog action: ${action}`));
|
||||||
|
}
|
||||||
95
src/services/payment/paymentDialogHost.js
Normal file
95
src/services/payment/paymentDialogHost.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export const PAYMENT_CLOSE_ICON_SVG = "<svg viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M566.97558594 521.09667969L856.8828125 231.18945312c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355468l-1.58203125-1.58203125c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0L512 466.51660156 222.09277344 176.21386719c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0l-1.58203125 1.58203125c-15.02929688 14.63378906-15.02929688 38.75976563 0 53.39355469l289.90722656 289.90722656L167.1171875 811.00390625c-14.63378906 14.63378906-14.63378906 38.75976563 0 53.39355469l1.58203125 1.58203125c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0L512 576.07226563 801.90722656 865.97949219c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0l1.58203125-1.58203125c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355469L566.97558594 521.09667969z\" fill=\"currentColor\"/></svg>";
|
||||||
|
|
||||||
|
const DEFAULT_OVERLAY_STYLE = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:100002;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;";
|
||||||
|
const DEFAULT_CLOSE_BUTTON_STYLE = "position:absolute;top:10px;right:10px;width:32px;height:32px;padding:0;border:0;border-radius:50%;background:rgba(0,0,0,0.06);color:#666;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;";
|
||||||
|
|
||||||
|
const overlayCloseHandlers = Object.create(null);
|
||||||
|
|
||||||
|
function getLocalDocument() {
|
||||||
|
if (typeof window === "undefined" || !window.document) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return window.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPaymentDialogHost(overlayId) {
|
||||||
|
function getDocument() {
|
||||||
|
return getLocalDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOverlay() {
|
||||||
|
const doc = getDocument();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const overlay = doc.getElementById(overlayId);
|
||||||
|
if (overlay && overlay.parentNode) {
|
||||||
|
overlay.parentNode.removeChild(overlay);
|
||||||
|
}
|
||||||
|
const closeHandler = overlayCloseHandlers[overlayId];
|
||||||
|
if (closeHandler && doc.removeEventListener) {
|
||||||
|
doc.removeEventListener("keydown", closeHandler);
|
||||||
|
}
|
||||||
|
overlayCloseHandlers[overlayId] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountOverlay(options) {
|
||||||
|
const doc = getDocument();
|
||||||
|
if (!doc || !doc.body) {
|
||||||
|
throw new Error("payment dialog document is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOverlay();
|
||||||
|
|
||||||
|
const overlay = doc.createElement("div");
|
||||||
|
overlay.id = overlayId;
|
||||||
|
overlay.style.cssText = options.overlayStyle || DEFAULT_OVERLAY_STYLE;
|
||||||
|
if (options.overlayClass) {
|
||||||
|
overlay.className = options.overlayClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = doc.createElement("div");
|
||||||
|
panel.style.cssText = options.panelStyle || "";
|
||||||
|
if (options.panelClass) {
|
||||||
|
panel.className = options.panelClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBtn = doc.createElement("button");
|
||||||
|
closeBtn.type = "button";
|
||||||
|
closeBtn.setAttribute("aria-label", "关闭");
|
||||||
|
closeBtn.style.cssText = options.closeButtonStyle || DEFAULT_CLOSE_BUTTON_STYLE;
|
||||||
|
closeBtn.innerHTML = PAYMENT_CLOSE_ICON_SVG;
|
||||||
|
closeBtn.onclick = (event) => {
|
||||||
|
event.preventDefault && event.preventDefault();
|
||||||
|
removeOverlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.appendChild(closeBtn);
|
||||||
|
if (typeof options.fillPanel === "function") {
|
||||||
|
options.fillPanel(panel, closeBtn);
|
||||||
|
}
|
||||||
|
overlay.appendChild(panel);
|
||||||
|
doc.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const closeHandler = (event) => {
|
||||||
|
if (event && event.key === "Escape") {
|
||||||
|
removeOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
overlayCloseHandlers[overlayId] = closeHandler;
|
||||||
|
doc.addEventListener("keydown", closeHandler);
|
||||||
|
|
||||||
|
overlay.onclick = (event) => {
|
||||||
|
if (event.target === overlay) {
|
||||||
|
removeOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { doc, overlay, panel, closeBtn };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
removeOverlay,
|
||||||
|
mountOverlay,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,28 +1,165 @@
|
|||||||
import { inRuntime, bridgeAsync } from "../bridge";
|
import { inRuntime, bridgeAsync } from "../bridge";
|
||||||
import { isSupportWebScan, startScanForWeb, stopScanForWeb, isSupportImageScan, startScanForImage, unlockScanBeep } from "../web";
|
import {
|
||||||
import { isSupportWxScan, startScanForWx } from "../wx";
|
isSupportWebScan,
|
||||||
|
startScanForWeb,
|
||||||
|
stopScanForWeb,
|
||||||
|
isSupportImageScan,
|
||||||
|
startScanForImage,
|
||||||
|
unlockScanBeep,
|
||||||
|
playScanBeep,
|
||||||
|
isWebScanImageFallbackEnabled,
|
||||||
|
canUseWebCameraScan,
|
||||||
|
shouldSkipWebCameraProbe,
|
||||||
|
cleanupWebScanResiduals,
|
||||||
|
detectImageFileForScan
|
||||||
|
} from "../web";
|
||||||
|
import { isSupportWxScan, startScanForWx, isWxEnv } from "../wx";
|
||||||
import { startScanner, stopScanner } from "../scanner";
|
import { startScanner, stopScanner } from "../scanner";
|
||||||
import { getConfig } from "../config";
|
import { getConfig } from "../config";
|
||||||
import { toAny } from "../../utils/toany";
|
import { toAny } from "../../utils/toany";
|
||||||
import { printDebug } from "../../utils/logger";
|
import { printDebug, printWarn } from "../../utils/logger";
|
||||||
|
import { waitForSdkConfigReady } from "../sdkLifecycle";
|
||||||
|
import { forwardEmbedScanResultIfNeeded, forwardEmbedScanErrorIfNeeded } from "../embedScanBridge";
|
||||||
|
|
||||||
let _scan_status = "ready";
|
let _scan_status = "ready";
|
||||||
let _scan_status_listener = null;
|
let _scan_status_listener = null;
|
||||||
let _scan_listener_list = [];
|
let _scan_listener_list = [];
|
||||||
|
let _scan_error_listener_list = [];
|
||||||
let _scan_resolve = null;
|
let _scan_resolve = null;
|
||||||
let _scan_closing = false;
|
let _scan_closing = false;
|
||||||
let _scan_next_start_time = 0;
|
let _scan_next_start_time = 0;
|
||||||
|
let _scan_session_key = null;
|
||||||
|
let _embed_scan_host_enabled = false;
|
||||||
|
|
||||||
const SCAN_RESTART_DELAY = 500;
|
const SCAN_RESTART_DELAY = 500;
|
||||||
|
const BRIDGE_SCAN_TIMEOUT = 5000;
|
||||||
|
const SCAN_SESSION_TIMEOUT = 90000;
|
||||||
|
|
||||||
function getScanRestartDelay() {
|
function getScanRestartDelay() {
|
||||||
return toAny(getConfig("scanRestartDelay"), SCAN_RESTART_DELAY);
|
return toAny(getConfig("scanRestartDelay"), SCAN_RESTART_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBridgeScanTimeout() {
|
||||||
|
const timeout = getConfig("bridgeScanTimeout");
|
||||||
|
return typeof timeout === "number" && timeout > 0 ? timeout : BRIDGE_SCAN_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScanSessionTimeout() {
|
||||||
|
const timeout = getConfig("scanSessionTimeout");
|
||||||
|
return typeof timeout === "number" && timeout > 0 ? timeout : SCAN_SESSION_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withScanSessionTimeout(scanPromise) {
|
||||||
|
return Promise.race([
|
||||||
|
scanPromise,
|
||||||
|
new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
printWarn("scan session timeout");
|
||||||
|
resolve({ cancel: 1, scanTimeout: true });
|
||||||
|
}, getScanSessionTimeout());
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __fallbackScanAfterBridgeFailure(err) {
|
||||||
|
if (!isScanning()) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
printWarn("bridge scan unavailable, fallback:", err);
|
||||||
|
return __startNonBridgeScan(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __fallbackScanAfterWxFailure(err) {
|
||||||
|
if (!isScanning()) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (__isScanCancel(err)) {
|
||||||
|
return { cancel: 1 };
|
||||||
|
}
|
||||||
|
printWarn("wx scan failed:", err);
|
||||||
|
__scanError(err, { source: "wx" });
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __startNonBridgeScan(err) {
|
||||||
|
if (isWxEnv()) {
|
||||||
|
return __startWxScan();
|
||||||
|
}
|
||||||
|
if (isSupportWebScan()) {
|
||||||
|
return __startWebScan();
|
||||||
|
}
|
||||||
|
if (isSupportImageScan()) {
|
||||||
|
return __startImageScan();
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBarcodeString(input) {
|
||||||
|
// 标准化的类型映射表:将各种变体映射到统一标识
|
||||||
|
// 这样即使传入 EAN_13、EAN-13、EAN13 都能匹配成功
|
||||||
|
const normalizedTypeMap = {
|
||||||
|
// EAN 系列
|
||||||
|
"ean13": true, "ean-13": true, "ean_13": true,
|
||||||
|
"ean8": true, "ean-8": true, "ean_8": true,
|
||||||
|
// UPC 系列
|
||||||
|
"upca": true, "upc-a": true, "upc_a": true,
|
||||||
|
"upce": true, "upc-e": true, "upc_e": true,
|
||||||
|
// CODE 系列
|
||||||
|
"code128": true, "code-128": true, "code_128": true,
|
||||||
|
"code39": true, "code-39": true, "code_39": true,
|
||||||
|
"code93": true, "code-93": true, "code_93": true,
|
||||||
|
// 其他常见类型
|
||||||
|
"itf": true, "itf14": true, "itf-14": true, "itf_14": true,
|
||||||
|
"codabar": true,
|
||||||
|
"pdf417": true, "pdf-417": true, "pdf_417": true,
|
||||||
|
"qrcode": true, "qr-code": true, "qr_code": true,
|
||||||
|
"datamatrix": true, "data-matrix": true, "data_matrix": true,
|
||||||
|
"aztec": true
|
||||||
|
};
|
||||||
|
// 健壮性检查
|
||||||
|
if (typeof input !== 'string') {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
// 按第一个逗号分割
|
||||||
|
const commaIndex = input.indexOf(',');
|
||||||
|
if (commaIndex === -1) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
const possibleType = input.substring(0, commaIndex).trim();
|
||||||
|
const value = input.substring(commaIndex + 1).trim();
|
||||||
|
// 类型或值为空,返回原值
|
||||||
|
if (possibleType === "" || value === "") {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
// 标准化类型(转小写,便于匹配)
|
||||||
|
const normalizedType = possibleType.toLowerCase();
|
||||||
|
// 检查是否为已知的条形码类型(支持多种分隔符变体)
|
||||||
|
const isKnownType = normalizedTypeMap.hasOwnProperty(normalizedType);
|
||||||
|
|
||||||
|
if (isKnownType) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __needsHardwareScanner() {
|
||||||
|
if (_embed_scan_host_enabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < _scan_listener_list.length; i++) {
|
||||||
|
if (!_scan_listener_list[i].selfKeyOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function __checkScanner() {
|
function __checkScanner() {
|
||||||
if (_scan_listener_list.length > 0) {
|
if (__needsHardwareScanner()) {
|
||||||
startScanner((result) => {
|
startScanner((result) => {
|
||||||
__scannerResult(result);
|
result = parseBarcodeString(result);
|
||||||
|
__scannerResult(result, { source: "scanner", skipBeep: true });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
stopScanner();
|
stopScanner();
|
||||||
@@ -40,33 +177,161 @@ function __match(result, match) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __resolveScanKey(meta) {
|
||||||
|
if (meta && meta.scanKey === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (meta && typeof meta.scanKey === "string" && meta.scanKey) {
|
||||||
|
return meta.scanKey;
|
||||||
|
}
|
||||||
|
if (meta && meta.source === "scanner") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _scan_session_key;
|
||||||
|
}
|
||||||
|
|
||||||
function __result(result) {
|
function __listenerMatches(item, result, scanKey, meta) {
|
||||||
|
if (item.selfKeyOnly) {
|
||||||
|
if (meta && meta.source === "scanner") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scanKey || item.key !== scanKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (scanKey && item.key !== scanKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return __match(result, item.match);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __shouldSkipBeep(meta) {
|
||||||
|
if (meta && meta.skipBeep) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const source = meta && meta.source;
|
||||||
|
return source === "scanner" || source === "bridge" || source === "wx";
|
||||||
|
}
|
||||||
|
|
||||||
|
function __isScanCancel(raw) {
|
||||||
|
if (!raw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (raw.cancel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const message = normalizeScanError(raw);
|
||||||
|
if (!message) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const lowerMessage = message.toLowerCase();
|
||||||
|
return lowerMessage.indexOf("cancel") !== -1
|
||||||
|
|| message.indexOf("取消") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __result(result, meta) {
|
||||||
|
result = parseBarcodeString(result);
|
||||||
|
const scanKey = __resolveScanKey(meta);
|
||||||
|
forwardEmbedScanResultIfNeeded(result, Object.assign({}, meta, { scanKey }));
|
||||||
let matched = false;
|
let matched = false;
|
||||||
for (let i = 0; i < _scan_listener_list.length; i++) {
|
for (let i = 0; i < _scan_listener_list.length; i++) {
|
||||||
const item = _scan_listener_list[i];
|
const item = _scan_listener_list[i];
|
||||||
if (item.listener && __match(result, item.match)) {
|
if (item.listener && __listenerMatches(item, result, scanKey, meta)) {
|
||||||
matched = true;
|
matched = true;
|
||||||
item.listener({ result, key: item.key });
|
item.listener({ result, key: item.key });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matched) {
|
if (matched) {
|
||||||
|
if (!__shouldSkipBeep(meta)) {
|
||||||
|
playScanBeep();
|
||||||
|
}
|
||||||
_scan_next_start_time = Date.now() + getScanRestartDelay();
|
_scan_next_start_time = Date.now() + getScanRestartDelay();
|
||||||
}
|
}
|
||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
function __hasMatchedListener(result) {
|
function normalizeScanError(raw) {
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (raw && raw.error != null && raw.error !== "") {
|
||||||
|
return String(raw.error);
|
||||||
|
}
|
||||||
|
if (raw && raw.message) {
|
||||||
|
return String(raw.message);
|
||||||
|
}
|
||||||
|
if (raw == null || raw === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __scanError(raw, meta) {
|
||||||
|
const error = normalizeScanError(raw);
|
||||||
|
if (!error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scanKey = __resolveScanKey(meta);
|
||||||
|
forwardEmbedScanErrorIfNeeded(error, Object.assign({}, meta, { scanKey }));
|
||||||
|
let matched = false;
|
||||||
|
for (let i = 0; i < _scan_error_listener_list.length; i++) {
|
||||||
|
const item = _scan_error_listener_list[i];
|
||||||
|
if (item.listener && __listenerMatches(item, error, scanKey)) {
|
||||||
|
matched = true;
|
||||||
|
item.listener(Object.assign({
|
||||||
|
error,
|
||||||
|
key: item.key
|
||||||
|
}, meta || {}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __notifyImageScanFailure(raw, meta) {
|
||||||
|
if (raw && raw.cancel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const error = normalizeScanError(raw);
|
||||||
|
const notified = __scanError(raw, Object.assign({ source: "image" }, meta || {}));
|
||||||
|
if (!notified && error) {
|
||||||
|
printWarn("image scan failed:", error);
|
||||||
|
}
|
||||||
|
return notified;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __notifyWebScanFailure(raw, meta) {
|
||||||
|
if (raw && raw.cancel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const payload = raw && raw.error != null ? raw.error : raw;
|
||||||
|
const error = normalizeScanError(payload);
|
||||||
|
const notified = __scanError(payload, Object.assign({ source: "web" }, meta || {}));
|
||||||
|
if (!notified && error) {
|
||||||
|
printWarn("web scan failed:", error);
|
||||||
|
}
|
||||||
|
return notified;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __hasMatchedListener(result, meta) {
|
||||||
|
const scanKey = __resolveScanKey(meta);
|
||||||
for (let i = 0; i < _scan_listener_list.length; i++) {
|
for (let i = 0; i < _scan_listener_list.length; i++) {
|
||||||
const item = _scan_listener_list[i];
|
const item = _scan_listener_list[i];
|
||||||
if (item.listener && __match(result, item.match)) {
|
if (item.listener && __listenerMatches(item, result, scanKey, meta)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __clearScanSessionKey() {
|
||||||
|
_scan_session_key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __beginScanSession(key) {
|
||||||
|
_scan_session_key = typeof key === "string" && key ? key : null;
|
||||||
|
}
|
||||||
|
|
||||||
function __scanning() {
|
function __scanning() {
|
||||||
if (_scan_status !== "scanning") {
|
if (_scan_status !== "scanning") {
|
||||||
_scan_status = "scanning";
|
_scan_status = "scanning";
|
||||||
@@ -105,10 +370,49 @@ function __stopCurrentScan() {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
function __scannerResult(result) {
|
/**
|
||||||
if (!__hasMatchedListener(result)) {
|
* 父页通过 postMessage 将识别结果投递到嵌入 iframe 时调用(与本地扫码枪/监听同一链路)。
|
||||||
|
* @returns {boolean} 是否有监听消费了该结果
|
||||||
|
*/
|
||||||
|
export function dispatchEmbedScanResult(raw, meta) {
|
||||||
|
const result =
|
||||||
|
typeof raw === "string" ? parseBarcodeString(raw) : raw;
|
||||||
|
return __scannerResult(result, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 父页通过 postMessage 将识别错误投递到嵌入 iframe 时调用。
|
||||||
|
* @returns {boolean} 是否有监听消费了该错误
|
||||||
|
*/
|
||||||
|
export function dispatchEmbedScanError(raw, meta) {
|
||||||
|
const error = normalizeScanError(raw);
|
||||||
|
return __scanError(error, Object.assign({ source: "image" }, meta || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 嵌入 iframe 已消费扫码结果时通知父页结束当前识别(关闭摄像头/UI 等)。
|
||||||
|
*/
|
||||||
|
export function acknowledgeEmbedScanConsumed(raw) {
|
||||||
|
const result =
|
||||||
|
typeof raw === "string" ? parseBarcodeString(raw) : raw;
|
||||||
|
if (!isScanning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const resolve = __finishScan();
|
||||||
|
_scan_closing = true;
|
||||||
|
_scan_next_start_time = Date.now() + getScanRestartDelay();
|
||||||
|
__stopCurrentScan().then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
_scan_closing = false;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
resolve && resolve({ result });
|
||||||
|
}
|
||||||
|
|
||||||
|
function __scannerResult(result, meta) {
|
||||||
|
if (!__hasMatchedListener(result, meta)) {
|
||||||
|
return __result(result, meta);
|
||||||
|
}
|
||||||
if (isScanning()) {
|
if (isScanning()) {
|
||||||
const resolve = __finishScan();
|
const resolve = __finishScan();
|
||||||
_scan_closing = true;
|
_scan_closing = true;
|
||||||
@@ -117,26 +421,26 @@ function __scannerResult(result) {
|
|||||||
_scan_closing = false;
|
_scan_closing = false;
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
__result(result);
|
const matched = __result(result, meta);
|
||||||
resolve && resolve({
|
resolve && resolve({
|
||||||
result
|
result
|
||||||
});
|
});
|
||||||
return;
|
return matched;
|
||||||
}
|
}
|
||||||
__result(result);
|
return __result(result, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __startBridgeScan() {
|
function __startBridgeScan() {
|
||||||
return bridgeAsync("startScan", {
|
return bridgeAsync("startScan", {
|
||||||
closeable: true
|
closeable: true
|
||||||
}).then(resp => {
|
}, getBridgeScanTimeout()).then(resp => {
|
||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if (!resp || !resp.result) {
|
if (!resp || !resp.result) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if (__result(resp.result)) {
|
if (__result(resp.result, { source: "bridge" })) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if (isScanning()) {
|
if (isScanning()) {
|
||||||
@@ -145,12 +449,12 @@ function __startBridgeScan() {
|
|||||||
return resp;
|
return resp;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return err;
|
throw err;
|
||||||
}
|
}
|
||||||
if (!err || !err.result) {
|
if (!err || !err.result) {
|
||||||
return err;
|
throw err;
|
||||||
}
|
}
|
||||||
if (__result(err.result)) {
|
if (__result(err.result, { source: "bridge" })) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (isScanning()) {
|
if (isScanning()) {
|
||||||
@@ -168,29 +472,68 @@ function __startWxScan() {
|
|||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
if (__isScanCancel(resp)) {
|
||||||
|
return { cancel: 1 };
|
||||||
|
}
|
||||||
|
if (resp && resp.error && !resp.result) {
|
||||||
|
throw resp.error;
|
||||||
|
}
|
||||||
if (!resp || !resp.result) {
|
if (!resp || !resp.result) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if (__result(resp.result)) {
|
if (__result(resp.result, { source: "wx" })) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if (isScanning()) {
|
return resp;
|
||||||
return __startWxScan();
|
}).catch(err => {
|
||||||
|
if (!isScanning()) {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
if (err && err.result) {
|
||||||
|
if (__result(err.result, { source: "wx" })) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
if (__isScanCancel(err)) {
|
||||||
|
return { cancel: 1 };
|
||||||
|
}
|
||||||
|
return __fallbackScanAfterWxFailure(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function __startWebScan(useImageScan = false) {
|
||||||
|
return startScanForWeb(__result, __scanError).then(resp => {
|
||||||
|
if (!isScanning()) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
if (!resp || !resp.result) {
|
||||||
|
if (resp && resp.success === false) {
|
||||||
|
__notifyWebScanFailure(resp);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
__result(resp.result, { source: "web" });
|
||||||
return resp;
|
return resp;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (!err || !err.result) {
|
if (err && err.cancel) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (__result(err.result)) {
|
if (err && err.scanTimeout) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (isScanning()) {
|
if (err && err.imageFallbackUsed) {
|
||||||
return __startWxScan();
|
__notifyWebScanFailure(err);
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
if (isWebScanImageFallbackEnabled() && useImageScan) {
|
||||||
|
return __startImageScan();
|
||||||
|
}
|
||||||
|
__notifyWebScanFailure(err);
|
||||||
|
printWarn("web scan failed:", err);
|
||||||
return err;
|
return err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -200,29 +543,27 @@ function __startImageScan() {
|
|||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
if (resp && resp.cancel) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
if (!resp || !resp.result) {
|
if (!resp || !resp.result) {
|
||||||
|
__notifyImageScanFailure(resp);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if (__result(resp.result)) {
|
__result(resp.result, { source: "image" });
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
if (isScanning()) {
|
|
||||||
return __startImageScan();
|
|
||||||
}
|
|
||||||
return resp;
|
return resp;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (!err || !err.result) {
|
if (err && err.cancel) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (__result(err.result)) {
|
if (err && err.result) {
|
||||||
|
__result(err.result, { source: "image" });
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (isScanning()) {
|
__notifyImageScanFailure(err);
|
||||||
return __startImageScan();
|
|
||||||
}
|
|
||||||
return err;
|
return err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -231,16 +572,30 @@ export function isScanning() {
|
|||||||
return _scan_status === "scanning";
|
return _scan_status === "scanning";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setEmbedScanHostEnabled(enabled) {
|
||||||
|
const nextEnabled = enabled !== false;
|
||||||
|
if (_embed_scan_host_enabled === nextEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_embed_scan_host_enabled = nextEnabled;
|
||||||
|
__checkScanner();
|
||||||
|
}
|
||||||
|
|
||||||
export function clear() {
|
export function clear() {
|
||||||
for (let i = 0; i < _scan_listener_list.length; i++) {
|
for (let i = 0; i < _scan_listener_list.length; i++) {
|
||||||
const item = _scan_listener_list[i];
|
const item = _scan_listener_list[i];
|
||||||
item.cancel();
|
item.cancel();
|
||||||
}
|
}
|
||||||
_scan_listener_list.length = 0;
|
_scan_listener_list.length = 0;
|
||||||
|
for (let i = 0; i < _scan_error_listener_list.length; i++) {
|
||||||
|
const item = _scan_error_listener_list[i];
|
||||||
|
item.cancel();
|
||||||
|
}
|
||||||
|
_scan_error_listener_list.length = 0;
|
||||||
__checkScanner();
|
__checkScanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onScanListener(listener, key, match, level) {
|
export function onScanListener(listener, key, match, level, selfKeyOnly) {
|
||||||
if (!key || typeof key !== 'string') {
|
if (!key || typeof key !== 'string') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -259,11 +614,13 @@ export function onScanListener(listener, key, match, level) {
|
|||||||
item.level = level;
|
item.level = level;
|
||||||
item.match = match;
|
item.match = match;
|
||||||
item.listener = listener;
|
item.listener = listener;
|
||||||
|
item.selfKeyOnly = selfKeyOnly === true;
|
||||||
} else {
|
} else {
|
||||||
item = {
|
item = {
|
||||||
key,
|
key,
|
||||||
match: match || "",
|
match: match || "",
|
||||||
level: level || 0,
|
level: level || 0,
|
||||||
|
selfKeyOnly: selfKeyOnly === true,
|
||||||
listener: listener,
|
listener: listener,
|
||||||
cancel: () => {
|
cancel: () => {
|
||||||
const index = _scan_listener_list.indexOf(item);
|
const index = _scan_listener_list.indexOf(item);
|
||||||
@@ -300,6 +657,68 @@ export function offScanListener(listener) {
|
|||||||
__checkScanner();
|
__checkScanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createScanErrorListenerItem(listener, key, match, level) {
|
||||||
|
const item = {
|
||||||
|
key,
|
||||||
|
match: match || "",
|
||||||
|
level: level || 0,
|
||||||
|
listener: listener,
|
||||||
|
cancel: () => {
|
||||||
|
const index = _scan_error_listener_list.indexOf(item);
|
||||||
|
if (index !== -1) {
|
||||||
|
const items = _scan_error_listener_list.splice(index, 1);
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const removed = items[i];
|
||||||
|
removed.listener && removed.listener({ cancel: 1, key: removed.key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onScanErrorListener(listener, key, match, level) {
|
||||||
|
if (!key || typeof key !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof listener !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let item = null;
|
||||||
|
for (let i = 0; i < _scan_error_listener_list.length; i++) {
|
||||||
|
const _i = _scan_error_listener_list[i];
|
||||||
|
if (_i.key === key) {
|
||||||
|
item = _i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item) {
|
||||||
|
item.level = level;
|
||||||
|
item.match = match;
|
||||||
|
item.listener = listener;
|
||||||
|
} else {
|
||||||
|
item = createScanErrorListenerItem(listener, key, match, level);
|
||||||
|
_scan_error_listener_list.push(item);
|
||||||
|
}
|
||||||
|
_scan_error_listener_list.sort((a, b) => b.level - a.level);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offScanErrorListener(listener) {
|
||||||
|
for (let i = 0; i < _scan_error_listener_list.length; i++) {
|
||||||
|
const _i = _scan_error_listener_list[i];
|
||||||
|
if (typeof listener === 'string') {
|
||||||
|
if (_i.key === listener) {
|
||||||
|
_i.cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (_i.listener === listener) {
|
||||||
|
_i.cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function setStatusListener(listener) {
|
export function setStatusListener(listener) {
|
||||||
if (typeof listener !== 'function') {
|
if (typeof listener !== 'function') {
|
||||||
return;
|
return;
|
||||||
@@ -312,66 +731,50 @@ export function getStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stopScan() {
|
export function stopScan() {
|
||||||
|
cleanupWebScanResiduals();
|
||||||
if (!isScanning()) {
|
if (!isScanning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const resolve = __finishScan();
|
||||||
|
_scan_closing = true;
|
||||||
__stopCurrentScan().then(() => {
|
__stopCurrentScan().then(() => {
|
||||||
__closed();
|
resolve && resolve({ cancel: 1 });
|
||||||
|
_scan_closing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startScan() {
|
export function startScan(key) {
|
||||||
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Promise.resolve().then(() => {
|
unlockScanBeep();
|
||||||
|
Promise.resolve().then(() => waitForSdkConfigReady()).then(() => {
|
||||||
|
__beginScanSession(key);
|
||||||
__scanning();
|
__scanning();
|
||||||
let scannerPromise = new Promise(resolve => {
|
let scannerPromise = new Promise(resolve => {
|
||||||
_scan_resolve = resolve;
|
_scan_resolve = resolve;
|
||||||
});
|
});
|
||||||
let scanPromise = Promise.resolve();
|
let scanPromise = Promise.resolve();
|
||||||
if (inRuntime()) {
|
if (isWxEnv()) {
|
||||||
scanPromise = __startBridgeScan();
|
scanPromise = __startWxScan().catch(__fallbackScanAfterWxFailure);
|
||||||
} else if (isSupportWxScan()) {
|
} else if (inRuntime()) {
|
||||||
scanPromise = __startWxScan();
|
scanPromise = __startBridgeScan().catch(__fallbackScanAfterBridgeFailure);
|
||||||
} else if (isSupportWebScan()) {
|
} else if (isSupportWebScan()) {
|
||||||
unlockScanBeep();
|
scanPromise = __startWebScan(true);
|
||||||
scanPromise = startScanForWeb(getConfig("webScanCanvasStyle"), __result);
|
|
||||||
} else if (isSupportImageScan()) {
|
} else if (isSupportImageScan()) {
|
||||||
scanPromise = __startImageScan();
|
scanPromise = __startImageScan();
|
||||||
} else {
|
} else {
|
||||||
printDebug("Not support scanner");
|
printWarn("Not support scanner");
|
||||||
}
|
}
|
||||||
return Promise.race([scanPromise, scannerPromise]);
|
return withScanSessionTimeout(Promise.race([scanPromise, scannerPromise]));
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
_scan_resolve = null;
|
_scan_resolve = null;
|
||||||
|
__clearScanSessionKey();
|
||||||
__closed();
|
__closed();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scanVideo() {
|
export function scanImage(key) {
|
||||||
if (!isSupportWebScan()) {
|
|
||||||
printDebug("Not support video scanner");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unlockScanBeep();
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
__scanning();
|
|
||||||
return startScanForWeb(getConfig("webScanCanvasStyle"), __result).then(resp => {
|
|
||||||
if (resp && resp.result) {
|
|
||||||
__result(resp.result);
|
|
||||||
}
|
|
||||||
throw resp.error;
|
|
||||||
}).catch(err => { });
|
|
||||||
}).finally(() => {
|
|
||||||
__closed();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scanImage() {
|
|
||||||
if (!isSupportImageScan()) {
|
if (!isSupportImageScan()) {
|
||||||
printDebug("Not support image scanner");
|
printDebug("Not support image scanner");
|
||||||
return;
|
return;
|
||||||
@@ -379,15 +782,89 @@ export function scanImage() {
|
|||||||
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Promise.resolve().then(() => {
|
unlockScanBeep();
|
||||||
__scanning();
|
__beginScanSession(key);
|
||||||
return startScanForImage().then(resp => {
|
__scanning();
|
||||||
if (resp && resp.result) {
|
withScanSessionTimeout(__startImageScan()).catch(err => {
|
||||||
__result(resp.result);
|
if (err && err.cancel) {
|
||||||
}
|
return;
|
||||||
throw resp.error;
|
}
|
||||||
}).catch(err => { });
|
__notifyImageScanFailure(err);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
__clearScanSessionKey();
|
||||||
__closed();
|
__closed();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 由原生/业务传入已选图片 File,避免 WebView 无法从 input.files 取文件 */
|
||||||
|
export function scanImageFromFile(file, key) {
|
||||||
|
if (!isSupportImageScan()) {
|
||||||
|
printDebug("Not support image scanner");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unlockScanBeep();
|
||||||
|
cleanupWebScanResiduals();
|
||||||
|
__beginScanSession(key);
|
||||||
|
__scanning();
|
||||||
|
withScanSessionTimeout(
|
||||||
|
detectImageFileForScan(file).then(resp => {
|
||||||
|
if (!isScanning()) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
if (resp && resp.cancel) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
if (!resp || !resp.result) {
|
||||||
|
__notifyImageScanFailure(resp);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
__result(resp.result, { source: "image" });
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
).catch(err => {
|
||||||
|
if (err && err.cancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
__notifyImageScanFailure(err);
|
||||||
|
}).finally(() => {
|
||||||
|
__clearScanSessionKey();
|
||||||
|
__closed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportList = [
|
||||||
|
{
|
||||||
|
name: "bridge",
|
||||||
|
get support() {
|
||||||
|
return !!inRuntime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wx",
|
||||||
|
get support() {
|
||||||
|
return !!isSupportWxScan();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "web",
|
||||||
|
get support() {
|
||||||
|
if (shouldSkipWebCameraProbe()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !!canUseWebCameraScan();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
get support() {
|
||||||
|
return !!isSupportImageScan();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scanner",
|
||||||
|
support: true
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -5,6 +5,28 @@ let _scannerTimer = null;
|
|||||||
let _scannerLastInputTime = 0;
|
let _scannerLastInputTime = 0;
|
||||||
|
|
||||||
const SCANNER_INPUT_INTERVAL = 100;
|
const SCANNER_INPUT_INTERVAL = 100;
|
||||||
|
const SCANNER_IGNORE_KEYS = [
|
||||||
|
"PageUp",
|
||||||
|
"PageDown",
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowDown",
|
||||||
|
"ArrowUp",
|
||||||
|
"Unidentified",
|
||||||
|
"Insert",
|
||||||
|
"End",
|
||||||
|
"Home",
|
||||||
|
"Delete",
|
||||||
|
"Escape",
|
||||||
|
"Process",
|
||||||
|
"Shift",
|
||||||
|
"Backspace",
|
||||||
|
"Delete",
|
||||||
|
"Tab",
|
||||||
|
"Space",
|
||||||
|
"Control",
|
||||||
|
"CapsLock",
|
||||||
|
];
|
||||||
|
|
||||||
function getWindow() {
|
function getWindow() {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
@@ -31,10 +53,36 @@ function delayClearScannerValue() {
|
|||||||
}, SCANNER_INPUT_INTERVAL);
|
}, SCANNER_INPUT_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeAltCharCodes(value) {
|
||||||
|
return value
|
||||||
|
.replace(/Alt(\d{5})/g, (_match, code) => {
|
||||||
|
if (code.length < 5) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String.fromCharCode(Number(code));
|
||||||
|
})
|
||||||
|
.replace(/Alt(\d+)/g, () => "")
|
||||||
|
.replace(/Alt/g, () => "");
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeScannerValue(value) {
|
function normalizeScannerValue(value) {
|
||||||
return value.replace(/[\uFF01-\uFF5E]/g, char => {
|
return decodeAltCharCodes(value).replace(/[\uFF01-\uFF5E]/g, char => {
|
||||||
return String.fromCharCode(char.charCodeAt(0) - 0xFEE0);
|
return String.fromCharCode(char.charCodeAt(0) - 0xFEE0);
|
||||||
}).replace(/\u3002/g, ".");
|
}).replace(/\u3002/g, ".")
|
||||||
|
.replace(/\u2014/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInputElementFocused() {
|
||||||
|
const win = getWindow();
|
||||||
|
const activeElement = win && win.document && win.document.activeElement;
|
||||||
|
if (!activeElement || activeElement === win.document.body || activeElement === win.document.documentElement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const tagName = activeElement.tagName && activeElement.tagName.toLowerCase();
|
||||||
|
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return activeElement.isContentEditable === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopScannerEvent(event) {
|
function stopScannerEvent(event) {
|
||||||
@@ -46,7 +94,11 @@ function onScannerKeydown(event) {
|
|||||||
if (_scannerStatus !== "scanning") {
|
if (_scannerStatus !== "scanning") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
if (isInputElementFocused()) {
|
||||||
|
clearScannerValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
@@ -60,7 +112,7 @@ function onScannerKeydown(event) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!event.key || event.key.length !== 1) {
|
if (!event.key || SCANNER_IGNORE_KEYS.includes(event.key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -68,11 +120,12 @@ function onScannerKeydown(event) {
|
|||||||
clearScannerValue();
|
clearScannerValue();
|
||||||
}
|
}
|
||||||
_scannerLastInputTime = now;
|
_scannerLastInputTime = now;
|
||||||
_scannerValue += event.key;
|
// 累积按键序列,支持 Alt+数字 编码的中文扫码枪输入
|
||||||
|
_scannerValue = `${_scannerValue}${event.key}`;
|
||||||
delayClearScannerValue();
|
delayClearScannerValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startScanner(callback){
|
export function startScanner(callback) {
|
||||||
if (!callback || typeof callback !== "function") {
|
if (!callback || typeof callback !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,7 +142,7 @@ export function startScanner(callback){
|
|||||||
win.addEventListener("keydown", onScannerKeydown);
|
win.addEventListener("keydown", onScannerKeydown);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopScanner(){
|
export function stopScanner() {
|
||||||
if (_scannerStatus !== "scanning") {
|
if (_scannerStatus !== "scanning") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/services/sdkLifecycle.js
Normal file
59
src/services/sdkLifecycle.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { getConfig } from "./config";
|
||||||
|
import { resolveUseParentProxy } from "./embedProxy";
|
||||||
|
|
||||||
|
let _runtimeUrlBaseline = "";
|
||||||
|
let _runtimeUrlChangeHandler = null;
|
||||||
|
let _configReadyPromise = null;
|
||||||
|
|
||||||
|
export function bindSdkConfigReadyPromise(promise) {
|
||||||
|
_configReadyPromise = promise || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForSdkConfigReady() {
|
||||||
|
if (_configReadyPromise) {
|
||||||
|
return _configReadyPromise;
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SDK 运行时 URL(微信 JSSDK 签名、config 缓存均以此为准,不含 hash)
|
||||||
|
*/
|
||||||
|
export function getSdkRuntimeUrl() {
|
||||||
|
const override = getConfig("wxJssdkSignatureUrl");
|
||||||
|
if (override) {
|
||||||
|
return String(override).split("#")[0];
|
||||||
|
}
|
||||||
|
if (resolveUseParentProxy()) {
|
||||||
|
try {
|
||||||
|
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
|
||||||
|
return window.parent.location.href.split("#")[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 跨域父页不可读 location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof window === "undefined" || !window.location) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return window.location.href.split("#")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSdkRuntimeUrlBaseline(url) {
|
||||||
|
_runtimeUrlBaseline = url ? String(url) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSdkRuntimeUrlChanged() {
|
||||||
|
const current = getSdkRuntimeUrl();
|
||||||
|
return !!(_runtimeUrlBaseline && current && _runtimeUrlBaseline !== current);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSdkRuntimeUrlChangeHandler(handler) {
|
||||||
|
_runtimeUrlChangeHandler = typeof handler === "function" ? handler : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifySdkRuntimeUrlChanged() {
|
||||||
|
if (typeof _runtimeUrlChangeHandler === "function") {
|
||||||
|
_runtimeUrlChangeHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,26 @@
|
|||||||
import { getConfig } from "../config";
|
import { getConfig } from "../config";
|
||||||
|
import { readWxLikeEnvFromWindow, getParentWxEnvReport } from "../embedEnvProbe";
|
||||||
|
import { resolveUseParentProxy } from "../embedProxy";
|
||||||
import { request } from "../../utils/request";
|
import { request } from "../../utils/request";
|
||||||
import { toAny } from "../../utils/toany";
|
import { toAny } from "../../utils/toany";
|
||||||
|
import { printDebug } from "../../utils/logger";
|
||||||
|
import {
|
||||||
|
getSdkRuntimeUrl,
|
||||||
|
isSdkRuntimeUrlChanged,
|
||||||
|
notifySdkRuntimeUrlChanged,
|
||||||
|
} from "../sdkLifecycle";
|
||||||
|
|
||||||
const WX_JS_SDK_URL = "https://res.wx.qq.com/open/js/jweixin-1.6.0.js";
|
const WX_JS_SDK_URL = "https://res.wx.qq.com/open/js/jweixin-1.6.0.js";
|
||||||
const WX_SCAN_API = "scanQRCode";
|
const WX_SCAN_API = "scanQRCode";
|
||||||
|
|
||||||
let _wxReadyPromise = null;
|
let _wxReadyPromise = null;
|
||||||
let _wxReady = false;
|
let _wxReady = false;
|
||||||
|
let _wxSignatureUrl = "";
|
||||||
|
let _wxPendingSignatureUrl = "";
|
||||||
|
let _wxInitGeneration = 0;
|
||||||
|
let _urlWatchInstalled = false;
|
||||||
|
let _urlPollTimer = null;
|
||||||
|
let _urlReinitTimer = null;
|
||||||
|
|
||||||
function getWx() {
|
function getWx() {
|
||||||
if (typeof wx !== "undefined") {
|
if (typeof wx !== "undefined") {
|
||||||
@@ -46,6 +60,130 @@ function loadWxScript() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 微信 JS-SDK 签名 URL:与 SDK 运行时 URL 一致 */
|
||||||
|
function getPageUrlForWxJssdkSignature() {
|
||||||
|
return getSdkRuntimeUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWxJssdkAutoReinitEnabled() {
|
||||||
|
return getConfig("wxJssdkAutoReinitOnUrlChange") !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWxJssdkWatchWindow() {
|
||||||
|
const override = getConfig("wxJssdkSignatureUrl");
|
||||||
|
if (override || typeof window === "undefined") {
|
||||||
|
return typeof window !== "undefined" ? window : null;
|
||||||
|
}
|
||||||
|
if (resolveUseParentProxy()) {
|
||||||
|
try {
|
||||||
|
if (window.parent && window.parent !== window) {
|
||||||
|
return window.parent;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 跨域父页不可读,回退监听当前页
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetWxJssdkState() {
|
||||||
|
_wxReady = false;
|
||||||
|
_wxReadyPromise = null;
|
||||||
|
_wxPendingSignatureUrl = "";
|
||||||
|
_wxInitGeneration += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { resetWxJssdkState };
|
||||||
|
|
||||||
|
function isWxSignatureUrlChanged() {
|
||||||
|
const current = getPageUrlForWxJssdkSignature();
|
||||||
|
const baseline = _wxSignatureUrl || _wxPendingSignatureUrl;
|
||||||
|
return !!(baseline && current && baseline !== current);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWxJssdkUrlPollInterval() {
|
||||||
|
const interval = getConfig("wxJssdkUrlPollInterval");
|
||||||
|
if (interval === 0 || interval === false) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (typeof interval === "number" && interval > 0) {
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleWxJssdkReinitIfNeeded() {
|
||||||
|
if (!isWxEnv() || !isWxJssdkAutoReinitEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isSdkRuntimeUrlChanged() && !isWxSignatureUrlChanged()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_urlReinitTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_urlReinitTimer = setTimeout(() => {
|
||||||
|
_urlReinitTimer = null;
|
||||||
|
if (!isSdkRuntimeUrlChanged() && !isWxSignatureUrlChanged()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printDebug("sdk runtime url changed, notify reconfig:", getSdkRuntimeUrl());
|
||||||
|
notifySdkRuntimeUrlChanged();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopWxJssdkUrlPoll() {
|
||||||
|
if (_urlPollTimer) {
|
||||||
|
clearInterval(_urlPollTimer);
|
||||||
|
_urlPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWxJssdkUrlPoll() {
|
||||||
|
const interval = getWxJssdkUrlPollInterval();
|
||||||
|
if (!interval || _urlPollTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_urlPollTimer = setInterval(() => {
|
||||||
|
scheduleWxJssdkReinitIfNeeded();
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installWxJssdkUrlWatcher() {
|
||||||
|
if (!isWxJssdkAutoReinitEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const watchWin = getWxJssdkWatchWindow();
|
||||||
|
if (!watchWin || !watchWin.addEventListener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_urlWatchInstalled) {
|
||||||
|
_urlWatchInstalled = true;
|
||||||
|
watchWin.addEventListener("popstate", scheduleWxJssdkReinitIfNeeded);
|
||||||
|
watchWin.addEventListener("hashchange", scheduleWxJssdkReinitIfNeeded);
|
||||||
|
patchHistoryMethod(watchWin.history, "pushState", scheduleWxJssdkReinitIfNeeded);
|
||||||
|
patchHistoryMethod(watchWin.history, "replaceState", scheduleWxJssdkReinitIfNeeded);
|
||||||
|
}
|
||||||
|
startWxJssdkUrlPoll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchHistoryMethod(history, methodName, callback) {
|
||||||
|
if (!history || typeof history[methodName] !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const original = history[methodName];
|
||||||
|
if (original.__iscanWxPatched__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const patched = function patchedHistoryMethod() {
|
||||||
|
const result = original.apply(this, arguments);
|
||||||
|
callback();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
patched.__iscanWxPatched__ = true;
|
||||||
|
history[methodName] = patched;
|
||||||
|
}
|
||||||
|
|
||||||
function fetchWxConfig() {
|
function fetchWxConfig() {
|
||||||
let initWechatJssdk = toAny(getConfig("initWechatJssdk"), {});
|
let initWechatJssdk = toAny(getConfig("initWechatJssdk"), {});
|
||||||
if (!!initWechatJssdk.sdkConfig) {
|
if (!!initWechatJssdk.sdkConfig) {
|
||||||
@@ -59,7 +197,7 @@ function fetchWxConfig() {
|
|||||||
url: apiUrl,
|
url: apiUrl,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
data: {
|
data: {
|
||||||
url: window.location.href.split("#")[0]
|
url: getPageUrlForWxJssdkSignature()
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
let data = toAny(res.data, {});
|
let data = toAny(res.data, {});
|
||||||
@@ -77,8 +215,22 @@ function fetchWxConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isWxEnv() {
|
export function isWxEnv() {
|
||||||
return typeof navigator !== "undefined"
|
if (readWxLikeEnvFromWindow(typeof window !== "undefined" ? window : null)) {
|
||||||
&& /micromessenger/i.test(navigator.userAgent || "");
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
if (window.top && window.top !== window && readWxLikeEnvFromWindow(window.top)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 跨域不可读 top:依赖父页 probeWxEnv / embedEnvProbe 缓存
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolveUseParentProxy() && getParentWxEnvReport() === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWxMiniProgramEnv() {
|
export function isWxMiniProgramEnv() {
|
||||||
@@ -88,20 +240,30 @@ export function isWxMiniProgramEnv() {
|
|||||||
|
|
||||||
export function isSupportWxScan() {
|
export function isSupportWxScan() {
|
||||||
const wx = getWx();
|
const wx = getWx();
|
||||||
return isWxMiniProgramEnv()
|
if (isWxMiniProgramEnv()) {
|
||||||
|| isWxEnv()
|
return true;
|
||||||
&& _wxReady
|
}
|
||||||
&& wx
|
// 嵌入且 API 走父页时,子页未必初始化 wx;只要识别为微信环境即视为支持(实际能力由父页 SDK 决定)
|
||||||
&& wx.scanQRCode;
|
if (resolveUseParentProxy() && isWxEnv()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!(isWxEnv() && _wxReady && wx && wx.scanQRCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initWxJssdk() {
|
export function initWxJssdk() {
|
||||||
if (!isWxEnv()) {
|
if (!isWxEnv()) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
installWxJssdkUrlWatcher();
|
||||||
|
if (isWxSignatureUrlChanged()) {
|
||||||
|
resetWxJssdkState();
|
||||||
|
}
|
||||||
if (_wxReadyPromise) {
|
if (_wxReadyPromise) {
|
||||||
return _wxReadyPromise;
|
return _wxReadyPromise;
|
||||||
}
|
}
|
||||||
|
const generation = _wxInitGeneration;
|
||||||
|
const signatureUrlAtStart = getPageUrlForWxJssdkSignature();
|
||||||
|
_wxPendingSignatureUrl = signatureUrlAtStart;
|
||||||
_wxReadyPromise = Promise.all([loadWxScript(), fetchWxConfig()]).then(items => {
|
_wxReadyPromise = Promise.all([loadWxScript(), fetchWxConfig()]).then(items => {
|
||||||
const wx = items[0];
|
const wx = items[0];
|
||||||
const config = items[1];
|
const config = items[1];
|
||||||
@@ -117,11 +279,27 @@ export function initWxJssdk() {
|
|||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
wx.ready(() => {
|
wx.ready(() => {
|
||||||
|
if (generation !== _wxInitGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentUrl = getPageUrlForWxJssdkSignature();
|
||||||
|
if (signatureUrlAtStart && currentUrl && signatureUrlAtStart !== currentUrl) {
|
||||||
|
resetWxJssdkState();
|
||||||
|
initWxJssdk().then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_wxReady = true;
|
_wxReady = true;
|
||||||
|
_wxSignatureUrl = currentUrl;
|
||||||
|
_wxPendingSignatureUrl = "";
|
||||||
resolve(wx);
|
resolve(wx);
|
||||||
});
|
});
|
||||||
wx.error(err => {
|
wx.error(err => {
|
||||||
|
if (generation !== _wxInitGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_wxReady = false;
|
_wxReady = false;
|
||||||
|
_wxSignatureUrl = "";
|
||||||
|
_wxPendingSignatureUrl = "";
|
||||||
_wxReadyPromise = null;
|
_wxReadyPromise = null;
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
@@ -130,8 +308,12 @@ export function initWxJssdk() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
_wxReady = false;
|
if (generation === _wxInitGeneration) {
|
||||||
_wxReadyPromise = null;
|
_wxReady = false;
|
||||||
|
_wxSignatureUrl = "";
|
||||||
|
_wxPendingSignatureUrl = "";
|
||||||
|
_wxReadyPromise = null;
|
||||||
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
return _wxReadyPromise;
|
return _wxReadyPromise;
|
||||||
@@ -166,6 +348,17 @@ function getWxScanErrorMessage(err) {
|
|||||||
return errorMsg;
|
return errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWxScanCancel(err) {
|
||||||
|
if (!err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const errMsg = typeof err === "string" ? err : err.errMsg;
|
||||||
|
if (!errMsg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return String(errMsg).toLowerCase().indexOf("cancel") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
function startScanForWxMiniProgram(options) {
|
function startScanForWxMiniProgram(options) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const wx = getWx();
|
const wx = getWx();
|
||||||
@@ -191,6 +384,12 @@ function startScanForWxMiniProgram(options) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
fail: err => {
|
fail: err => {
|
||||||
|
if (isWxScanCancel(err)) {
|
||||||
|
resolve({
|
||||||
|
cancel: 1
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolve({
|
resolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: getWxScanErrorMessage(err)
|
error: getWxScanErrorMessage(err)
|
||||||
@@ -227,11 +426,16 @@ export function startScanForWx(options) {
|
|||||||
},
|
},
|
||||||
cancel: () => {
|
cancel: () => {
|
||||||
resolve({
|
resolve({
|
||||||
success: false,
|
cancel: 1
|
||||||
error: "用户取消扫码"
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fail: err => {
|
fail: err => {
|
||||||
|
if (isWxScanCancel(err)) {
|
||||||
|
resolve({
|
||||||
|
cancel: 1
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
399
types/index.d.ts
vendored
399
types/index.d.ts
vendored
@@ -6,6 +6,26 @@ interface ScanConfigOptions {
|
|||||||
* 扫码重启延迟,单位:毫秒,默认500ms
|
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||||
*/
|
*/
|
||||||
scanRestartDelay?: number,
|
scanRestartDelay?: number,
|
||||||
|
/**
|
||||||
|
* iframe 场景下是否将 API 调用转发到父页面同名 SDK(postMessage)。
|
||||||
|
* - `'auto'`(默认):处于子 frame 时 `startScan` 等由父页执行;`onScanListener` 仅注册在 iframe 内,识别结果由父页 `postMessage` 回传
|
||||||
|
* - `true` / `'on'` / `'parent'`:存在父 window 时强制转发
|
||||||
|
* - `false` / `'off'` / `'local'`:始终在本页执行(子页自己要跑扫码时用)
|
||||||
|
*/
|
||||||
|
embedProxyMode?: 'auto' | boolean | 'on' | 'off' | 'local' | 'parent',
|
||||||
|
/**
|
||||||
|
* 请求微信 JS-SDK 签名时使用的页面 URL(不含 hash)。
|
||||||
|
* 跨域 iframe 无法读取父页地址时需手动设为当前微信内打开的页面链接。
|
||||||
|
*/
|
||||||
|
wxJssdkSignatureUrl?: string,
|
||||||
|
/**
|
||||||
|
* 页面 URL 变化后是否自动重新初始化微信 JSSDK(默认 true,避免扫一扫失效)
|
||||||
|
*/
|
||||||
|
wxJssdkAutoReinitOnUrlChange?: boolean,
|
||||||
|
/**
|
||||||
|
* URL 轮询检测间隔(毫秒),用于捕获未走 history API 的 SPA 路由;默认 1000,设为 0 关闭
|
||||||
|
*/
|
||||||
|
wxJssdkUrlPollInterval?: number,
|
||||||
/**
|
/**
|
||||||
* 桥接是否启用,默认启用
|
* 桥接是否启用,默认启用
|
||||||
*/
|
*/
|
||||||
@@ -33,6 +53,50 @@ interface ScanConfigOptions {
|
|||||||
* 2. 结束扫码的方法名称为:stopScan
|
* 2. 结束扫码的方法名称为:stopScan
|
||||||
*/
|
*/
|
||||||
bridgeName?: string,
|
bridgeName?: string,
|
||||||
|
/**
|
||||||
|
* 桥接扫码超时(毫秒),超时后回退 Web/图片识别,默认 5000
|
||||||
|
*/
|
||||||
|
bridgeScanTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 是否允许 H5 摄像头扫码:true 强制开启(仍需有媒体 API),false 强制关闭
|
||||||
|
*/
|
||||||
|
webScanCameraEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* startScan 走 Web 摄像头前是否展示权限说明弹窗,默认 true
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionDialogEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗标题
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionTitle?: string,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗正文
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionMessage?: string,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗确认按钮文案
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionConfirmText?: string,
|
||||||
|
/**
|
||||||
|
* 摄像头权限说明弹窗取消按钮文案
|
||||||
|
*/
|
||||||
|
webScanCameraPermissionCancelText?: string,
|
||||||
|
/**
|
||||||
|
* 选图后延迟读取 file 对象(毫秒),微信/安卓 WebView 默认 100
|
||||||
|
*/
|
||||||
|
webScanFileReadDelay?: number,
|
||||||
|
/**
|
||||||
|
* 强制使用 WASM ponyfill 识别(微信/部分 WebView 建议开启,默认微信内自动开启)
|
||||||
|
*/
|
||||||
|
webScanPreferPonyfill?: boolean,
|
||||||
|
/**
|
||||||
|
* WASM 文件完整 URL;未配置时相对 SDK 脚本地址解析 lib/reader.wasm
|
||||||
|
*/
|
||||||
|
webScanWasmUrl?: string,
|
||||||
|
/**
|
||||||
|
* WASM 基准路径(SDK 脚本 URL 或目录);用于 async/defer 加载时修正 reader.wasm 路径
|
||||||
|
*/
|
||||||
|
webScanWasmBaseUrl?: string,
|
||||||
/**
|
/**
|
||||||
* webScan是否启用,默认启用
|
* webScan是否启用,默认启用
|
||||||
*/
|
*/
|
||||||
@@ -42,9 +106,29 @@ interface ScanConfigOptions {
|
|||||||
*/
|
*/
|
||||||
webScanCanvasEnabled?: boolean,
|
webScanCanvasEnabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* 网页扫码canvas样式,默认:position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;
|
* 网页扫码canvas样式,默认:PC 为左上角固定 300×300;移动端为 min(视口宽, 视口高) 的正方形,固定在左上角;z-index: 9999
|
||||||
*/
|
*/
|
||||||
webScanCanvasStyle?: string,
|
webScanCanvasStyle?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码canvas关闭按钮样式,默认显示在canvas右上角
|
||||||
|
*/
|
||||||
|
webScanCloseButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码 canvas 根元素的 class,便于配合外部样式表定制布局与外观
|
||||||
|
*/
|
||||||
|
webScanCanvasClass?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码关闭按钮的 class
|
||||||
|
*/
|
||||||
|
webScanCloseButtonClass?: string,
|
||||||
|
/**
|
||||||
|
* Canvas 开启且支持图片识别时,关闭按钮下方的「选图」按钮样式(fixed 定位基准由 SDK 计算,可与关闭按钮一致覆盖)
|
||||||
|
*/
|
||||||
|
webScanPickImageButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* 网页扫码选图按钮的 class
|
||||||
|
*/
|
||||||
|
webScanPickImageButtonClass?: string,
|
||||||
/**
|
/**
|
||||||
* 网页扫码类型,默认支持二维码和条码
|
* 网页扫码类型,默认支持二维码和条码
|
||||||
*/
|
*/
|
||||||
@@ -58,13 +142,160 @@ interface ScanConfigOptions {
|
|||||||
*/
|
*/
|
||||||
webScanVideoMirrorVertical?: boolean,
|
webScanVideoMirrorVertical?: boolean,
|
||||||
/**
|
/**
|
||||||
* 网页扫码成功提示音地址,默认使用内置提示音
|
* 摄像头不可用(如部分安卓内置浏览器禁用 getUserMedia)时是否自动回退为拍照/选图识别,默认启用
|
||||||
*/
|
*/
|
||||||
webScanBeepAudio?: string,
|
webScanImageFallbackOnVideoError?: boolean,
|
||||||
/**
|
/**
|
||||||
* 网页扫码成功提示音是否启用,默认启用
|
* 打开摄像头超时时间(毫秒),超时后触发图片回退,默认 10000
|
||||||
*/
|
*/
|
||||||
webScanBeepEnabled?: boolean,
|
webScanVideoAccessTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 摄像头已打开但长时间无画面时触发图片回退(毫秒),默认 8000
|
||||||
|
*/
|
||||||
|
webScanVideoReadyTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 图片/回退识别时 detect 超时(毫秒),默认 15000
|
||||||
|
*/
|
||||||
|
webScanDetectTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 图片识别是否优先使用 ZXing ponyfill(原生 detect 在部分 WebView 可能卡住),默认 true
|
||||||
|
*/
|
||||||
|
webScanImagePreferPonyfill?: boolean,
|
||||||
|
/**
|
||||||
|
* 移动端图片回退是否使用 capture 拍照(false 为相册选图,兼容性更好),默认 false
|
||||||
|
*/
|
||||||
|
webScanImagePreferCapture?: boolean,
|
||||||
|
/**
|
||||||
|
* 图片识别是否优先 canvas 解码(安卓 WebView 建议开启),默认在安卓/微信内自动开启
|
||||||
|
*/
|
||||||
|
webScanImageDetectPreferCanvas?: boolean,
|
||||||
|
/**
|
||||||
|
* ZXing WASM 加载超时(毫秒),默认 20000
|
||||||
|
*/
|
||||||
|
webScanPrepareTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 选图等待超时(毫秒),默认 120000
|
||||||
|
*/
|
||||||
|
webScanChooseImageTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 选图方式:button=显示「选择图片」按钮(安卓/微信默认,需用户点击);auto=自动弹出系统选图
|
||||||
|
*/
|
||||||
|
webScanImagePickerMode?: 'auto' | 'button',
|
||||||
|
webScanImagePickerTitle?: string,
|
||||||
|
webScanImagePickerButtonText?: string,
|
||||||
|
webScanImagePickerCancelText?: string,
|
||||||
|
/**
|
||||||
|
* 单次扫码会话超时(毫秒),超时后状态恢复 ready,默认 90000
|
||||||
|
*/
|
||||||
|
scanSessionTimeout?: number,
|
||||||
|
/**
|
||||||
|
* 扫码成功提示音地址,默认使用内置提示音;任意识别模式匹配成功时播放
|
||||||
|
*/
|
||||||
|
scanBeepAudio?: string,
|
||||||
|
/**
|
||||||
|
* 扫码成功是否播放提示音,默认启用;任意识别模式匹配成功时生效
|
||||||
|
*/
|
||||||
|
scanBeepEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* PC 微信 Native 扫码支付是否展示内置二维码弹层,默认 true
|
||||||
|
*/
|
||||||
|
paymentNativeQrEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层标题
|
||||||
|
*/
|
||||||
|
paymentNativeQrTitle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层说明文案
|
||||||
|
*/
|
||||||
|
paymentNativeQrMessage?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码底部提示
|
||||||
|
*/
|
||||||
|
paymentNativeQrHint?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码边长(像素),默认 220
|
||||||
|
*/
|
||||||
|
paymentNativeQrSize?: number,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层遮罩样式
|
||||||
|
*/
|
||||||
|
paymentNativeQrOverlayStyle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层面板样式
|
||||||
|
*/
|
||||||
|
paymentNativeQrPanelStyle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码关闭按钮样式
|
||||||
|
*/
|
||||||
|
paymentNativeQrCloseButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层遮罩 class
|
||||||
|
*/
|
||||||
|
paymentNativeQrOverlayClass?: string,
|
||||||
|
/**
|
||||||
|
* Native 支付二维码弹层面板 class
|
||||||
|
*/
|
||||||
|
paymentNativeQrPanelClass?: string,
|
||||||
|
/**
|
||||||
|
* PC 支付宝表单是否使用弹层 + iframe,默认 true;false 时整页跳转
|
||||||
|
*/
|
||||||
|
paymentAlipayFormEnabled?: boolean,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层标题
|
||||||
|
*/
|
||||||
|
paymentAlipayFormTitle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层说明文案
|
||||||
|
*/
|
||||||
|
paymentAlipayFormMessage?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单 iframe 高度(像素),默认 520
|
||||||
|
*/
|
||||||
|
paymentAlipayFormIframeHeight?: number,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层遮罩样式(未配置时回退 paymentNativeQrOverlayStyle)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormOverlayStyle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层面板样式(未配置时回退 paymentNativeQrPanelStyle)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormPanelStyle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层关闭按钮样式(未配置时回退 paymentNativeQrCloseButtonStyle)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormCloseButtonStyle?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层遮罩 class(未配置时回退 paymentNativeQrOverlayClass)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormOverlayClass?: string,
|
||||||
|
/**
|
||||||
|
* 支付宝表单弹层面板 class(未配置时回退 paymentNativeQrPanelClass)
|
||||||
|
*/
|
||||||
|
paymentAlipayFormPanelClass?: string,
|
||||||
|
/**
|
||||||
|
* 微信支付 OAuth 配置(paymentType 为 wechat 且微信内 JSAPI 时由 requestPayment 自动处理)
|
||||||
|
*/
|
||||||
|
initWechatPayment?: {
|
||||||
|
/**
|
||||||
|
* 用 code 换 openid,对应 api.biz.wechat.oauth;
|
||||||
|
* 未配置时默认 /api?action=api.biz.wechat.oauth(若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
|
||||||
|
*/
|
||||||
|
oauthApiUrl?: string,
|
||||||
|
/**
|
||||||
|
* 获取 OAuth 授权信息(含 appId、authorizeUrl),对应 api.biz.wechat.oauthAuthorizeUrl;
|
||||||
|
* 未配置时默认 /api?action=api.biz.wechat.oauthAuthorizeUrl(若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
|
||||||
|
*/
|
||||||
|
oauthAuthorizeUrlApiUrl?: string,
|
||||||
|
/**
|
||||||
|
* openid 缓存 key,默认 iscan_wechat_openid_${appId}(appId 来自授权接口响应)
|
||||||
|
*/
|
||||||
|
storageKey?: string,
|
||||||
|
/**
|
||||||
|
* OAuth 授权回跳完整 URL(redirectUri),须与当前页域名一致且已在公众号登记;
|
||||||
|
* 未配置时默认使用当前页 location.href(不含 hash)
|
||||||
|
*/
|
||||||
|
redirectUri?: string,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 微信JSSDK配置,微信环境才会生效,配置后会自动初始化微信JSSDK
|
* 微信JSSDK配置,微信环境才会生效,配置后会自动初始化微信JSSDK
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +348,16 @@ interface ScanResult {
|
|||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码错误
|
||||||
|
*/
|
||||||
|
interface ScanErrorInfo {
|
||||||
|
error: string,
|
||||||
|
key: string,
|
||||||
|
source?: string,
|
||||||
|
cancel?: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听key
|
* 监听key
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +374,10 @@ interface ScanListenerInfo {
|
|||||||
* 监听级别
|
* 监听级别
|
||||||
*/
|
*/
|
||||||
level?: number;
|
level?: number;
|
||||||
|
/**
|
||||||
|
* 为 true 时仅接收同 key 主动扫码(startScan/scanImage 等)的结果,忽略扫码枪
|
||||||
|
*/
|
||||||
|
selfKeyOnly?: boolean;
|
||||||
/**
|
/**
|
||||||
* 监听回调
|
* 监听回调
|
||||||
*/
|
*/
|
||||||
@@ -153,11 +398,108 @@ type ScanStatus = "scanning" | "ready";
|
|||||||
*/
|
*/
|
||||||
type ScanResultCallback = (result: ScanResult) => any;
|
type ScanResultCallback = (result: ScanResult) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听扫码错误回调
|
||||||
|
*/
|
||||||
|
type ScanErrorCallback = (error: ScanErrorInfo) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码错误监听信息
|
||||||
|
*/
|
||||||
|
interface ScanErrorListenerInfo {
|
||||||
|
key?: string;
|
||||||
|
match?: string;
|
||||||
|
level?: number;
|
||||||
|
listener: ScanErrorCallback;
|
||||||
|
cancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听状态回调
|
* 监听状态回调
|
||||||
*/
|
*/
|
||||||
type ScanStatusCallback = (status: ScanStatus) => any;
|
type ScanStatusCallback = (status: ScanStatus) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付渠道
|
||||||
|
*/
|
||||||
|
type PaymentType = 'wechat' | 'alipay';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSAPI 收银台交互结果(仅表示用户在收银台的操作,非支付到账结果)
|
||||||
|
*/
|
||||||
|
type PaymentCashierResult = 'ok' | 'cancel' | 'fail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付场景
|
||||||
|
*/
|
||||||
|
type PayScene = 'jsapi' | 'h5' | 'native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SDK 传给业务支付函数的参数(OAuth、环境判断由 SDK 自动完成)
|
||||||
|
*/
|
||||||
|
interface PaymentPrepareParams {
|
||||||
|
/** 微信 JSAPI 场景下的 openid,其他场景为空字符串 */
|
||||||
|
openid: string;
|
||||||
|
/** 当前环境对应的微信支付场景 */
|
||||||
|
payScene: PayScene;
|
||||||
|
/** navigator.userAgent */
|
||||||
|
userAgent: string;
|
||||||
|
/** 用户 IP,默认空字符串 */
|
||||||
|
clientIp: string;
|
||||||
|
/** 支付完成回跳地址 */
|
||||||
|
returnUrl: string;
|
||||||
|
/** 支付渠道 */
|
||||||
|
paymentType: PaymentType;
|
||||||
|
/** 支付金额(元),由 requestPayment 传入 */
|
||||||
|
amount?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务支付函数:调用自己的支付接口,返回 paymentFormData
|
||||||
|
*/
|
||||||
|
type PaymentFormDataProvider = (
|
||||||
|
params: PaymentPrepareParams
|
||||||
|
) => string | Record<string, any> | Promise<string | Record<string, any>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SDK 发起支付后的返回(不代表支付成功,业务须自行查单)
|
||||||
|
*/
|
||||||
|
interface PaymentInvokeResult {
|
||||||
|
payType?: 'jsapi' | 'h5' | 'native' | 'alipay';
|
||||||
|
/** SDK 是否已发起支付流程 */
|
||||||
|
invoked: boolean;
|
||||||
|
/** 微信 OAuth 授权跳转中(页面将刷新,业务函数尚未执行) */
|
||||||
|
oauthRedirecting?: boolean;
|
||||||
|
/** 支付未发起(含微信 OAuth 失败) */
|
||||||
|
failed?: boolean;
|
||||||
|
/** 微信 OAuth / openid 获取失败 */
|
||||||
|
oauthFailed?: boolean;
|
||||||
|
/** OAuth 或调起失败时的错误信息 */
|
||||||
|
error?: string;
|
||||||
|
/** H5 / 移动端支付宝是否已跳转整页 */
|
||||||
|
redirected?: boolean;
|
||||||
|
/** Native 场景是否展示了二维码弹层 */
|
||||||
|
qrShown?: boolean;
|
||||||
|
/** PC 支付宝是否展示了表单弹层 */
|
||||||
|
dialogShown?: boolean;
|
||||||
|
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
|
||||||
|
cashierResult?: PaymentCashierResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付请求选项
|
||||||
|
*/
|
||||||
|
interface RequestPaymentOptions {
|
||||||
|
/** 支付金额(元),展示在支付弹窗;也可由业务返回 amount / totalAmount / payAmount */
|
||||||
|
amount?: number | string;
|
||||||
|
/** 货币符号,默认 CNY(¥) */
|
||||||
|
currency?: string;
|
||||||
|
/** 支付完成回跳地址,未传时使用当前页 URL */
|
||||||
|
returnUrl?: string;
|
||||||
|
/** OAuth 授权回跳完整 URL(redirectUri),覆盖 initWechatPayment.redirectUri */
|
||||||
|
redirectUri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** IScan */
|
/** IScan */
|
||||||
interface IScan {
|
interface IScan {
|
||||||
/**
|
/**
|
||||||
@@ -176,13 +518,27 @@ interface IScan {
|
|||||||
* @param key 监听key
|
* @param key 监听key
|
||||||
* @param match 监听匹配
|
* @param match 监听匹配
|
||||||
* @param level 监听级别
|
* @param level 监听级别
|
||||||
|
* @param selfKeyOnly 为 true 时仅接收同 key 主动扫码结果,忽略扫码枪
|
||||||
*/
|
*/
|
||||||
onScanListener(callback: ScanResultCallback, key: string, match?: string, level?: number): ScanListenerInfo;
|
onScanListener(callback: ScanResultCallback, key: string, match?: string, level?: number, selfKeyOnly?: boolean): ScanListenerInfo;
|
||||||
/**
|
/**
|
||||||
* 取消监听扫码结果
|
* 取消监听扫码结果
|
||||||
* @param callback 监听回调,或监听key
|
* @param callback 监听回调,或监听key
|
||||||
*/
|
*/
|
||||||
offScanListener(callback: ScanResultCallback | string): void;
|
offScanListener(callback: ScanResultCallback | string): void;
|
||||||
|
/**
|
||||||
|
* 添加监听扫码错误(如图片识别失败)
|
||||||
|
* @param callback 错误回调
|
||||||
|
* @param key 监听 key
|
||||||
|
* @param match 可选正则,匹配 error 文本后回调
|
||||||
|
* @param level 优先级
|
||||||
|
*/
|
||||||
|
onScanErrorListener(callback: ScanErrorCallback, key: string, match?: string, level?: number): ScanErrorListenerInfo;
|
||||||
|
/**
|
||||||
|
* 取消监听扫码错误
|
||||||
|
* @param callback 监听回调,或监听 key
|
||||||
|
*/
|
||||||
|
offScanErrorListener(callback: ScanErrorCallback | string): void;
|
||||||
/**
|
/**
|
||||||
* 获取扫码状态
|
* 获取扫码状态
|
||||||
* @returns ScanStatus
|
* @returns ScanStatus
|
||||||
@@ -193,21 +549,38 @@ interface IScan {
|
|||||||
*/
|
*/
|
||||||
stopScan(): void;
|
stopScan(): void;
|
||||||
/**
|
/**
|
||||||
* 开启扫码
|
* 开启扫码(摄像头 / 桥接 / 微信等)
|
||||||
|
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
|
||||||
*/
|
*/
|
||||||
startScan(): void;
|
startScan(key?: string): void;
|
||||||
/**
|
|
||||||
* 开启视频扫码
|
|
||||||
*/
|
|
||||||
scanVideo(): void;
|
|
||||||
/**
|
/**
|
||||||
* 选择图片进行识别
|
* 选择图片进行识别
|
||||||
|
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
|
||||||
*/
|
*/
|
||||||
scanImage(): void;
|
scanImage(key?: string): void;
|
||||||
|
/**
|
||||||
|
* 由业务/原生传入已选图片 File 识别(WebView input.files 异常时使用)
|
||||||
|
* @param file 已选图片 File
|
||||||
|
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
|
||||||
|
*/
|
||||||
|
scanImageFromFile(file: File | Blob, key?: string): void;
|
||||||
/**
|
/**
|
||||||
* 清除全部监听
|
* 清除全部监听
|
||||||
*/
|
*/
|
||||||
clear(): void;
|
clear(): void;
|
||||||
|
/**
|
||||||
|
* 发起支付:按 paymentType 决定是否走微信 OAuth,再将参数传给业务函数获取 paymentFormData 并调起支付。
|
||||||
|
* 不代表支付成功,业务须自行查单。
|
||||||
|
*/
|
||||||
|
requestPayment(
|
||||||
|
paymentType: PaymentType,
|
||||||
|
fetchPaymentFormData: PaymentFormDataProvider,
|
||||||
|
options?: RequestPaymentOptions
|
||||||
|
): Promise<PaymentInvokeResult>;
|
||||||
|
/**
|
||||||
|
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层、支付宝表单弹层)
|
||||||
|
*/
|
||||||
|
closePaymentDialog(): void;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const config = {
|
|||||||
}),
|
}),
|
||||||
new CpWebpackPlugin([
|
new CpWebpackPlugin([
|
||||||
{ from: path.resolve('./types'), to: path.resolve('./dist') },
|
{ from: path.resolve('./types'), to: path.resolve('./dist') },
|
||||||
|
{ from: path.resolve('./README.md'), to: path.resolve('./dist/index.md') },
|
||||||
{ from: zxingReaderWasm, to: path.resolve('./dist/lib/reader.wasm') },
|
{ from: zxingReaderWasm, to: path.resolve('./dist/lib/reader.wasm') },
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user