Compare commits

...

5 Commits

Author SHA1 Message Date
iqudoo
689eec085f 扫一扫优化 2026-06-26 21:52:47 +08:00
iqudoo
bee953617d fux 2026-06-17 20:20:27 +08:00
iqudoo
23b97548cf fix 2026-06-17 20:17:50 +08:00
iqudoo
378c7e4ba5 fix 2026-06-17 16:36:12 +08:00
iqudoo
c2476b9f85 支付 2026-06-17 12:50:02 +08:00
22 changed files with 2210 additions and 94 deletions

216
dist/index.d.ts vendored
View File

@@ -18,6 +18,14 @@ interface ScanConfigOptions {
* 跨域 iframe 无法读取父页地址时需手动设为当前微信内打开的页面链接。
*/
wxJssdkSignatureUrl?: string,
/**
* 页面 URL 变化后是否自动重新初始化微信 JSSDK默认 true避免扫一扫失效
*/
wxJssdkAutoReinitOnUrlChange?: boolean,
/**
* URL 轮询检测间隔(毫秒),用于捕获未走 history API 的 SPA 路由;默认 1000设为 0 关闭
*/
wxJssdkUrlPollInterval?: number,
/**
* 桥接是否启用,默认启用
*/
@@ -53,14 +61,6 @@ interface ScanConfigOptions {
* 是否允许 H5 摄像头扫码true 强制开启(仍需有媒体 APIfalse 强制关闭
*/
webScanCameraEnabled?: boolean,
/**
* @deprecated 请用 webScanCameraEnabled保留兼容
*/
webScanCameraInWechat?: boolean,
/**
* @deprecated 摄像头权限已后置到 startScan请用 webScanVideoAccessTimeout
*/
webScanCameraProbeTimeout?: number,
/**
* startScan 走 Web 摄像头前是否展示权限说明弹窗,默认 true
*/
@@ -196,6 +196,106 @@ interface ScanConfigOptions {
* 扫码成功是否播放提示音,默认启用;任意识别模式匹配成功时生效
*/
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默认 truefalse 时整页跳转
*/
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 授权回跳完整 URLredirectUri须与当前页域名一致且已在公众号登记
* 未配置时默认使用当前页 location.href不含 hash
*/
redirectUri?: string,
},
/**
* 微信JSSDK配置微信环境才会生效配置后会自动初始化微信JSSDK
*/
@@ -319,6 +419,87 @@ interface ScanErrorListenerInfo {
*/
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 授权回跳完整 URLredirectUri覆盖 initWechatPayment.redirectUri */
redirectUri?: string;
}
/** IScan */
interface IScan {
/**
@@ -377,12 +558,29 @@ interface IScan {
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
*/
scanImage(key?: string): void;
/** 由业务/原生传入已选图片 File 识别WebView input.files 异常时使用) */
/**
* 由业务/原生传入已选图片 File 识别WebView input.files 异常时使用)
* @param file 已选图片 File
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
*/
scanImageFromFile(file: File | Blob, key?: string): void;
/**
* 清除全部监听
*/
clear(): void;
/**
* 发起支付:按 paymentType 决定是否走微信 OAuth再将参数传给业务函数获取 paymentFormData 并调起支付。
* 不代表支付成功,业务须自行查单。
*/
requestPayment(
paymentType: PaymentType,
fetchPaymentFormData: PaymentFormDataProvider,
options?: RequestPaymentOptions
): Promise<PaymentInvokeResult>;
/**
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层、支付宝表单弹层)
*/
closePaymentDialog(): void;
}

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -51,5 +51,8 @@
},
"repository": {},
"author": "",
"license": "ISC"
"license": "ISC",
"dependencies": {
"qrcode": "^1.5.4"
}
}

154
pnpm-lock.yaml generated
View File

@@ -7,6 +7,10 @@ settings:
importers:
.:
dependencies:
qrcode:
specifier: ^1.5.4
version: 1.5.4
devDependencies:
autoprefixer:
specifier: ^7.2.3
@@ -243,6 +247,10 @@ packages:
resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
engines: {node: '>=6'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@2.2.1:
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
engines: {node: '>=0.10.0'}
@@ -251,6 +259,10 @@ packages:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
anymatch@2.0.0:
resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==}
@@ -762,6 +774,9 @@ packages:
cliui@5.0.0:
resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
@@ -781,6 +796,10 @@ packages:
color-convert@1.9.3:
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:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
@@ -1051,6 +1070,9 @@ packages:
diffie-hellman@5.0.3:
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dom-converter@0.2.0:
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
@@ -1108,6 +1130,9 @@ packages:
emoji-regex@7.0.3:
resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emojis-list@3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'}
@@ -1305,6 +1330,10 @@ packages:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
findup-sync@3.0.0:
resolution: {integrity: sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==}
engines: {node: '>= 0.10'}
@@ -1776,6 +1805,10 @@ packages:
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
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:
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
engines: {node: '>= 0.4'}
@@ -1989,6 +2022,10 @@ packages:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@@ -2276,6 +2313,10 @@ packages:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-map@2.1.0:
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
engines: {node: '>=6'}
@@ -2333,6 +2374,10 @@ packages:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -2397,6 +2442,10 @@ packages:
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
engines: {node: '>=4.0.0'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
portfinder@1.0.38:
resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==}
engines: {node: '>= 10.12'}
@@ -2587,6 +2636,11 @@ packages:
(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:
resolution: {integrity: sha512-f5M0HQqZWkzU8GELTY8LyMrGkr3bPjKoFtTkwUEqJQbcljbeK8M7mliP9Ia2xoOI6oMerp+QPS7oYJtpGmWe/A==}
@@ -2985,6 +3039,10 @@ packages:
resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
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:
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
engines: {node: '>= 0.4'}
@@ -3011,6 +3069,10 @@ packages:
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
engines: {node: '>=6'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@@ -3352,6 +3414,10 @@ packages:
resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
engines: {node: '>=6'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@@ -3382,9 +3448,17 @@ packages:
yargs-parser@13.1.2:
resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@13.3.2:
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:
resolution: {integrity: sha512-2YMAriaYHX9wrBY2k7H0epSo+dyCaCZg/vOtt+nEDXM9ul480gkXz/9SkwpOeHcD2H5qqDG8lWDSBwpTcZpa6w==}
peerDependencies:
@@ -3566,12 +3640,18 @@ snapshots:
ansi-regex@4.1.1: {}
ansi-regex@5.0.1: {}
ansi-styles@2.2.1: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
anymatch@2.0.0:
dependencies:
micromatch: 3.1.10(supports-color@6.1.0)
@@ -4473,6 +4553,12 @@ snapshots:
strip-ansi: 5.2.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: {}
co@4.6.0: {}
@@ -4490,6 +4576,10 @@ snapshots:
dependencies:
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.4: {}
@@ -4826,6 +4916,8 @@ snapshots:
miller-rabin: 4.0.1
randombytes: 2.1.0
dijkstrajs@1.0.3: {}
dom-converter@0.2.0:
dependencies:
utila: 0.4.0
@@ -4900,6 +4992,8 @@ snapshots:
emoji-regex@7.0.3: {}
emoji-regex@8.0.0: {}
emojis-list@3.0.0: {}
encodeurl@2.0.0: {}
@@ -5188,6 +5282,11 @@ snapshots:
dependencies:
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):
dependencies:
detect-file: 1.0.0
@@ -5712,6 +5811,8 @@ snapshots:
is-fullwidth-code-point@2.0.0: {}
is-fullwidth-code-point@3.0.0: {}
is-generator-function@1.1.2:
dependencies:
call-bound: 1.0.4
@@ -5906,6 +6007,10 @@ snapshots:
p-locate: 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.memoize@4.1.2: {}
@@ -6256,6 +6361,10 @@ snapshots:
dependencies:
p-limit: 2.3.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-map@2.1.0: {}
p-try@1.0.0: {}
@@ -6310,6 +6419,8 @@ snapshots:
path-exists@3.0.0: {}
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
path-is-inside@1.0.2: {}
@@ -6359,6 +6470,8 @@ snapshots:
pngjs@3.4.0: {}
pngjs@5.0.0: {}
portfinder@1.0.38:
dependencies:
async: 3.2.6
@@ -6620,6 +6733,12 @@ snapshots:
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@6.14.2:
@@ -7137,6 +7256,12 @@ snapshots:
is-fullwidth-code-point: 2.0.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:
dependencies:
call-bind: 1.0.9
@@ -7176,6 +7301,10 @@ snapshots:
dependencies:
ansi-regex: 4.1.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-bom@3.0.0: {}
style-loader@0.18.2:
@@ -7612,6 +7741,12 @@ snapshots:
string-width: 3.1.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: {}
ws@6.2.3:
@@ -7631,6 +7766,11 @@ snapshots:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@13.3.2:
dependencies:
cliui: 5.0.0
@@ -7644,6 +7784,20 @@ snapshots:
y18n: 4.0.3
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):
dependencies:
'@types/emscripten': 1.41.5

View File

@@ -7,13 +7,21 @@ import {
startScan, stopScan, scanImage, scanImageFromFile, clear
} from './services/provider/scan';
import { setConfig, getVersion } from './services/config';
import { initWxJssdk } from './services/wx';
import { initWxJssdk, installWxJssdkUrlWatcher, resetWxJssdkState } from './services/wx';
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 _calledReady = false;
@@ -22,20 +30,26 @@ export function isReadyCalled() {
return _calledReady;
}
function config(config) {
if (config) {
setConfig(config);
}
if (_readyPromise) {
return _readyPromise;
}
function invalidateSdkReadyState() {
_readyPromise = null;
_calledReady = false;
setSdkRuntimeUrlBaseline("");
bindSdkConfigReadyPromise(null);
resetWxJssdkState();
}
function runSdkConfigInit() {
const urlAtStart = getSdkRuntimeUrl();
setSdkRuntimeUrlBaseline(urlAtStart);
_readyPromise = Promise.resolve().then(() => {
initWxJssdk().catch(err => {
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()) {
@@ -45,13 +59,39 @@ function config(config) {
}
}).then(() => {
_calledReady = true;
setSdkRuntimeUrlBaseline(getSdkRuntimeUrl());
}).catch(err => {
_readyPromise = null;
invalidateSdkReadyState();
throw err;
});
bindSdkConfigReadyPromise(_readyPromise);
return _readyPromise;
}
function config(config) {
if (config) {
setConfig(config);
}
if (isSdkRuntimeUrlChanged()) {
invalidateSdkReadyState();
}
if (_readyPromise) {
return _readyPromise;
}
return runSdkConfigInit();
}
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({}, {
config,
onScanListener,
@@ -65,4 +105,6 @@ export default Object.assign({}, {
scanImage,
scanImageFromFile,
clear,
requestPayment,
closePaymentDialog,
});

View File

@@ -21,6 +21,12 @@ import {
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;
@@ -50,6 +56,7 @@ function isEmbedMessage(data) {
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) {
@@ -93,16 +100,21 @@ function hydrateEmbedParams(params, messageSource, targetOrigin) {
const cbId = val.__IScanEmbedCb__;
return function embedCbProxy() {
const args = Array.prototype.slice.call(arguments);
messageSource.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "callback",
cbId,
args,
},
targetOrigin
);
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") {
@@ -247,6 +259,20 @@ function embedChildOnMessage(ev) {
}
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) {
@@ -262,9 +288,35 @@ function embedChildOnMessage(ev) {
}
if (data.kind === "callback") {
const fn = childCallbackFns[data.cbId];
if (typeof fn === "function") {
fn.apply(null, data.args || []);
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;
}
}
@@ -304,6 +356,10 @@ function embedInvoke(methodKey, params) {
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))
@@ -344,6 +400,11 @@ function handleEmbedHostInvoke(lib, ev) {
},
ev.origin
);
})
.then(() => {
if (shouldDelegatePaymentDialog) {
clearEmbedPaymentDialogTarget();
}
});
}
@@ -390,6 +451,23 @@ export function installEmbedHost(lib) {
}
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;
}

View File

@@ -9,7 +9,8 @@ if (typeof document !== 'undefined' && document.currentScript && document.curren
}
const IScan = exportSDK(core, null, "config", "setStatusListener", "onScanListener",
"offScanListener", "onScanErrorListener", "offScanErrorListener", "stopScan", "startScan", "scanImage", "scanImageFromFile", "clear");
"offScanListener", "onScanErrorListener", "offScanErrorListener", "stopScan", "startScan", "scanImage", "scanImageFromFile", "clear",
"requestPayment", "closePaymentDialog");
installEmbedHost(core);

View 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);
}

