跳到主要内容

跳转电子签H5

1. App集成电子签H5兼容性适配

1.1 推荐方式:使用腾讯电子签 WebView 容器 SDK(Android / iOS / HarmonyOS)

如果你的 App 是原生或 Hybrid 应用,推荐直接集成腾讯电子签官方提供的 WebView 容器 SDK。SDK 已封装好人脸核身权限、文件选择器、weixin:// / alipays:// 等 Scheme 拦截、qianapp:// 返回应用协议解析、Cookie / DOM Storage 持久化等所有兼容性细节,最少 10 行代码即可完成接入,免去自行适配 WebView 的各类边界问题。

平台语言系统下限内核
AndroidKotlin / JavaAndroid 7.0 (API 24)系统 WebView (WebKit),不支持 X5 / TBS
iOSSwift / Objective-CiOS 14.3WKWebView
HarmonyOS NEXTArkTSAPI 12+(HarmonyOS NEXT 5.0+)ArkWeb

SDK 获取方式:请联系腾讯电子签对接人员获取最新版本与接入文档。

SDK 提供两种集成模式,按业务场景二选一:

  • 容器模式:全新页面,1 行代码打开签署 H5;SDK 自带顶栏、进度条、关闭确认等 UI。详见下方 §1.1.1。
  • 配置模式:改造已有 WebView,保留客户自有 UI 和导航,只叠加 SDK 兼容性能力。详见下方 §1.1.2。

1.1.1 容器模式 TencentQianWebView.open

SDK 内置承载页(含顶栏、进度条、关闭确认、Edge-to-Edge 适配),调用一次 open 即可拉起签署 H5,回调中拿签署结果。

Android(Kotlin)最小集成
import com.tencent.qian.webview.TencentQianWebView
import com.tencent.qian.webview.TencentQianWebViewConfig
import com.tencent.qian.webview.model.SignCallback
import com.tencent.qian.webview.model.SignResult

TencentQianWebView.open(
activity = this,
url = signUrl, // 业务后端获取的 H5 签署 / 认证链接
config = TencentQianWebViewConfig(topBarTitle = "电子签署"),
callback = object : SignCallback {
override fun onSignResult(result: SignResult) {
// result.action / result.result / result.flowId / result.extras
// 字段语义与本文 §4「返回应用数据格式」完全一致
}
}
)
iOS(Swift)最小集成
import TencentQianWebView

TencentQianWebView.open(
from: self,
url: signUrl,
config: TencentQianWebViewConfig(topBarTitle: "电子签署"),
callbacks: SignCallbacks(onSignResult: { result in
// result.action / result.result / result.flowId / result.extras
})
)
HarmonyOS(ArkTS)最小集成
import {
TencentQianWebView,
TencentQianWebViewConfig,
SignResult,
} from 'tencentqianwebview';

TencentQianWebView.open(
context, // UIAbilityContext
signUrl,
new TencentQianWebViewConfig(), // 可设置 topBarTitle / confirmBeforeClose 等
(result: SignResult) => {
// result.action / result.result / result.flowId / result.extras
},
);

1.1.2 配置模式 TencentQianWebViewConfigurator.apply

如果你已经有一套成熟的 WebView 容器(自有顶栏、进度条、路由、错误页等),希望保留全部自有 UI 而只叠加 SDK 的兼容性能力(人脸核身权限、文件选择器、Scheme 拦截、qianapp:// 协议解析、Cookie 持久化等),就用配置模式。

共同约束(三端都遵守):

  • 不要在 apply 之前 自行 setWebViewClient / WKNavigationDelegate / .onLoadIntercept 等回调,否则会被 SDK 替换。
  • 同一 WebView 实例多次 apply 会替换前次注入,不堆叠
Android(Kotlin)配置模式

apply 必须在 Activity.onCreate 中调用(内部要注册 ActivityResultContracts.RequestMultiplePermissions),activity 必须是 ComponentActivity

import com.tencent.qian.webview.TencentQianWebViewConfigurator
import com.tencent.qian.webview.TencentQianWebViewConfig
import com.tencent.qian.webview.model.SignCallback
import com.tencent.qian.webview.model.SignResult

class MyWebActivity : AppCompatActivity() {
private lateinit var webView: WebView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = findViewById(R.id.my_existing_webview) // 客户自有 WebView

TencentQianWebViewConfigurator.apply(
activity = this,
webView = webView,
config = TencentQianWebViewConfig(),
callback = object : SignCallback {
override fun onSignResult(result: SignResult) { /* ... */ }
},
progressListener = { progress -> /* 同步到客户自有进度条 */ },
)

webView.loadUrl(signUrl)
}
}
iOS(Swift)配置模式

