SDK 事件与前端接入
本页面是 集成方案 中 方案二:API + SDK v2 集成 的前端接入补充说明。
方案二告诉您如何获取 SDK URL 并让用户完成 KYC;本页告诉您在接入方前端页面上:
- 如何嵌入 SDK URL
- 如何监听 SDK 发出的关闭请求,正确销毁 SDK 页面
- 联调阶段可用的 URL 参数
集成方式
从 POST /kyc/individual/sdk/url 获取到 SDK URL(形如 https://otc.legendtrading.com/v2/kyc/s/{code})后,推荐通过以下两种方式在您的前端页面呈现:
方式一:iframe 嵌入
在您的页面用 <iframe> 承载 SDK URL,SDK 作为您页面的一部分展示(适合放在弹窗、抽屉、侧边栏里)。
<iframe
src="https://otc.legendtrading.com/v2/kyc/s/{code}"
allow="camera; geolocation"
style="width: 100%; height: 100%; border: 0;"
></iframe>方式二:打开链接
用 window.open() 打开 SDK URL,把 KYC 流程独立到新窗口完成(适合弹出窗口 / 新标签页场景)。
window.open("https://otc.legendtrading.com/v2/kyc/s/{code}")App WebView 内:window.open() 需要宿主额外开发——宿主必须响应该新窗口请求(Android 用 onCreateWindow,iOS 用 createWebViewWithConfiguration),否则新窗口会白屏。详见下文 window.open() 也需要宿主实现。
SDK 内部在流程结束、或用户主动退出时,需要关闭承载它的 iframe / 弹窗。但浏览器安全策略不允许 SDK 页面直接关闭您的宿主窗口,所以 SDK 会通过消息通知您、由您来完成关闭动作。详见下文close-requested事件章节。
在手机 App 的 WebView 中嵌入
如果用户是在您的手机 App 内(WebView / WKWebView,而非浏览器)完成 KYC,SDK 运行在该 WebView 中,您的 App 就是宿主。
与浏览器不同,嵌入式 WebView 没有外壳:没有地址栏、没有下载管理器、没有文件选择器、也没有「新建标签页」。因此浏览器免费提供的能力,需要由宿主 App 补齐。具体需要哪些,取决于嵌入方式而非平台——同一个要求在两个平台上都存在,只是 API 名称不同。
其中两项容易被遗漏,且失败都是静默的、不报错:
- 文件上传(PoA 证件上传)——按钮点击无反应。
window.open()(集成方式 → 方式二)——新窗口白屏。
文件上传需要宿主提供文件选择器
当网页需要用户选择文件时,嵌入式 WebView 自身不带文件选择器界面,会把请求交给宿主 App,由 App 负责拉起选择器。
Android 上,如果宿主没有实现该回调,请求会被静默丢弃——按钮点了完全没反应,且不会抛出、记录或向用户展示任何错误。iOS 上使用系统选择器,因此无需宿主实现。
这是 Android 上「上传按钮点击无反应」类反馈最常见的成因。
KYC SDK 会触发该场景:地址证明(PoA)流程可能要求用户上传证件文件(见 联调辅助 中 GPS 失败 → POA 流程 一行),IDV 采集也可能经由文件选择框完成。
接入清单
| # | 事项 | 平台 | 适用场景 |
|---|---|---|---|
| 1 | INTERNET 权限(Android:<uses-permission android:name="android.permission.INTERNET" />) | Android | 始终需要 |
| 2 | 声明 CAMERA 权限并在运行时申请(iOS:NSCameraUsageDescription;GPS PoA 另需 NSLocationWhenInUseUsageDescription) | 两端 | IDV / GPS PoA |
| 3 | Android:实现 WebChromeClient.onShowFileChooser,并始终回传回调 | Android | PoA 证件上传 |
| 4 | 授权 SDK 页面内的摄像头 / 麦克风请求(Android:onPermissionRequest;iOS 15+:requestMediaCapturePermissionForOrigin) | 两端 | IDV 拍摄 |
| 5 | Android:setSupportMultipleWindows(true) 并实现 onCreateWindow · iOS:createWebViewWithConfiguration | 两端 | 仅当您的页面用 window.open() 打开 SDK 时 |
第 3 项遗漏表现为**「点击没有任何反应」;第 5 项遗漏或配置不全表现为白屏**。两者都是静默失败,见下文。
window.open() 也需要宿主实现
window.open() 也需要宿主实现如果您的页面调用 window.open(sdkUrl) 打开 SDK(集成方式 → 方式二),宿主必须处理它。WebView 不带「新建标签页」能力,因此 window.open() 会转为向宿主请求创建新窗口——宿主不响应、或响应不完整,新窗口都无法渲染出 SDK。
各平台 API 名称不同,但义务相同:
- Android——交由
WebChromeClient.onCreateWindow处理。 - iOS——交由
WKUIDelegate的createWebViewWithConfiguration。WebKit 官方文档原文:若未实现该方法,WebView 会取消该次导航——因此在 iOS 上同样是宿主义务,并非 Android 独有。
Android 侧的默认行为在两方面对您不利:
WebSettings.setSupportMultipleWindows默认为false。关闭时,新窗口请求会被当作顶层导航、替换当前页面,onCreateWindow根本不会被调用。WebChromeClient.onCreateWindow默认返回false——AOSP 原文:「该方法的默认实现什么都不做,因此返回 false」。
因此,宿主完全不做处理时,窗口压根不会打开。更麻烦的是:宿主建出了窗口、但新 WebView 配置不全时,会得到白屏——这很容易被误判为「SDK 加载失败」。白屏的确切含义是窗口已建出、但其中的 WebView 无法渲染页面;SDK 是单页应用(SPA),若该新 WebView 未启用 JavaScript,就没有内容可渲染。
以下三种故障形态已在真机验证(Android)。iOS 上若弹窗异常,也值得按同样思路排查:
| 宿主行为 | 用户看到的现象 |
|---|---|
onCreateWindow 返回 false(或未处理) | 点击无反应 |
建出了窗口,但新 WebView 未启用 JavaScript | 白屏 |
建出窗口,且新 WebView 配置完整 | SDK 正常加载 |
iOS 上新窗口若为白屏,排查方向相同:createWebViewWithConfiguration 返回的 WebView 必须用传入的 configuration 创建(WebKit 有此要求),且该 WebView 未禁用 JavaScript。
可用实现如下:
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.setSupportMultipleWindows(true) // 必须开启,否则 onCreateWindow 不会被调用
webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message
): Boolean {
val popup = WebView(view.context).apply {
settings.javaScriptEnabled = true // 必须开启——否则 SDK 白屏
settings.domStorageEnabled = true
webViewClient = WebViewClient()
}
container.addView(popup) // 新 WebView 必须加入视图层级
(resultMsg.obj as WebView.WebViewTransport).webView = popup
resultMsg.sendToTarget() // 必须回传
return true // 必须返回 true
}
}
以下三点缺一不可,漏掉任意一点都会导致弹窗失败:返回true、调用resultMsg.sendToTarget()、把新WebView加入视图层级。AOSP 另注明:返回false却又调用sendToTarget()属未定义行为。
App 内更省事的替代方案。 既然 window.open() 需要这些额外的宿主开发,App 内接入若改为直接加载 SDK URL 或使用 iframe,会比 window.open() 更稳。方式一(iframe)除清单中已列项外没有额外要求。
Android 示例
以下示例涵盖清单中的 Android 回调——onShowFileChooser(文件上传)与 onCreateWindow(window.open() 路径)。
class KycActivity : AppCompatActivity() {
private var filePathCallback: ValueCallback<Array<Uri>>? = null
private val fileChooserLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val callback = filePathCallback ?: return@registerForActivityResult
filePathCallback = null
// 返回 null 表示用户取消了选择 —— 也必须回传,
// 否则该文件选择框将永久失效。
callback.onReceiveValue(
WebChromeClient.FileChooserParams.parseResult(result.resultCode, result.data)
)
}
@SuppressLint("SetJavaScriptEnabled")
private fun buildWebView(): WebView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
webChromeClient = object : WebChromeClient() {
// Android 上必须实现:否则 SDK 中的文件选择框点了不会有任何反应。
override fun onShowFileChooser(
webView: WebView,
callback: ValueCallback<Array<Uri>>,
params: FileChooserParams,
): Boolean {
// 先清掉上一次未完成的回调,否则后续点击会失效。
filePathCallback?.onReceiveValue(null)
filePathCallback = callback
return try {
fileChooserLauncher.launch(params.createIntent())
true
} catch (e: ActivityNotFoundException) {
filePathCallback = null
callback.onReceiveValue(null)
false
}
}
// 让 SDK 可以使用摄像头完成 IDV。
override fun onPermissionRequest(request: PermissionRequest) {
request.grant(request.resources)
}
}
loadUrl("https://otc.legendtrading.com/v2/kyc/s/{code}?layout=mobile")
}
}
回调必须始终回传,包括用户取消的情况。若未调用filePathCallback.onReceiveValue(...)(例如用户直接关掉了选择器),该文件选择框会在本次会话中永久失效,而不是只失效一次。
网页端无法替代或绕过
该要求属于原生宿主的义务,网页端没有任何方式可以替代:
- 没有任何 HTML 属性或 JavaScript 可以把文件选择委托给 WebView——不存在这样的 API。
- iframe 的
allow="camera; geolocation"属性不覆盖文件选择。它对应的是 Permissions Policy,其 feature 列表中不包含<input type="file">;allow对它所声明的两个用途(IDV 拍摄与 GPS 定位)是正确的,但无法授予文件选择权限。
其他宿主环境
- iOS:文件上传无需回调——
WKWebView会自行弹出系统选择器。但 IDV 与 GPS PoA 步骤仍需要配置NSCameraUsageDescription/NSLocationWhenInUseUsageDescription;若使用window.open(),则还需要实现createWebViewWithConfiguration。 - 封装框架:部分框架已替您实现这些回调——
react-native-webview、Cordova / Capacitor 自带;早期版本的flutter_webview没有。若使用封装框架且某项能力不可用,请先确认该框架是否实现了对应回调,再排查其他方向。
URL 参数
在 SDK URL 后可以追加以下 query 参数,用于联调、布局适配、品牌定制、消息安全加固:
| 参数 | 取值 | 是否必填 | 说明 |
|---|---|---|---|
mode | dev | 否 | 切换到联调环境。不传 = 生产环境 |
remote | sandboxapi | 否 | 联调环境的后端子域,必须与 mode=dev 同时传 |
layout | mobile | desktop | 否 | SDK 布局形态。不传或其它值 = desktop |
primary-color | 十六进制颜色 #RGB 或 #RRGGBB(URL 中 # 需转义为 %23,例:%23FF6600) | 否 | 自定义 SDK 主题色(按钮、链接、进度等主色)。仅接受严格的十六进制格式,非法值会被忽略并回退到默认主题色 |
parentOrigin | 您页面的完整 origin(如 https://a.com) | 否(强烈建议填) | 收窄 SDK 向宿主 postMessage 时的 targetOrigin。不传时 SDK 会用 *,意味着任意 origin 都能收到消息 |
联调环境完整示例
https://otc.legendtrading.com/v2/kyc/s/{code}?mode=dev&remote=sandboxapi&layout=mobile&primary-color=%23FF6600&parentOrigin=https://a.com
mode=dev&remote=sandboxapi— 前后端都指向联调环境,不影响生产数据layout=mobile— 使用移动端布局(适合嵌入手机 App WebView 或较窄的容器)primary-color=%23FF6600— 自定义 SDK 主题色为橙色#FF6600(#必须转义为%23)parentOrigin=https://a.com— 只允许https://a.com收到 SDK 发出的关闭请求消息
生产环境示例
生产环境不需要传 mode / remote,直接使用原始 SDK URL 即可。仍然强烈建议追加 parentOrigin 参数以提高安全性,并按需追加 layout / primary-color 完成布局与品牌色适配:
https://otc.legendtrading.com/v2/kyc/s/{code}?layout=mobile&primary-color=%23FF6600&parentOrigin=https://your-domain.com
layout=mobile— 使用移动端布局(不传或其它值 = 桌面布局)primary-color=%23FF6600— 自定义 SDK 主题色,与您的品牌色保持一致(#必须转义为%23)parentOrigin=https://your-domain.com— 收窄 SDKpostMessage的targetOrigin
监听 close-requested 事件
close-requested 事件SDK 页面在流程结束或用户主动退出时,需要关闭承载它的 iframe / 弹窗。由于浏览器安全策略不允许 SDK 页面直接关闭您的宿主窗口,SDK 会通过 postMessage 向您的页面发一条 close-requested 消息,请您帮忙完成关闭动作。
您在宿主页面上监听这条消息,收到后销毁承载 SDK 的容器(并可以在这里衔接您自己的后续业务逻辑)。
消息格式
SDK 向宿主页面发送的 postMessage 消息统一如下:
{
source: "legend-kyc-relay", // 固定值,识别消息来源
type: "close-requested", // 固定值,当前只有这一种消息类型
detail: { // 附加信息,可能为 null
flow: "kyc" | "additional-kyc" | null
}
}字段说明:
source— 固定"legend-kyc-relay",用于将 SDK 消息与页面其它postMessage区分type— 目前只有"close-requested"一种detail.flow— 触发关闭时用户所处的 KYC 流程;kyc= 普通 KYC,additional-kyc= 续填 KYC,null= 用户还未进入 KYC 主流程(例如在初始拦截页触发关闭)
安全边界
监听器必须校验 event.origin === "https://otc.legendtrading.com",否则任意第三方页面都能通过 postMessage 伪造消息、诱发意外关闭行为。
同时建议在 SDK URL 上追加 parentOrigin 参数收窄 SDK 端的 targetOrigin,双向加固。
场景一:iframe 嵌入
假设您用 modal 承载 iframe,用户在 SDK 内触发关闭时需要销毁 iframe / 关闭 modal:
<div id="kyc-modal" class="modal hidden">
<iframe
id="kyc-iframe"
src="https://otc.legendtrading.com/v2/kyc/s/{code}?parentOrigin=https://a.com"
allow="camera; geolocation"
></iframe>
</div>window.addEventListener("message", (event) => {
// 1. 校验消息来源
if (event.origin !== "https://otc.legendtrading.com") return
// 2. 校验消息格式
const { source, type, detail } = event.data || {}
if (source !== "legend-kyc-relay") return
if (type !== "close-requested") return
// 3. 销毁承载 SDK 的 iframe / 关闭 modal
document.getElementById("kyc-iframe")?.remove()
document.getElementById("kyc-modal")?.classList.add("hidden")
// 4. 在这里衔接您自己的业务逻辑,例如:
// - 调用您的接口,重新拉取用户 KYC 状态
// - 跳转到"KYC 已提交"提示页
// - 更新前端 UI(按钮 disable / 显示 loading 等)
// - detail.flow 可以帮您区分是普通 KYC 还是续填 KYC
})场景二:popup 弹窗
您用 window.open() 打开 SDK。popup 端 SDK 收到关闭请求时会自己调用 window.close(),宿主端只做善后:
// 打开 SDK popup
const popup = window.open(
"https://otc.legendtrading.com/v2/kyc/s/{code}?parentOrigin=https://a.com"
)
// 监听 popup 发来的关闭请求
window.addEventListener("message", (event) => {
if (event.origin !== "https://otc.legendtrading.com") return
const { source, type, detail } = event.data || {}
if (source !== "legend-kyc-relay" || type !== "close-requested") return
// popup 端 SDK 自己会 window.close(),宿主端在这里做善后:
// - 调用您的接口,重新拉取用户 KYC 状态
// - 更新前端 UI / 跳转页面
// - detail.flow 可以帮您区分是普通 KYC 还是续填 KYC
// 兜底:如果 popup 因浏览器差异没能自关,宿主端主动关一次
if (popup && !popup.closed) popup.close()
})联调辅助:模拟 Veriff 审核结果
在联调环境(mode=dev&remote=sandboxapi)下,用户在 SDK 内提交人脸识别(IDV)后,SDK 会停留在结果等待页并持续轮询审核结果。联调环境没有真实的 Veriff 审核,流程会一直停在这里——您需要通过 Legend 开发者平台(DevTools)手动模拟一次 Veriff 审核结果,SDK 才能继续往下走。
操作步骤
- 在 SDK 内正常提交人脸识别,SDK 进入结果等待页(开始轮询)
- 打开 DevTools:
https://otcsb.legendtrading.com/integration/login,输入 Legend 提供的访问密码登录(仅联调环境可用) - 进入「用户管理」,通过用户 ID、邮箱或手机号搜索到该测试用户,点击进入用户详情页
- 在「模拟 Veriff 操作」区域,按您要验证的流程点击对应按钮(三选一):
| 按钮 | 模拟的审核结果 | SDK 后续走向 |
|---|---|---|
| GPS 成功流程 | IDV 通过 + GPS 定位地址与用户填写地址一致 | GPS PoA 直接通过,无需上传地址证明 |
| GPS 失败走 POA 流程 | IDV 通过 + GPS 地址不匹配 | SDK 转入文件 PoA,引导用户上传地址证明 |
| POA 流程 | IDV 通过,不附带 GPS 地址 | 不经 GPS 验证,SDK 直接进入文件 PoA |
- 点击后稍等片刻,SDK 轮询到审核结果即会自动进入下一步
三个按钮模拟的都是「Veriff 审核通过」,区别只在 PoA(地址证明)分支的走向。联调环境不会对证件真伪、人脸相似度做真实校验。
当流程要求用户上传地址证明时,可以改用预制模板文件代替真实证件——校验会自动通过。请到 Sumsub 官方的 Verification document templates 页面下载;接受与被拒绝的模板样式另见 PoA 上传接口。
注意事项
- 必须先在 SDK 提交人脸识别:该操作等效于 Veriff 服务器向 Legend 发送的「审核通过」回调,要求测试用户已存在 Veriff 会话。若点击后提示「不存在 Veriff 记录」或「不存在 attempts 记录」,说明该用户还没有提交过人脸识别——请先回到 SDK 完成提交,再来点击按钮。
- 仅联调环境可用:DevTools 及模拟功能仅部署在联调(sandbox)环境,生产环境无此入口,也不会影响生产数据。
常见问题
联调时用户提交人脸识别后,SDK 一直停在等待页不动?
联调环境没有真实的 Veriff 审核,SDK 会一直轮询等待审核结果。请到开发者平台(DevTools)的「用户管理 → 用户详情 → 模拟 Veriff 操作」手动触发一次审核结果,SDK 即会继续。详见上文 联调辅助:模拟 Veriff 审核结果。
收不到 close-requested 消息怎么排查?
close-requested 消息怎么排查?- 确认
window.addEventListener("message", ...)在 iframe / popup 打开之前就已挂上,避免消息比监听器早到 - 检查
event.origin校验值 —— 生产、联调都是https://otc.legendtrading.com(mode/remote只切换后端 API,不影响 SDK 页面 origin) - 检查
parentOrigin参数拼接是否正确 —— 必须是完整 origin(如https://a.com),不带路径、不带尾斜杠 - 打开浏览器 DevTools Console 查是否有
postMessage相关报错
primary-color 传了没生效怎么排查?
primary-color 传了没生效怎么排查?- 确认
#已在 URL 中转义为%23—— 直接写#会被浏览器当作 fragment 截断,SDK 收不到 - 确认取值符合
#RGB或#RRGGBB格式(3 位或 6 位十六进制)—— 其它写法(rgb()、颜色名、8 位带透明度等)都会被安全校验拦截、回退到默认色 - 确认参数名是
primary-color(中划线),不是primaryColor或primary_color
SDK URL 有效期是多久?
单个 SDK URL 有效期为 24 小时。过期后需要重新调用 POST /kyc/individual/sdk/url 获取新 URL。
用户中途关闭后再次打开同一个 SDK URL,进度还在吗?
在。同一个 URL 在 24 小时内可以重复打开,SDK 会自动恢复到用户上次的进度,无需您额外处理。
移动端 App 内嵌 WebView 需要注意什么?
- WebView 需要开启 JavaScript 与
postMessage支持(iOSWKWebView/ AndroidWebView默认已开启) - WebView 需要授予 摄像头 与 地理位置 权限,否则 IDV / GPS PoA 环节会失败
- 嵌入式 WebView 没有浏览器外壳,部分能力需宿主 App 自行补齐。Android 上必须实现
WebChromeClient.onShowFileChooser,否则 PoA 证件上传会点击无反应——静默失败、没有任何报错。若您的页面用window.open()打开 SDK,宿主还必须响应新窗口请求(Android:onCreateWindow;iOS:createWebViewWithConfiguration),否则新窗口会白屏。完整清单与示例见 在手机 App 的 WebView 中嵌入。 - 建议使用
?layout=mobileURL 参数切换到移动端布局
如何知道用户 KYC 最终审核结果?
close-requested 只表示用户从 SDK 侧完成或退出,不代表 KYC 已经通过。KYC 审核结果需要通过 kyc-status Webhook 或 POST /app/kyc-status 轮询获取,详见 集成方案 → 实时获取 KYC 审核状态。
Updated 13 days ago