View 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,
};
}

View 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"));
});
}

View 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";
}

View 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)
);
}

View 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);
}

View 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());
}

View 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);
}

View 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}`));
}

View 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,
};
}

View File

@@ -18,6 +18,7 @@ import { startScanner, stopScanner } from "../scanner";
import { getConfig } from "../config";
import { toAny } from "../../utils/toany";
import { printDebug, printWarn } from "../../utils/logger";
import { waitForSdkConfigReady } from "../sdkLifecycle";
import { forwardEmbedScanResultIfNeeded, forwardEmbedScanErrorIfNeeded } from "../embedScanBridge";
let _scan_status = "ready";
@@ -81,6 +82,9 @@ function __fallbackScanAfterWxFailure(err) {
}
function __startNonBridgeScan(err) {
if (isWxEnv()) {
return __startWxScan();
}
if (isSupportWebScan()) {
return __startWebScan();
}
@@ -744,17 +748,17 @@ export function startScan(key) {
return;
}
unlockScanBeep();
Promise.resolve().then(() => {
Promise.resolve().then(() => waitForSdkConfigReady()).then(() => {
__beginScanSession(key);
__scanning();
let scannerPromise = new Promise(resolve => {
_scan_resolve = resolve;
});
let scanPromise = Promise.resolve();
if (inRuntime()) {
if (isWxEnv()) {
scanPromise = __startWxScan().catch(__fallbackScanAfterWxFailure);
} else if (inRuntime()) {
scanPromise = __startBridgeScan().catch(__fallbackScanAfterBridgeFailure);
} else if (isSupportWxScan()) {
scanPromise = __startWxScan();
} else if (isSupportWebScan()) {
scanPromise = __startWebScan(true);
} else if (isSupportImageScan()) {

View 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();
}
}

View File

@@ -401,16 +401,11 @@ function invokeGetUserMedia(constraints) {
return Promise.reject(new Error("getUserMedia is not supported"));
}
function getWebScanCameraProbeTimeout() {
const timeout = getConfig("webScanCameraProbeTimeout");
return typeof timeout === "number" && timeout > 0 ? timeout : 3000;
}
function resolveWebCameraConfigOverride() {
if (getConfig("webScanCameraEnabled") === false || getConfig("webScanCameraInWechat") === false) {
if (getConfig("webScanCameraEnabled") === false) {
return false;
}
if (getConfig("webScanCameraEnabled") === true || getConfig("webScanCameraInWechat") === true) {
if (getConfig("webScanCameraEnabled") === true) {
return true;
}
return null;
@@ -424,19 +419,6 @@ export function shouldSkipWebCameraProbe() {
return !!isSupportWxScan();
}
/**
* 同步判断是否具备 Web 摄像头扫码条件(不申请权限)。
* @deprecated 请用 canUseWebCameraScan保留兼容不再调用 getUserMedia
*/
export function probeWebCameraScan() {
if (shouldSkipWebCameraProbe()) {
return Promise.resolve(false);
}
const supported = canUseWebCameraScan();
printDebug("web camera capability (sync):", supported);
return Promise.resolve(supported);
}
export function resetWebCameraSupportProbe() {
webCameraSupportProbed = null;
webCameraPermissionGrantedCache = null;
@@ -583,6 +565,51 @@ function getZXingReaderWasmUrl() {
return "./" + ZXING_READER_WASM_PATH;
}
let cachedWasmBytes = null;
let cachedWasmBytesUrl = "";
function fetchWasmArrayBuffer(wasmUrl) {
if (cachedWasmBytes && cachedWasmBytesUrl === wasmUrl) {
return Promise.resolve(cachedWasmBytes);
}
return fetch(wasmUrl, { credentials: "same-origin" }).then((response) => {
if (!response || typeof response.arrayBuffer !== "function") {
throw new Error("wasm fetch response is invalid");
}
if (!response.ok) {
throw new Error(`wasm fetch failed: ${response.status}`);
}
return response.arrayBuffer();
}).then((bytes) => {
cachedWasmBytes = bytes;
cachedWasmBytesUrl = wasmUrl;
return bytes;
});
}
function createZxingWasmOverrides() {
const wasmUrl = getZXingReaderWasmUrl();
return {
locateFile(path) {
if (path && path.indexOf(".wasm") !== -1) {
return wasmUrl;
}
return path;
},
instantiateWasm(imports, receiveInstance) {
fetchWasmArrayBuffer(wasmUrl).then((bytes) => {
return WebAssembly.instantiate(bytes, imports);
}).then((result) => {
receiveInstance(result.instance, result.module);
}).catch((err) => {
printWarn("wasm instantiate failed:", err, "url:", wasmUrl);
throw err;
});
return {};
},
};
}
function prepareBarcodeDetectorWithTimeout(options) {
return promiseWithTimeout(
prepareBarcodeDetector(options),
@@ -600,14 +627,7 @@ function prepareBarcodeDetector(options) {
if (!barcodeDetectorPreparePromise || options.forcePonyfill) {
const preparePromise = prepareZXingModule({
fireImmediately: true,
overrides: {
locateFile: path => {
if (path && path.indexOf(".wasm") !== -1) {
return getZXingReaderWasmUrl();
}
return path;
}
}
overrides: createZxingWasmOverrides(),
}).then(() => BarcodeDetectorPonyfill).catch(err => {
if (!options.forcePonyfill) {
barcodeDetectorPreparePromise = null;

View File

@@ -3,12 +3,24 @@ import { readWxLikeEnvFromWindow, getParentWxEnvReport } from "../embedEnvProbe"
import { resolveUseParentProxy } from "../embedProxy";
import { request } from "../../utils/request";
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_SCAN_API = "scanQRCode";
let _wxReadyPromise = null;
let _wxReady = false;
let _wxSignatureUrl = "";
let _wxPendingSignatureUrl = "";
let _wxInitGeneration = 0;
let _urlWatchInstalled = false;
let _urlPollTimer = null;
let _urlReinitTimer = null;
function getWx() {
if (typeof wx !== "undefined") {
@@ -48,25 +60,128 @@ function loadWxScript() {
});
}
/** 微信 JS-SDK 签名 URL嵌入且走父页代理时用父页地址(与微信内打开的页面一致);可配置 wxJssdkSignatureUrl 覆盖 */
/** 微信 JS-SDK 签名 URL与 SDK 运行时 URL 一致 */
function getPageUrlForWxJssdkSignature() {
const override = getConfig("wxJssdkSignatureUrl");
if (override) {
return String(override).split("#")[0];
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) {
// 跨域父页不可读,回退监听当前页
}
if (resolveUseParentProxy()) {
try {
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
return window.parent.location.href.split("#")[0];
}
} catch (e) {
// 跨域父页不可读 location由调用方配置 wxJssdkSignatureUrl
}
}
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;
}
if (typeof window === "undefined") {
return "";
}
return window.location.href.split("#")[0];
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() {
@@ -139,9 +254,16 @@ export function initWxJssdk() {
if (!isWxEnv()) {
return Promise.resolve();
}
installWxJssdkUrlWatcher();
if (isWxSignatureUrlChanged()) {
resetWxJssdkState();
}
if (_wxReadyPromise) {
return _wxReadyPromise;
}
const generation = _wxInitGeneration;
const signatureUrlAtStart = getPageUrlForWxJssdkSignature();
_wxPendingSignatureUrl = signatureUrlAtStart;
_wxReadyPromise = Promise.all([loadWxScript(), fetchWxConfig()]).then(items => {
const wx = items[0];
const config = items[1];
@@ -157,11 +279,27 @@ export function initWxJssdk() {
}
return new Promise((resolve, reject) => {
wx.ready(() => {
if (generation !== _wxInitGeneration) {
return;
}
const currentUrl = getPageUrlForWxJssdkSignature();
if (signatureUrlAtStart && currentUrl && signatureUrlAtStart !== currentUrl) {
resetWxJssdkState();
initWxJssdk().then(resolve).catch(reject);
return;
}
_wxReady = true;
_wxSignatureUrl = currentUrl;
_wxPendingSignatureUrl = "";
resolve(wx);
});
wx.error(err => {
if (generation !== _wxInitGeneration) {
return;
}
_wxReady = false;
_wxSignatureUrl = "";
_wxPendingSignatureUrl = "";
_wxReadyPromise = null;
reject(err);
});
@@ -170,8 +308,12 @@ export function initWxJssdk() {
}));
});
}).catch(err => {
_wxReady = false;
_wxReadyPromise = null;
if (generation === _wxInitGeneration) {
_wxReady = false;
_wxSignatureUrl = "";
_wxPendingSignatureUrl = "";
_wxReadyPromise = null;
}
throw err;
});
return _wxReadyPromise;

216
types/index.d.ts vendored
View File

@@ -18,6 +18,14 @@ interface ScanConfigOptions {
* 跨域 iframe 无法读取父页地址时需手动设为当前微信内打开的页面链接。
*/
wxJssdkSignatureUrl?: string,
/**
* 页面 URL 变化后是否自动重新初始化微信 JSSDK默认 true避免扫一扫失效
*/
wxJssdkAutoReinitOnUrlChange?: boolean,
/**
* URL 轮询检测间隔(毫秒),用于捕获未走 history API 的 SPA 路由;默认 1000设为 0 关闭
*/
wxJssdkUrlPollInterval?: number,
/**
* 桥接是否启用,默认启用
*/
@@ -53,14 +61,6 @@ interface ScanConfigOptions {
* 是否允许 H5 摄像头扫码true 强制开启(仍需有媒体 APIfalse 强制关闭
*/
webScanCameraEnabled?: boolean,
/**
* @deprecated 请用 webScanCameraEnabled保留兼容
*/
webScanCameraInWechat?: boolean,
/**
* @deprecated 摄像头权限已后置到 startScan请用 webScanVideoAccessTimeout
*/
webScanCameraProbeTimeout?: number,
/**
* startScan 走 Web 摄像头前是否展示权限说明弹窗,默认 true
*/
@@ -196,6 +196,106 @@ interface ScanConfigOptions {
* 扫码成功是否播放提示音,默认启用;任意识别模式匹配成功时生效
*/
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默认 truefalse 时整页跳转
*/
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 授权回跳完整 URLredirectUri须与当前页域名一致且已在公众号登记
* 未配置时默认使用当前页 location.href不含 hash
*/
redirectUri?: string,
},
/**
* 微信JSSDK配置微信环境才会生效配置后会自动初始化微信JSSDK
*/
@@ -319,6 +419,87 @@ interface ScanErrorListenerInfo {
*/
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 授权回跳完整 URLredirectUri覆盖 initWechatPayment.redirectUri */
redirectUri?: string;
}
/** IScan */
interface IScan {
/**
@@ -377,12 +558,29 @@ interface IScan {
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
*/
scanImage(key?: string): void;
/** 由业务/原生传入已选图片 File 识别WebView input.files 异常时使用) */
/**
* 由业务/原生传入已选图片 File 识别WebView input.files 异常时使用)
* @param file 已选图片 File
* @param key 可选,指定后结果仅分发给同 key 的 onScanListener
*/
scanImageFromFile(file: File | Blob, key?: string): void;
/**
* 清除全部监听
*/
clear(): void;
/**
* 发起支付:按 paymentType 决定是否走微信 OAuth再将参数传给业务函数获取 paymentFormData 并调起支付。
* 不代表支付成功,业务须自行查单。
*/
requestPayment(
paymentType: PaymentType,
fetchPaymentFormData: PaymentFormDataProvider,
options?: RequestPaymentOptions
): Promise<PaymentInvokeResult>;
/**
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层、支付宝表单弹层)
*/
closePaymentDialog(): void;
}