返回的 handle 用于持有 SDK 内部 delegate 引用,必须随客户 VC 一同存活(典型做法:作为 VC 的实例属性)。

import TencentQianWebView

final class MyWebViewController: UIViewController {
private let webView = WKWebView()
private var handle: TencentQianWebViewHandle? // 必须强引用持有

override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
// ...自有布局/顶栏...

handle = TencentQianWebViewConfigurator.apply(
webView: webView,
hostController: self,
config: TencentQianWebViewConfig(),
callbacks: SignCallbacks(onSignResult: { result in /* ... */ })
)
webView.load(URLRequest(url: signUrl))
}
}
HarmonyOS(ArkTS)配置模式

apply 返回一个 TencentQianWebViewHandlers 集合,客户需要把里面的每个回调函数逐个绑定到自有 Web 组件对应的回调上。SDK 不替换、不重建客户的 WebviewController

import {
TencentQianWebViewConfigurator,
TencentQianWebViewConfig,
TencentQianWebViewHandlers,
SignResult,
} from 'tencentqianwebview';

@Entry
@Component
struct MyWebPage {
private controller: webview.WebviewController = new webview.WebviewController();
private handlers: TencentQianWebViewHandlers = TencentQianWebViewConfigurator.apply(
this.controller,
this.getUIContext().getHostContext() as common.UIAbilityContext,
new TencentQianWebViewConfig(),
(result: SignResult) => { /* ... */ },
);

build() {
Column() {
// ...客户自有顶栏 / 进度条...
Web({ src: signUrl, controller: this.controller })
.javaScriptAccess(true)
.domStorageAccess(true)
.fileAccess(false)
.multiWindowAccess(false)
.onControllerAttached(this.handlers.onControllerAttached)
.onLoadIntercept(this.handlers.onLoadIntercept)
.onPageBegin(this.handlers.onPageBegin)
.onPageEnd(this.handlers.onPageEnd)
.onProgressChange(this.handlers.onProgressChange)
.onPermissionRequest(this.handlers.onPermissionRequest)
.onShowFileSelector(this.handlers.onShowFileSelector)
.onAlert(this.handlers.onAlert)
.onConfirm(this.handlers.onConfirm)
.onPrompt(this.handlers.onPrompt)
.onDownloadStart(this.handlers.onDownloadStart)
.onErrorReceive(this.handlers.onErrorReceive)
.onHttpErrorReceive(this.handlers.onHttpErrorReceive)
.onSslErrorEventReceive(this.handlers.onSslErrorEventReceive)
.onRenderExited(this.handlers.onRenderExited)
.javaScriptOnDocumentStart(this.handlers.documentStartScripts)
}
}
}

HarmonyOS 配置模式比 Android / iOS 多了"逐回调绑定"这一步,是因为 ArkUI 的 Web 组件回调是链式属性而非 delegate 对象,必须由客户在 build 中显式声明。漏绑任一回调都会导致对应能力失效,详细回调清单见 SDK 集成包提供的接入说明。

1.2 自行适配方式

如果暂时无法集成上述 SDK,可按本节列出的项目自行适配 WebView。

自检方式:完成适配后,在自有 WebView 中打开 §1.3 兼容性测试自检页,保证全部通过,再回归一下具体的业务场景。

1.2.1 WebView 基础设置

电子签 H5 依赖 DOM Storage、Cookie、第三方 Cookie 三项才能维持登录态与人脸核身上下文,缺一不可。下表列出各项配置在三端的对应做法(按维度分组,逐项核对即可)。

