Mobile Web Integration
Not the same as the native iOS/Android SDKs. If you're looking to integrate Dyneti's compiled native SDK, see the iOS or Android sections instead. This page covers a different scenario: embedding DyScan Web's Modal Flow inside your own native iOS or Android app.
DyScan Web only exists as JavaScript. There's no compiled Swift or Kotlin/Java package for it. To use it from a native app, a web view has to stand in for it, running a shell page that bridges the gap between the native code and the JavaScript SDK.
The bridge page
Below, we present a sample html file that runs in the web view, loads client.js, drives the scan per Modal Flow Usage and Scan Configuration, and forwards the result to native code. Customize it depending on your needs. Both platforms load the same HTML page. Only postToNative itself differs by platform; see the iOS and Android sections below for that one function.
<!-- dyscan-bridge.html, added to your app bundle -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<script src="https://dyscanweb.dyneti.com/static/front_end/dist/client.js"></script>
</head>
<body>
<script>
const API_KEY = 'YOUR_SHAREABLE_API_KEY';
function postToNative(payload) {
/* platform-specific; see iOS or Android section below */
}
async function startDyscan() {
try {
// Build `config` per Modal Flow Usage / Scan Configuration.
const scan = new DyScan.Scan({ key: API_KEY, config: { /* ... */ } });
// scan.begin() never rejects on a failed/canceled scan; every
// outcome resolves as part of `result`. The timeout below is the
// only way to catch the iframe never responding at all.
const result = await Promise.race([
scan.begin(''),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('scan timed out')), 90000)
),
]);
// Forwarded as-is; see the Scan Status docs for what's in
// result.data (status, scanResult, isFraud, declineReasons, ...).
postToNative({ ok: true, result });
} catch (err) {
// Only reaches here for a bridge-level failure; client.js never
// loaded, DyScan undefined, or the timeout above.
postToNative({ ok: false, message: (err && err.message) || String(err) });
}
}
</script>
</body>
</html>
Hosting the page
Host dyscan-bridge.html on a domain you control, reachable over HTTPS and load it from native code the same way you'd load any other web page. Before you ship, register that exact origin with Dyneti as one of the domains hosting the integration mentioned in the Overview prerequisites.
Presentation
We recommend you host the web view as a full-screen native container, not a small sheet or popover. DyScan renders its own modal overlay inside the page, so a cramped native container makes that inner modal feel broken. Dismiss on all three outcomes (success, cancellation, error); don't leave it open waiting for further input once you have a result.
iOS
1. Add the camera usage description
<!-- Info.plist -->
<key>NSCameraUsageDescription</key>
<string>This app uses your camera to scan your payment card during checkout.</string>
2. Configure the WebView
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true // camera preview must render inline; without this, iOS diverts it to native fullscreen playback or blocks it
let webView = WKWebView(frame: .zero, configuration: configuration)
3. Grant the camera permission request
By default, iOS blocks web content from accessing the camera even with NSCameraUsageDescription in place. Your WKUIDelegate has to explicitly grant it:
@available(iOS 15.0, *)
func webView(_ webView: WKWebView,
requestMediaCapturePermissionFor origin: WKSecurityOrigin,
initiatedByFrame frame: WKFrameInfo,
type: WKMediaCaptureType,
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(.grant) // skip this and the camera preview silently never appears; nothing throws
}
4. Load the DyScan bridge page
webView.load(URLRequest(url: URL(string: "https://your-hosted-domain.example/dyscan-bridge.html")!))
5. Bridge the result back to native code
Assemble the bridge page above with iOS's postToNative:
// dyscan-bridge.html; replaces the postToNative placeholder above
function postToNative(payload) {
window.webkit.messageHandlers.dyscanHandler.postMessage(payload);
}
let contentController = WKUserContentController()
contentController.add(coordinator, name: "dyscanHandler")
configuration.userContentController = contentController
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == "dyscanHandler",
let body = message.body as? [String: Any] else { return }
// handle the result
}
Kick off the scan once the page has actually loaded:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("startDyscan();")
}
Minimum iOS version
iOS 15+, driven by the WKUIDelegate camera-permission API in step 3.
Android
Examples below are Kotlin; every API used here (WebSettings, WebChromeClient, WebViewClient, addJavascriptInterface) is a plain Android/AndroidX API with a direct Java equivalent.
1. Add the camera permission
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
2. Configure the WebView
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // off by default on Android (WKWebView has it on by default); DyScan's JS touches localStorage
webView.settings.mediaPlaybackRequiresUserGesture = false // camera preview must start on its own, not wait for a tap
3. Grant the camera permission request
Two layers, both required. First, the normal Android runtime permission (requestPermissions/checkSelfPermission, or ActivityResultContracts.RequestPermission() in Compose). Second, the direct Android equivalent of iOS's WKUIDelegate step: the WebView has its own separate gate in front of getUserMedia() that nothing grants automatically:
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
runOnUiThread {
request.grant(request.resources) // no-op without the OS-level CAMERA permission above; skip this and the preview silently never appears
}
}
}
4. Load the DyScan bridge page
webView.loadUrl("https://your-hosted-domain.example/dyscan-bridge.html")
5. Bridge the result back to native code
Assemble the bridge page above with Android's postToNative:
// dyscan-bridge.html; replaces the postToNative placeholder above
function postToNative(payload) {
AndroidBridge.postMessage(JSON.stringify(payload));
}
webView.addJavascriptInterface(object {
@JavascriptInterface
fun postMessage(json: String) {
runOnUiThread {
// handle the result
}
}
}, "AndroidBridge")
Unlike iOS's WKScriptMessageHandler, which always delivers on the main thread, @JavascriptInterface methods run on a WebView worker thread; any UI update inside one has to explicitly hop back via runOnUiThread (or Handler(Looper.getMainLooper()).post { } outside an Activity, e.g. in Compose).
Kick off the scan once the page has actually loaded:
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
webView.evaluateJavascript("startDyscan();", null)
}
}
Minimum Android version
API 23+ (Android 6.0), driven by the runtime permission APIs (checkSelfPermission/requestPermissions) used in step 3.