Skip to content

Mobile apps (iOS and Android)

tryVidimus has no native SDK: the capture page is a web page, and your app only hosts it. This guide shows how to open it in a web view, allow the camera, and get back to your app.

How it works

Your server creates the capture session (POST /v1/capture-sessions) and sends the capture_url to your app. The app opens that address and the person takes the photos on the page, with the live camera: there is no import from the gallery. You have two ways to open it. In a web view inside your app (recommended: the person never leaves your app, and you can catch the end of the capture). Or in the phone's browser (Safari, Chrome, or an in-app browser tab such as SFSafariViewController or Chrome Custom Tabs): simpler, but the person returns to your app through a link.

What the capture page needs

An up-to-date system web view (iOS 14.3 or later for the camera in WKWebView; a current Android System WebView). JavaScript on. DOM storage on: the page keeps the photos on the phone so that a closed page can be resumed. The camera permission, granted by the person and by your app. Network access to the capture host and to the file storage the page uploads to (do not block other hosts). Use a real phone to test: simulators have no camera.

iOS: the camera permission

Add the reason for the camera to your Info.plist. Without it the app is stopped by the system the first time the camera is requested. Write it in the language of your app.

<key>NSCameraUsageDescription</key>
<string>Take the photos requested for your booking.</string>

iOS: a WKWebView

Open capture_url in a WKWebView. Grant the camera to the capture page only (iOS 15 and later: requestMediaCapturePermissionFor), and refuse any other site. Do not use UIWebView, which is removed.

import WebKit

final class CaptureViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
    let captureHost = "capture.tryvidimus.com"
    let returnHost = "app.example.com"      // the site you declared
    var captureURL: URL!                     // capture_url, received from YOUR server

    override func viewDidLoad() {
        super.viewDidLoad()
        let config = WKWebViewConfiguration()
        config.allowsInlineMediaPlayback = true
        let webView = WKWebView(frame: view.bounds, configuration: config)
        webView.uiDelegate = self
        webView.navigationDelegate = self
        view.addSubview(webView)
        webView.load(URLRequest(url: captureURL))
    }

    // iOS 15+: the capture page may use the camera, no other site may.
    func webView(_ webView: WKWebView,
                 requestMediaCapturePermissionFor origin: WKSecurityOrigin,
                 initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType,
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        decisionHandler(origin.host == captureHost ? .grant : .deny)
    }

    // The page ends by going to your return_url: catch it and close the screen.
    func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = action.request.url, url.host == returnHost,
           url.path == "/bookings/4471/capture-done" {
            decisionHandler(.cancel)
            dismiss(animated: true)          // url carries ?proof_id=…&session_id=…
            return
        }
        decisionHandler(.allow)
    }
}

Android: the camera permission

Declare the camera in the manifest. The permission is also requested at run time, as shown below: the web view does not ask the person by itself.

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />

Android: a WebView

Turn on JavaScript and DOM storage. In onPermissionRequest, check that the request comes from the capture host, ask the person for the CAMERA permission if needed, then grant RESOURCE_VIDEO_CAPTURE. Deny anything else.

class CaptureActivity : ComponentActivity() {
    private val captureHost = "capture.tryvidimus.com"
    private val returnHost = "app.example.com"   // the site you declared
    private var pending: PermissionRequest? = null

    private val askCamera =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
            pending?.let {
                if (granted) it.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) else it.deny()
            }
            pending = null
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val webView = WebView(this)
        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true            // the page keeps the photos to resume
        webView.settings.mediaPlaybackRequiresUserGesture = false

        webView.webChromeClient = object : WebChromeClient() {
            override fun onPermissionRequest(request: PermissionRequest) {
                if (request.origin.host != captureHost) { request.deny(); return }
                runOnUiThread { pending = request; askCamera.launch(Manifest.permission.CAMERA) }
            }
        }
        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
                val url = request.url
                if (url.host == returnHost && url.path == "/bookings/4471/capture-done") {
                    finish()                                  // url carries proof_id and session_id
                    return true
                }
                return false
            }
        }
        setContentView(webView)
        webView.loadUrl(intent.getStringExtra("capture_url")!!)   // from YOUR server
    }
}

Back to your app

At the end of the capture the page goes to the return_url you passed (4 seconds, with a Back button). In a web view, catch that navigation yourself (decidePolicyFor on iOS, shouldOverrideUrlLoading on Android), close the screen and continue in your app: no universal link is needed. If you opened the phone's browser instead, the person comes back through a universal link or app link on your site. In both cases the return_url must be an https address on a site you declared (Sites page of your account or the settings API), and it carries proof_id and session_id. App-scheme addresses such as myapp:// are refused on purpose: another app could claim them.

Add the Associated Domains capability (applinks:app.example.com) and serve this file from your site, without redirection. Replace TEAMID and the bundle identifier.

// https://app.example.com/.well-known/apple-app-site-association
{
  "applinks": {
    "details": [
      { "appIDs": ["TEAMID.com.example.app"],
        "components": [{ "/": "/bookings/*/capture-done" }] }
    ]
  }
}

Add this intent filter to your activity and serve /.well-known/assetlinks.json from your site with your app's package name and signing certificate fingerprint.

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" android:host="app.example.com"
        android:pathPrefix="/bookings" />
</intent-filter>

React Native, Flutter, Capacitor

The same rules apply, through the web view component of the framework. React Native (react-native-webview): enable allowsInlineMediaPlayback, grant the camera on iOS with mediaCapturePermissionGrantType, request the Android CAMERA permission with PermissionsAndroid, and catch the end with onShouldStartLoadWithRequest. Flutter (flutter_inappwebview or webview_flutter): request the camera with permission_handler and grant it to the capture host only. Option names change between versions: test on a real phone, on iOS and Android.

Security

Create sessions from your server only, with your API key, and hand the app the capture_url alone: the link is valid for a limited time and ends with the capture. Let the web view load only your hosts, the capture host and the file storage; do not inject scripts into the page; do not write the capture_url in logs or analytics.

If something does not work

The page says the camera is denied: the app lacks the permission (Info.plist or manifest), or the person refused it; settings let them change it. A black picture or a page that does not open the camera: an old web view, JavaScript or DOM storage off, or the iOS delegate / Android onPermissionRequest missing. The person ends on the completion page instead of coming back: the return_url was not passed, its site is not declared (422 return_url_not_allowed at creation), or the app does not catch the navigation. The app does not know the capture is over: use the capture_session.completed webhook, not the redirection.

Never put your API key in the app: your server creates the capture session and hands the app the capture_url. And never rely on the return alone: wait for the webhook, or read the proof from your server.

Return to your app after the captureSee the API reference