JavaScript

  • Android:webSettings.setJavaScriptEnabled(true)
  • iOS:WKPreferences.javaScriptEnabled = true(iOS 14+ 用 defaultWebpagePreferences.allowsContentJavaScript
  • HarmonyOS:Web 组件 .javaScriptAccess(true)

DOM Storage

  • Android:webSettings.setDomStorageEnabled(true)
  • iOS:默认开启
  • HarmonyOS:.domStorageAccess(true)

Cookie / 第三方 Cookie

  • Android:CookieManager.setAcceptCookie(true) + CookieManager.setAcceptThirdPartyCookies(webView, true)
  • iOS:使用 WKWebsiteDataStore.default()多 WebView 必须共享同一个 WKProcessPool 单例,否则实例间 Cookie 不互通
  • HarmonyOS:URL 加载前调 webview.WebCookieManager.putAcceptCookieEnabled(true)

文件访问(安全:阻止 file://

  • Android:webSettings.setAllowFileAccess(false)
  • iOS:默认禁止
  • HarmonyOS:.fileAccess(false)

字体缩放(防大字号机型把活体检测引导文字撑爆)

  • Android:webSettings.setTextZoom(100)
  • iOS:无需处理
  • HarmonyOS:.textZoomAtio(100)

媒体自动播放

  • Android:webSettings.setMediaPlaybackRequiresUserGesture(false)
  • iOS:configuration.allowsInlineMediaPlayback = true + mediaTypesRequiringUserActionForPlayback = []
  • HarmonyOS:.mediaPlayGestureAccess(false)

视口

  • Android:webSettings.setUseWideViewPort(true) + setLoadWithOverviewMode(true)
  • iOS:默认
  • HarmonyOS:.wideViewModeAccess(true) + .overviewModeAccess(true)

User-Agent(用于电子签后端识别容器版本)

  • Android:webSettings.userAgentString += " qianwv/<ver>"
  • iOS:创建 WKWebView 之前configuration.applicationNameForUserAgent;运行时改 customUserAgent
  • HarmonyOS:onControllerAttached 回调内 controller.setCustomUserAgent(controller.getUserAgent() + ' qianwv/<ver>')

多窗口 / target=_blank

数据库

  • Android:默认开
  • iOS:默认开
  • HarmonyOS:.databaseAccess(true)

创建 WKWebView 时务必复用同一个 WKProcessPool 单例,否则多个 WebView 实例之间 Cookie 不互通,签署完 → 跳到结果页时会丢登录态。

HarmonyOS 的 UA 必须在 onControllerAttached 回调内设置(此时 controller 已 attach 但页面未开始加载),过早或过晚都会失败。

1.2.2 人脸核身:相机 / 麦克风权限链路

电子签 H5 的人脸核身基于 getUserMedia 调相机和麦克风。WebView 默认会直接拒绝永远不弹系统授权框,必须在原生层接管。

Android:实现 WebChromeClient.onPermissionRequest(PermissionRequest),把 Web 资源映射到系统权限:

override fun onPermissionRequest(request: PermissionRequest) {
val androidPerms = request.resources.mapNotNull { web ->
when (web) {
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Manifest.permission.CAMERA
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Manifest.permission.RECORD_AUDIO
else -> null // MIDI / ProtectedMediaId 不支持
}
}.distinct().toTypedArray()
if (androidPerms.isEmpty()) { request.deny(); return }
requestRuntimePermissions(androidPerms) { grants ->
if (grants.values.all { it }) request.grant(request.resources) else request.deny()
}
}

权限申请必须使用 ActivityResultContracts.RequestMultiplePermissions,并在 Activity.onCreate 之前registerForActivityResult 注册 launcher,否则抛 IllegalStateException

iOS:在 Info.plist 配置好相机 / 麦克风用途文案,系统会在 H5 调 getUserMedia 时自动弹原生权限框:

<key>NSCameraUsageDescription</key>
<string>用于电子签人脸核身,扫描身份证、采集面部特征</string>
<key>NSMicrophoneUsageDescription</key>
<string>用于电子签人脸核身的活体检测语音验证</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>用于上传签字图片或证件照</string>

iOS 15+ 可选实现 WKUIDelegate.webView(_:requestMediaCapturePermissionFor:initiatedByFrame:type:decisionHandler:) 回调显式控制授权,命中后直接 decisionHandler(.grant) 即可,不要按域名做白名单校验。

HarmonyOS:实现 Web 组件的 .onPermissionRequest((event) => { ... }) 回调,把 H5 请求映射为 HarmonyOS 运行时权限:

.onPermissionRequest((event) => {
const req = event.request;
const webPerms = req.getAccessibleResource(); // TYPE_VIDEO_CAPTURE / TYPE_AUDIO_CAPTURE
const osPerms: Permissions[] = [];
for (const p of webPerms) {
if (p === 'TYPE_VIDEO_CAPTURE') osPerms.push('ohos.permission.CAMERA');
if (p === 'TYPE_AUDIO_CAPTURE') osPerms.push('ohos.permission.MICROPHONE');
}
if (osPerms.length === 0) { req.deny(); return; }
abilityAccessCtrl.createAtManager()
.requestPermissionsFromUser(uiContext, osPerms)
.then((data) => {
const granted = data.authResults.every(r => r === 0);
granted ? req.grant(webPerms) : req.deny();
});
})

module.json5 必须声明权限并提供 reason + usedScene,否则 requestPermissionsFromUser 直接返回拒绝(详见 §1.2.8)。

上述 plist key iOS 11+ 缺失会直接 crash,且系统在 App 整个生命周期内只读一次,运行后再加要重装才生效。

1.2.3 文件选择器(<input type="file">

B2B 场景的资质上传、签署流程的图片上传都依赖 <input type="file">,Android WebView 不实现 onShowFileChooser 时点击无反应;iOS WKWebView 表现略好但不支持 accept MIME 过滤。

Android:覆写 WebChromeClient.onShowFileChooser(...),处理:

H5 写法必须做的事
<input> 默认 / accept="image/*"拉系统文件选择器 / 图库 + 拍照二选一
accept="image/*" capturecapture="user"直接拉相机,可选前置镜头偏好(前置:EXTRA_USE_FRONT_CAMERA = 1
capture="environment"拉相机后置镜头
accept="video/*" capture拉系统录像(MediaStore.ACTION_VIDEO_CAPTURE
multiple 属性Intent.EXTRA_ALLOW_MULTIPLE = true

注意点:ValueCallback<Array<Uri>> 只能 invoke 一次,若用户取消选择必须 callback.onReceiveValue(null) 释放,否则下一次 <input> 永远不响应。配合 FileProviderprovider_paths.xml)输出 EXTRA_OUTPUT 临时拍照文件 URI。

iOS:WKWebView 内部已实现 accept / capture / multiple 的系统调起,通常不需要额外代码,只需在 Info.plist 配置好相机/相册权限文案即可。

HarmonyOS:实现 .onShowFileSelector((event) => { ... return true }),按 event.fileSelector.getAcceptType()isCapture() 派发:

H5 写法HarmonyOS 派发对象
accept="image/*"capture@ohos.file.pickerPhotoViewPicker.select()
其它(PDF / 通用文件)DocumentViewPicker.select()
accept="image/*" capture="user"@ohos.multimedia.cameraPicker 启动相机,前置镜头优先
accept="video/*" capturecameraPicker 视频模式,需要预创建 saveUri 目标文件(鸿蒙 cameraPicker 不会自动创建录像文件)
用户取消必须 event.result.handleFileList([]) 把空数组回给 H5,否则 JS 回调永远悬挂

1.2.4 JS 弹窗(alert / confirm / prompt

部分签署确认提示走 window.alert/confirm/prompt,Android WebView 默认会吞掉(不弹任何东西),iOS WKWebView 默认行为也不可靠,HarmonyOS ArkWeb 默认不展示。

Android:实现 WebChromeClient.onJsAlert / onJsConfirm / onJsPrompt,弹 AlertDialog / MaterialAlertDialogBuilder,回调 JsResult.confirm() / cancel()捕获 BadTokenException:在 Activity 销毁动画期间触发会 crash,需检查 isFinishing / isDestroyed

iOS:实现 WKUIDelegaterunJavaScriptAlertPanelWithMessage / runJavaScriptConfirmPanelWithMessage / runJavaScriptTextInputPanelWithPrompt,用 UIAlertController 呈现。调用 present 前要检查目标 VC 不在 isBeingDismissed 状态、view.window != nil,否则会报 warning 或 crash。

HarmonyOS:实现 Web 组件的 .onAlert / .onConfirm / .onPrompt 回调,必须 return true,否则系统直接吞掉。用 UIContext.getPromptAction().showDialog(...) 或自定义 CustomDialog 渲染,分别回调 result.handleConfirm() / result.handleCancel()。Prompt 输入框需自定义弹窗实现,背景必须不透明,否则深色主题下文字与边框看不清。

1.2.5 文件下载(合同 / 模板 / 凭证 PDF)

电子签 H5 的 PDF 合同下载会触发 WebView 的下载行为,默认不会落盘。

Android

webView.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ ->
val req = DownloadManager.Request(Uri.parse(url))
.setMimeType(mimeType)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
(getSystemService(DOWNLOAD_SERVICE) as DownloadManager).enqueue(req)
}

要点:

  • API 23~28 需运行时申请 WRITE_EXTERNAL_STORAGE(API 29+ Scoped Storage 不需要)。
  • 下载完成可选 Intent.ACTION_VIEW 唤起 PDF 阅读器;若无应用响应需 fallback 提示。

iOS:通过 WKNavigationDelegate.decidePolicyFor navigationResponse 识别下载(含 application/pdfapplication/x-ofdContent-Disposition: attachment),用 URLSession 下载到沙盒,再 UIActivityViewController 让用户分享 / 保存。iPad 必须设置 popoverPresentationController.sourceView,否则 crash。

HarmonyOS:实现 Web 组件的 .onDownloadStart((url, userAgent, contentDisposition, mimetype, contentLength) => { ... })

  1. 校验协议:只允许 http(s)://blob: 直接拒(同 iOS/Android,需 H5 用 JSBridge 自行处理);data: / ftp: / file: 等也拒绝。
  2. 解析文件名:优先 Content-Disposition filename* / filename → 退化为 URL 最后一段 → 兜底 timestamp.bin清洗非法路径字符/\、控制字符)。
  3. 下载到沙盒:<context.cacheDir>/qian-download/<filename>fs.unlinkSync 已存在的同名缓存文件,否则 @ohos.request.downloadFile 会以 errCode = 13400002(目标文件已存在)失败。
  4. 下载完成后用 picker.DocumentViewPicker.save({ newFileNames: [filename] }) 让用户选择保存位置,再 fs.copyFile 拷贝并删除缓存;用户取消保存时保留缓存文件路径供业务定位。
  5. 失败时关闭所有打开的 fd 避免泄漏;区分 下载启动失败下载失败 errCode=<code>不支持的下载协议 等错误原因方便排障。

blob: 协议下载需要业务侧 H5 用 JSBridge 把 base64 数据传到原生层,单纯的 WebView 下载链路在三端都无法直接处理。

1.2.6 URL Scheme 拦截(核心

电子签 H5 会在以下场景触发非 http(s) 跳转,必须按 4 级优先级在 WebViewClient.shouldOverrideUrlLoading (Android) / WKNavigationDelegate.decidePolicyFor (iOS) / .onLoadIntercept((event) => boolean) (HarmonyOS) 中拦截:

优先级Scheme处理
1qianapp://解析为返回应用结果对象(参见 §3 返回应用JumpUrl格式§4 返回应用数据格式),不要让 WebView 加载它,关闭页面/导航回业务
2http:// / https://让 WebView 正常加载,不拦截
3白名单 weixin:// / alipays://Android:Intent.parseUri + startActivity;iOS:UIApplication.shared.open;HarmonyOS:@ohos.app.ability.commonopenLink / startAbility
4其它未知 scheme(含 intent://mailto:tel: 等)静默拒绝,不加载、不抛异常

Android 11+ 必备 <queries>:拉起微信 / 支付宝、调起系统相机拍照与录制,需在 AndroidManifest.xml 中显式声明,否则 targetSdk >= 30resolveActivity 永远返回 null:

<queries>
<intent><action android:name="android.intent.action.VIEW"/><data android:scheme="weixin"/></intent>
<intent><action android:name="android.intent.action.VIEW"/><data android:scheme="alipays"/></intent>
<intent><action android:name="android.intent.action.VIEW"/><data android:scheme="https"/></intent>
<intent><action android:name="android.media.action.VIDEO_CAPTURE"/></intent>
<intent><action android:name="android.media.action.IMAGE_CAPTURE"/></intent>
</queries>

iOS LSApplicationQueriesSchemes:iOS 9+ 必须先在 plist 声明 scheme,UIApplication.canOpenURL 才会返回 true:

<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string><string>weixinULAPI</string>
<string>alipay</string><string>alipays</string>
</array>

HarmonyOS:在 .onLoadIntercept 内识别到外链白名单 scheme 后,调用 UIAbilityContext.startAbility({ action: 'ohos.want.action.viewData', uri: url })@ohos.app.ability.common.openLink捕获 BusinessError(未安装目标 App 时会抛错),并 return true 阻止 ArkWeb 加载该 URL。未知 scheme 同样 return true 静默丢弃,避免出现 ERR_UNKNOWN_URL_SCHEME 错误页。

qianapp:// 解析规则:URL 长度 ≤ 8 KB,UTF-8 percent decode,重复 query key 取首个,空 value 保留为 ""(不要丢字段)。三端解析行为必须完全一致

1.2.7 错误处理与崩溃恢复

WebView 在加载主框架、SSL 校验、渲染进程崩溃等关键节点上,三端默认行为各不相同。需逐项接管,避免 Activity 被系统杀掉或安全策略被绕过。

主框架加载错误(弹错误页或自动重试,不上报子资源错误

  • Android:WebViewClient.onReceivedError只处理 request.isForMainFrame
  • iOS:WKNavigationDelegate.didFailProvisionalNavigation
  • HarmonyOS:.onErrorReceive((event) => { ... }),过滤 event.request.isMainFrame()

主框架 HTTP 错误(4xx / 5xx 提示用户)

  • Android:WebViewClient.onReceivedHttpError(同样过滤主框架)
  • iOS:WKNavigationDelegate.decidePolicyFor navigationResponse 中拿 response.statusCode
  • HarmonyOS:.onHttpErrorReceive(同样过滤主框架)

SSL 证书错误永远 cancel,不要 proceed/handleConfirm;放过 = 中间人攻击)

  • Android:WebViewClient.onReceivedSslError(handler, ...) → handler.cancel()
  • iOS:didReceive challenge → .performDefaultHandling
  • HarmonyOS:.onSslErrorEventReceive((event) => event.handler.handleCancel())

渲染进程崩溃(调 reload() 自愈;自愈失败则关闭页面)

  • Android:WebViewClient.onRenderProcessGone 必须 return true,否则 Activity 被 OS 杀掉
  • iOS:webViewWebContentProcessDidTerminate
  • HarmonyOS:.onRenderExited((event) => { ... })

进程被 OS 内存回收(进入页面后检查 webView.url == nil,自动 reload)

  • 三端复用上一项「渲染进程崩溃」的回调入口。

1.2.8 Manifest / Info.plist / module.json5 必备项

Android AndroidManifest.xml

<!-- 网络 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<!-- 相机 + 麦克风(人脸核身) -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

<!-- 文件读取(API <= 32 兼容) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>

<!-- 硬件 feature 标可选,无相机机型也能装 -->
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.microphone" android:required="false"/>

<application>android:usesCleartextTraffic="true" + android:hardwareAccelerated="true"(默认开,但确保不被全局关闭)。承载 Activity 设 android:windowSoftInputMode="adjustResize",防止键盘遮挡输入框。

iOS Info.plist

<key>NSCameraUsageDescription</key><string>用于电子签人脸核身...</string>
<key>NSMicrophoneUsageDescription</key><string>用于电子签人脸核身的活体检测语音验证</string>
<key>NSPhotoLibraryUsageDescription</key><string>用于上传签字图片或证件照</string>
<key>NSPhotoLibraryAddUsageDescription</key><string>用于保存合同 PDF 或签署凭证</string>
<key>LSApplicationQueriesSchemes</key>
<array><string>weixin</string><string>weixinULAPI</string><string>alipay</string><string>alipays</string></array>

HarmonyOS entry/src/main/module.json5

"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_NETWORK_INFO" },
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:microphone_permission_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}
]

reason 必须指向 resources/base/element/string.json 中已声明的字符串资源,usedScene.abilities 中列出的 Ability 都必须真实存在,否则 HAP 安装即拒。

1.2.9 系统栏 / 安全区适配

Android 15(targetSdk = 35)edge-to-edgetargetSdk = 35 时系统强制启用 edge-to-edge,App 内容会绘制到状态栏与导航栏之下;同时 edge-to-edge 模式下手动处理 IME insets 在 Android 16 与不同 OEM 设备上行为不一致(adjustResize 不生效、键盘不推页面、固定定位元素错位等)。

推荐做法:承载 WebView 的 Activity 显式退回传统窗口模式,由系统自动处理状态栏 padding 与键盘避让,全版本可靠:

// Activity.onCreate
WindowCompat.setDecorFitsSystemWindows(window, true) // 退出 edge-to-edge,回归传统窗口
setContentView(R.layout.activity_webview)
<!-- 根布局:让系统自动为状态栏 / 导航栏留出 padding -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- WebView ... -->
</FrameLayout>
<!-- AndroidManifest.xml:键盘弹起时缩小 Activity 可见区域,自动避让 input -->
<activity
android:name=".YourWebViewActivity"
android:windowSoftInputMode="adjustResize" />

不处理则 WebView 顶部内容会被状态栏遮挡、底部输入框会被软键盘 / 导航栏盖住。

iOS 刘海屏 / 灵动岛:承载 WKWebView 的 VC 应正确处理 additionalSafeAreaInsetssafeAreaInsets,避免顶栏文字被状态栏遮挡。

HarmonyOS 状态栏与安全区:状态栏样式(亮 / 暗内容)通过 window.getLastWindow(this.context) 拿到 window 实例,再 setWindowSystemBarProperties({ statusBarContentColor: '#000000' })。承载 ArkWeb 的页面容器需对 safeAreaInsets.top / .bottom 做内边距,否则内容会绘制到状态栏 / 导航条之下。

1.2.10 target="_blank" 与新窗口

电子签 H5 中部分外链(条款、帮助文档)使用 target="_blank"。WebView 默认会请求开新窗口,原生层若不接管将出现"无反应"或抛异常。

AndroidWebChromeClient.onCreateWindow(...) → return false 即丢弃;或拿到目标 URL 后用主 WebView 加载。

iOS:实现 WKUIDelegate.webView(_:createWebViewWith:for:windowFeatures:),用当前 webView load(URLRequest),return nil。

HarmonyOS:在 Web 组件上设置 .multiWindowAccess(false)从源头关闭多窗口能力——target="_blank" 链接会直接在当前 ArkWeb 中打开。

1.2.11 混淆规则(ProGuard / 鸿蒙 consumer-rules)

Android:如果你的 App 启用了 R8 / ProGuard,需保留 WebView 与 JS Bridge 反射调用的类。最小规则:

-keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; }
-keep class android.webkit.** { *; }

HarmonyOS:若 HAR 模块开启 obfuscation,需在 consumer-rules.txt 中保留与 ArkWeb 回调签名相关的入口类(onPermissionRequest / onShowFileSelector / onLoadIntercept 等回调中调用到的业务类),避免 Web 组件回调因符号被裁剪而无法触发。

1.2.12 WebView 销毁时序

直接在 Activity / VC onDestroy / deinitwebView.destroy() 或释放引用,仍可能导致后台线程访问已释放对象 crash。规范的销毁顺序:

stopLoading() → loadUrl("about:blank") → 移出视图层级 → destroy()/置 nil
  • Android:上述顺序之后再调 webView.destroy()
  • iOS:不要在 deinit 内释放,因 WKWebView 内部线程可能仍在执行回调;改为在 VC dealloc 之前的显式生命周期方法(如 viewDidDisappear 一次性清理)。
  • HarmonyOS:在 aboutToDisappearcontroller.stop()controller.loadUrl('about:blank'),并将 controller 引用置 null,让 ArkUI 自动回收。

1.2.13 UniApp App 端

使用 UniApp 构建 App 时,可直接用其 Webview 组件集成电子签 H5。先配置好 App 所需的相机、麦克风、文件存储、文件读取、访问文件系统等权限,权限点同上方 §1.2.8 列表。

返回 UniApp 页面时使用 qianuni:// JumpUrl 协议,详细规范见 §3.3 腾讯电子签H5 → 集成方UniApp

1.2.14 自检建议

完成上述适配后,在自有 WebView 中打开 §1.3 兼容性测试 自检页,逐项检测 Cookie 持久化、getUserMedia 调起、<input> 各类 capture 行为、JS 弹窗、qianapp:// 拦截、下载等通用用例。保证自检全部通过后,再回归一下本业务实际使用到的电子签场景(实名认证、合同签署、批量签署、企业认证、PDF 合同下载、印章管理等),确认全链路无异常。

1.3 兼容性测试

电子签提供一个纯 H5 自检页,可在原生 App WebView、桌面浏览器、UniApp WebView 等任意环境中打开,用于验证当前容器是否满足腾讯电子签 H5 + 人脸核身的运行要求。

自检页地址https://quick.beta.qian.tencent.cn/compatibilityTest

SDK 接入时可通过 compatibilityTestUrl 配置项覆盖默认地址。

自检页覆盖的测试分组:

分组检测内容
自动检测UA 完整字符串、容器识别(腾讯电子签 SDK / 人脸核身老 SDK / 微信 / 企微 / 支付宝 / 浏览器)、JavaScript 是否启用
存储Cookie 读写与持久化、localStorage / sessionStorage 可用性、第三方 Cookie 支持情况
相机getUserMedia 调起、前置 / 后置切换、人脸核身活体检测前置摄像头
文件选择器普通 <input type="file">accept MIME 过滤、capture 属性(user / environment)、多选、视频录制
JS Bridgealert / confirm / prompt 三类弹窗(iOS WKWebView 默认不弹,验证容器是否补齐)
协议qianapp:// 返回应用协议拦截(仅原生 App 容器有效)、weixin:// / alipays:// 等外链跳转
下载同域 / 跨域 PDF、CSV、图片等文件下载行为

img

2. 获取电子签H5链接

可参考 获取跳转小程序查看或签署链接生成子客登录链接 ,以及其它与H5相关的接口。

H5签署、认证等链接因涉及人脸核身,不支持在iframe中使用,请参考“3.1 腾讯电子签H5 → 集成方H5”配置JumpUrl串联业务流程。

3. 返回应用JumpUrl格式

App集成H5时,在电子签H5页面完成签署之后,一般需要返回到集成方的H5或原生页面。针对这两种不同的集成方式,集成方需要传不同的JumpUrl数据格式与兼容性适配。

3.1 腾讯电子签H5 → 集成方H5

该场景表现为:双方H5均在普通浏览器或APP Webview中,在腾讯电子签H5中完成签署之后,再返回到集成方H5页面。

JumpUrl格式: https://YOUR_CUSTOM_URL/xxxx,只需满足 https:// 开头的正确且合规的网址即可。

注意:根据安全要求,浏览器或Webview可能不允许跳转到普通的http协议。

返回应用时携带的数据:当query中有appendResult=qian时,在YOUR_CUSTOM_URL后面添加签署结果的query参数,具体格式参考 返回应用数据格式 ,比如 https://YOUR_CUSTOM_URL?flowId=xxx&action=sign&result=success&from=tencent_ess。 否则,直接跳转到原始的JumpUrl。

3.2 腾讯电子签H5 → 集成方原生App

该场景表现为:通过原生App的Webview组件承载腾讯电子签H5,在腾讯电子签H5中完成签署之后,再返回到原生页面。

JumpUrl格式: qianapp://YOUR_CUSTOM_URL,只需满足 qianapp:// 开头的URL即可。

注意:APP实现方,需要拦截Webview地址跳转,发现url是 qianapp:// 开头时跳转到原生页面。

返回应用时携带的数据:仅当query中有appendResult=qian时,在YOUR_CUSTOM_URL后面添加签署结果的query参数,具体格式参考 返回应用数据格式 ,比如 qianapp://YOUR_CUSTOM_URL?flowId=xxx&action=sign&result=success&from=tencent_ess。 否则,直接跳转到原始的JumpUrl。

3.3 腾讯电子签H5 → 集成方UniApp

该场景表现为:通过UniApp App端的Webview组件承载腾讯电子签H5,在腾讯电子签H5中完成签署之后,再返回到UniApp页面。

JumpUrl格式: qianuni://{YOUR_JSON_DATA},需满足 qianuni:// 开头且后面的JSON需满足以下 QianUniRouteType 格式。

例如,qianuni://{"method":"reLaunch","payload":{"url":"/pages/index/result?foo=bar"},"appendResult":"qian"}

type QianUniRouteType = {
method: 'navigateTo' | 'navigateBack' | 'switchTab' | 'reLaunch' | 'redirectTo',
payload: Object,
appendResult: 'qian', // 可选字段
}

其中的 payload 的数据格式,参考 UniApp 路由相关几个函数(如 uni.navigateTo / uni.navigateBack / uni.switchTab / uni.reLaunch / uni.redirectTo)的参数。

仅 appendResult 等于 'qian' 时,将在 payload.url 的 query 参数中附加签署成功状态参数,具体格式参考 返回应用数据格式 ,比如,payload.url 自动附加的结果为 /pages/index/result?foo=bar&flowId=xxx&action=sign&result=success&from=tencent_ess。 否则,直接使用原始的 payload.url 完成页面跳转。

注意:跳转路由有绝对路径和相对路径之分。 带斜杠/开头的路由,表示绝对路径; 不带斜杠/开头的路由,表示相对路径。推荐统一使用/开头的绝对路径,可避免出现找不到页面的情况。

4. 返回应用数据格式

返回到集成方应用时,每个字段的TS类型定义如下:

// 签署、填写、拒签,返回格式如下
{
flowId: string,
action: 'fill' | 'sign' | 'reject_fill' | 'reject_sign' | 'view' | '',
result: 'success' | 'fail' | '',
from: 'tencent_ess'
}

注意:有的场景不包含上面的所有字段。比如,批量签署或开通自动签,无flowId。

返回应用的效果如下图所示,支持倒计时自动返回:

img