> ## Documentation Index
> Fetch the complete documentation index at: https://developers.argosidentity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# LiveForm WebView Integration Guide

> How to integrate LiveForm in a WebView for iOS, Android, and React Native mobile apps

This guide explains how to integrate LiveForm as a WebView in a customer mobile app.

**Audience:** Mobile engineers integrating LiveForm into an existing iOS or Android app.

## Implementation Goals

The customer app must implement the following flow:

1. Build the LiveForm URL using the `pid` received from the customer server or app.
2. Check app permissions required by LiveForm before opening the WebView.
3. Open the LiveForm URL in a full-screen WebView.
4. Configure the WebView to support camera capture, image upload, cookies, and external link navigation.

## Supported Versions and Minimum Requirements

LiveForm is an HTTPS-based web application. The app's WebView must fully support modern browser features, camera permissions, file selection, and cookie/storage.

| Platform        | Minimum                                    | Recommended                             | Notes                                                                                                          |
| --------------- | ------------------------------------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| iOS             | iOS 15+                                    | iOS 16+                                 | iOS 15+ supports the `WKUIDelegate` media capture permission callback to restrict grants to the LiveForm host. |
| Android         | Android 9, API 28+                         | Android 10, API 29+                     | Use the latest Android System WebView or Chrome for stable file selection and camera capture.                  |
| Android WebView | Updatable Android System WebView or Chrome | Latest Android System WebView or Chrome | File selection, camera capture, and cookie/storage behavior can be affected by WebView/Chrome version.         |
| React Native    | `react-native-webview` 13.x+               | Latest stable                           | Native apps can implement the same functionality directly using `WKWebView`/Android `WebView`.                 |

Device testing scope:

* Verify the full flow on Android 9 (API 28)+: app install, WebView loading, file selection, camera capture, cookie/storage.
* Verify the full flow on iOS 15+: `WKWebView` file selection, camera capture, cookie/storage.
* Final approval requires completing a real LiveForm submission — camera capture, upload, submit, and returnUrl/deep link navigation.

## LiveForm URL

The customer app uses only the LiveForm production host. Pass `pid` as a query parameter.

```
https://form.argosidentity.com?pid={pid}
```

<Warning>
  Always URL-encode `pid` before inserting it into the URL.
</Warning>

The app does not need to distinguish between hosts or form type paths — the `pid` alone identifies the LiveForm session.

## App Permissions

LiveForm uses the camera and image upload features inside the WebView. When running LiveForm as a WebView in a mobile app, the customer app must obtain camera and file access permissions before the WebView opens — permissions that would normally be handled by the browser.

<Warning>
  Requesting and verifying camera and photo library (or media) permissions at the app level before opening the WebView is required. If required permissions are not granted, LiveForm camera capture or image upload may not work — do not open the WebView in that state.
</Warning>

<Tabs>
  <Tab title="iOS">
    Add the following usage descriptions to `Info.plist`:

    ```xml theme={null}
    <key>NSCameraUsageDescription</key>
    <string>Camera access is required for LiveForm ID document capture.</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>Photo library access is required for LiveForm image upload.</string>
    ```
  </Tab>

  <Tab title="Android">
    Add the following permissions to `AndroidManifest.xml`:

    ```xml theme={null}
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    ```

    <Note>
      If image upload is supported, also configure Android photo/media permissions matching your app's target SDK and file chooser implementation.
    </Note>
  </Tab>
</Tabs>

## Required Integration Flow

```
User action
  └─ Customer app starts LiveForm
      └─ Build LiveForm URL from pid

Before WebView opens
  ├─ Check camera permission
  ├─ Check photo library/media permission
  └─ Open full-screen WebView

Inside WebView
  ├─ Allow JavaScript and DOM storage
  ├─ Allow cookies
  ├─ Allow inline media and media capture
  ├─ Load http/https/data/about URLs inside WebView
  └─ Forward custom schemes to external apps
```

Do not open the WebView if the URL is invalid or permission checks have not completed.

## WebView Configuration

The WebView must allow JavaScript, storage, cookies, media playback, and media capture.

<Tabs>
  <Tab title="React Native">
    When using `react-native-webview`, the underlying `WebChromeClient` (Android) and `WKUIDelegate` (iOS) details are handled by the library's native layer. Set the WebView props below, then verify the Android and iOS checklists separately on real devices.

    ```tsx theme={null}
    import { Linking } from 'react-native';
    import { WebView } from 'react-native-webview';

    function openExternalUrl(url: string) {
      Linking.canOpenURL(url)
        .then((supported) => {
          if (supported) return Linking.openURL(url);
          return undefined;
        })
        .catch(() => undefined);
    }

    export function LiveFormWebView({ url }: { url: string }) {
      return (
        <WebView
          source={{ uri: url }}
          originWhitelist={['*']}
          javaScriptEnabled
          domStorageEnabled
          sharedCookiesEnabled
          thirdPartyCookiesEnabled
          allowsInlineMediaPlayback
          mediaPlaybackRequiresUserAction={false}
          mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"
          allowsFullscreenVideo
          allowFileAccess
          mixedContentMode="compatibility"
          setSupportMultipleWindows={false}
          onOpenWindow={({ nativeEvent }) => {
            if (nativeEvent.targetUrl) openExternalUrl(nativeEvent.targetUrl);
          }}
          onShouldStartLoadWithRequest={({ url: nextUrl }) => {
            if (
              nextUrl === 'about:blank' ||
              nextUrl.startsWith('about:') ||
              nextUrl.startsWith('data:') ||
              nextUrl.startsWith('http://') ||
              nextUrl.startsWith('https://')
            ) {
              return true;
            }

            openExternalUrl(nextUrl);
            return false;
          }}
        />
      );
    }
    ```

    **Key Settings Reference**

    | Setting                                             | Reason                                                                            |
    | --------------------------------------------------- | --------------------------------------------------------------------------------- |
    | `javaScriptEnabled`                                 | LiveForm runs as a web application.                                               |
    | `domStorageEnabled`                                 | Allows LiveForm to use browser storage.                                           |
    | `sharedCookiesEnabled` / `thirdPartyCookiesEnabled` | Maintains cookie behavior similar to a standard browser.                          |
    | `allowsInlineMediaPlayback`                         | Allows media UI to operate inside the WebView where supported.                    |
    | `mediaCapturePermissionGrantType`                   | Enables media capture permission handling for the LiveForm host.                  |
    | `setSupportMultipleWindows={false}`                 | Keeps new window flows within the app-controlled WebView path.                    |
    | `onShouldStartLoadWithRequest`                      | Forwards unknown custom schemes to external apps so they don't break the WebView. |
  </Tab>

  <Tab title="Android Native">
    In an Android native app, configure `WebView.settings` and handle camera permission requests and file upload requests in `WebChromeClient`.

    ```kotlin theme={null}
    import android.Manifest
    import android.annotation.SuppressLint
    import android.app.Activity
    import android.content.ActivityNotFoundException
    import android.content.Intent
    import android.net.Uri
    import android.os.Bundle
    import android.webkit.CookieManager
    import android.webkit.PermissionRequest
    import android.webkit.ValueCallback
    import android.webkit.WebChromeClient
    import android.webkit.WebChromeClient.FileChooserParams
    import android.webkit.WebResourceRequest
    import android.webkit.WebSettings
    import android.webkit.WebView
    import android.webkit.WebViewClient
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.ContextCompat
    import android.content.pm.PackageManager

    class LiveFormActivity : AppCompatActivity() {
        private lateinit var webView: WebView
        private var filePathCallback: ValueCallback<Array<Uri>>? = null

        private val fileChooserLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                val callback = filePathCallback
                filePathCallback = null

                if (callback == null) return@registerForActivityResult
                if (result.resultCode != Activity.RESULT_OK) {
                    callback.onReceiveValue(null)
                    return@registerForActivityResult
                }

                val data = result.data
                val uris = when {
                    data?.clipData != null -> {
                        val clipData = data.clipData!!
                        Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
                    }
                    data?.data != null -> arrayOf(data.data!!)
                    else -> null
                }

                callback.onReceiveValue(uris)
            }

        @SuppressLint("SetJavaScriptEnabled")
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            val liveFormUrl = intent.getStringExtra("LIVE_FORM_URL") ?: return finish()

            webView = WebView(this)
            setContentView(webView)

            CookieManager.getInstance().setAcceptCookie(true)
            CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)

            webView.settings.apply {
                javaScriptEnabled = true
                domStorageEnabled = true
                mediaPlaybackRequiresUserGesture = false
                mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
                allowFileAccess = true
            }

            webView.webViewClient = object : WebViewClient() {
                override fun shouldOverrideUrlLoading(
                    view: WebView,
                    request: WebResourceRequest,
                ): Boolean {
                    val uri = request.url
                    val scheme = uri.scheme

                    if (scheme == "http" || scheme == "https" || scheme == "about" || scheme == "data") {
                        return false
                    }

                    return try {
                        startActivity(Intent(Intent.ACTION_VIEW, uri))
                        true
                    } catch (_: ActivityNotFoundException) {
                        true
                    }
                }
            }

            webView.webChromeClient = object : WebChromeClient() {
                override fun onPermissionRequest(request: PermissionRequest) {
                    val origin = request.origin.host ?: return request.deny()
                    val allowedHost = origin == "form.argosidentity.com"

                    val cameraGranted = ContextCompat.checkSelfPermission(
                        this@LiveFormActivity,
                        Manifest.permission.CAMERA,
                    ) == PackageManager.PERMISSION_GRANTED

                    val resources = request.resources.filter {
                        it == PermissionRequest.RESOURCE_VIDEO_CAPTURE
                    }.toTypedArray()

                    if (allowedHost && cameraGranted && resources.isNotEmpty()) {
                        request.grant(resources)
                    } else {
                        request.deny()
                    }
                }

                override fun onShowFileChooser(
                    webView: WebView,
                    filePathCallback: ValueCallback<Array<Uri>>,
                    fileChooserParams: FileChooserParams,
                ): Boolean {
                    this@LiveFormActivity.filePathCallback?.onReceiveValue(null)
                    this@LiveFormActivity.filePathCallback = filePathCallback

                    return try {
                        fileChooserLauncher.launch(fileChooserParams.createIntent())
                        true
                    } catch (_: ActivityNotFoundException) {
                        this@LiveFormActivity.filePathCallback = null
                        filePathCallback.onReceiveValue(null)
                        false
                    }
                }
            }

            webView.loadUrl(liveFormUrl)
        }
    }
    ```

    **Android Requirements**

    * Request the `CAMERA` permission at the app level and verify it is granted before opening the WebView.
    * On Android 13+, if image selection requires permissions, handle `READ_MEDIA_IMAGES` or similar according to your file chooser implementation.
    * In `onPermissionRequest`, only grant camera access for the LiveForm host. Do not grant camera permission to all origins.
    * Implement `onShowFileChooser` to ensure `<input type="file">` based uploads work reliably.
  </Tab>

  <Tab title="iOS Native">
    In an iOS native app, use `WKWebView` with JavaScript and inline media playback enabled. On iOS 15+, use the `WKUIDelegate` media capture permission callback to restrict grants to the LiveForm host.

    ```swift theme={null}
    import UIKit
    import WebKit
    import AVFoundation
    import Photos

    final class LiveFormViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
        private var webView: WKWebView!
        private let liveFormURL: URL

        init(liveFormURL: URL) {
            self.liveFormURL = liveFormURL
            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewDidLoad() {
            super.viewDidLoad()

            let configuration = WKWebViewConfiguration()
            configuration.allowsInlineMediaPlayback = true
            configuration.defaultWebpagePreferences.allowsContentJavaScript = true
            configuration.websiteDataStore = .default()

            webView = WKWebView(frame: .zero, configuration: configuration)
            webView.navigationDelegate = self
            webView.uiDelegate = self
            webView.translatesAutoresizingMaskIntoConstraints = false

            view.addSubview(webView)
            NSLayoutConstraint.activate([
                webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
                webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
                webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            ])

            webView.load(URLRequest(url: liveFormURL))
        }

        func webView(
            _ webView: WKWebView,
            decidePolicyFor navigationAction: WKNavigationAction,
            decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
        ) {
            guard let url = navigationAction.request.url else {
                decisionHandler(.cancel)
                return
            }

            if let scheme = url.scheme, ["http", "https", "about", "data"].contains(scheme) {
                decisionHandler(.allow)
                return
            }

            UIApplication.shared.open(url)
            decisionHandler(.cancel)
        }

        @available(iOS 15.0, *)
        func webView(
            _ webView: WKWebView,
            requestMediaCapturePermissionFor origin: WKSecurityOrigin,
            initiatedByFrame frame: WKFrameInfo,
            type: WKMediaCaptureType,
            decisionHandler: @escaping (WKPermissionDecision) -> Void
        ) {
            let allowedHost = origin.host == "form.argosidentity.com"

            decisionHandler(allowedHost ? .grant : .deny)
        }
    }
    ```

    **iOS Requirements**

    * `NSCameraUsageDescription` and `NSPhotoLibraryUsageDescription` must be set.
    * Request camera permission using `AVCaptureDevice.requestAccess(for: .video)` (or equivalent) before opening the WebView and verify it is granted.
    * If the image upload flow is used, verify the Photo Library access policy matches your app's UX requirements.
    * Grant media capture permission only for the LiveForm host.
  </Tab>
</Tabs>

## LiveForm URL Builder

```tsx theme={null}
const LIVE_FORM_ORIGIN = 'https://form.argosidentity.com';

export function buildLiveFormUrl(pid: string) {
  return `${LIVE_FORM_ORIGIN}?pid=${encodeURIComponent(pid.trim())}`;
}
```

## Permission Request Example (Expo/React Native)

```tsx theme={null}
import * as ImagePicker from 'expo-image-picker';

export async function requestLiveFormPermissions() {
  const camera = await ImagePicker.requestCameraPermissionsAsync();
  const mediaLibrary = await ImagePicker.requestMediaLibraryPermissionsAsync();

  return {
    granted: camera.granted && mediaLibrary.granted,
    camera,
    mediaLibrary,
  };
}
```

If permission is denied:

* Do not open the WebView.
* Show a message explaining that camera and photo library permissions are required for LiveForm capture and upload.
* Provide a button to navigate to the app settings screen.

## Full-Screen Layout

LiveForm works best when it occupies as much of the screen as possible.

Recommendations:

* Hide the native header on the WebView route.
* Apply safe area insets to avoid the notch and home indicator.
* If the header is hidden, provide a small app-level back button.
* Do not embed the WebView inside a card or modal — let it fill the screen directly.

```
┌──────────────────────────┐
│  back                    │
│ ┌──────────────────────┐ │
│ │                      │ │
│ │      LiveForm        │ │
│ │      WebView         │ │
│ │                      │ │
│ └──────────────────────┘ │
└──────────────────────────┘
```

## Verification Checklist

Verify on both iOS and Android.

<Info>
  **Verification approach**

  The customer app should implement its own WebView screen based on the sample code provided, then verify the real LiveForm URL and a permissions/feature test page separately.

  * **LiveForm URL mode:** Verify the actual submission flow on the LiveForm production host.
  * **Permissions/feature test page:** Verify WebView storage/cookie, fetch, file input, and camera capture input in a single sequential flow, independent of LiveForm state.

  Passing the permissions/feature test confirms the WebView mechanism is working, but does not guarantee a successful LiveForm submission. Final approval requires passing both modes.
</Info>

<Tabs>
  <Tab title="Common (URL & Permissions)">
    **URL and Navigation**

    * A LiveForm URL in the format `https://form.argosidentity.com?pid={pid}` opens successfully.
    * `pid` is URL-encoded before being passed.
    * External custom schemes open in an external app or fail gracefully.

    **Permissions**

    * Camera permission is verified before entering the WebView on first launch.
    * Photo library/media permission is verified before entering the WebView on first launch.
    * A recoverable message is shown if permission is denied.
    * The "Open Settings" button navigates to the app settings screen.

    **WebView Behavior**

    * LiveForm loads with JavaScript enabled.
    * The camera UI opens when capturing an ID document.
    * The picker opens for image upload.
    * Cookies and storage persist during normal WebView navigation.
    * A loading state is shown while the page loads.
    * A recoverable error message is shown on WebView load error or HTTP error.
  </Tab>

  <Tab title="Android">
    * `android.permission.CAMERA` is declared in `AndroidManifest.xml`.
    * Android runtime camera permission is requested and verified as granted before opening the WebView.
    * `WebChromeClient.onPermissionRequest` is implemented in native WebView code.
    * `WebChromeClient.onShowFileChooser` is implemented in native WebView code.
    * Android System WebView or Chrome is up to date.
    * `javaScriptEnabled` is active.
    * `domStorageEnabled` is active.
    * Cookies and third-party cookies are allowed.
    * Media playback and camera access are not blocked by user gesture policy or WebView settings.
    * `ValueCallback` is called exactly once for all file chooser paths: success, cancel, and failure.
    * File upload results passed as `content://` URIs are handled correctly.
    * The app's file chooser implementation does not expose `file://` URIs directly.
    * The LiveForm file selection UI allows file types matching the `accept` attribute.
    * The LiveForm capture UI opens the camera path matching the `capture` attribute.
    * LiveForm resources are not blocked by HTTPS or mixed content policy.
    * `returnUrl` or deep link navigation is delivered correctly to the external app/browser/customer app.
  </Tab>

  <Tab title="iOS WKWebView">
    * `NSCameraUsageDescription` is declared in `Info.plist`.
    * `NSPhotoLibraryUsageDescription` is declared in `Info.plist`.
    * App-level camera permission request and status check work correctly before opening the WebView.
    * App-level photo library permission request and status check work correctly before opening the WebView.
    * `WKUIDelegate` is set on the native WKWebView.
    * On iOS 15+, the `WKUIDelegate` media capture permission handler is verified.
    * `input type="file"` works in `WKWebView`.
    * Files selected or captured are correctly passed back to the WebView.
    * The LiveForm file selection UI allows file types matching the `accept` attribute.
    * The LiveForm capture UI opens the camera path matching the `capture` attribute.
    * HEIC image uploads are handled correctly by the server or WebView flow.
    * JavaScript execution is enabled.
    * `window.open` or new window requests are handled without blocking, according to app policy.
    * `WKWebView` cookie and website data store policy does not block the LiveForm flow.
    * LiveForm resources are not blocked by HTTPS or mixed content policy.
    * `returnUrl`, universal link, and deep link navigation are handled correctly per app policy.
  </Tab>

  <Tab title="React Native">
    * `javaScriptEnabled` is active in `react-native-webview`.
    * `domStorageEnabled` is active in `react-native-webview`.
    * `sharedCookiesEnabled` and `thirdPartyCookiesEnabled` are configured per app policy.
    * Media capture WebView props do not block camera access.
    * `allowFileAccess` and file selection work on both Android and iOS.
    * Expo/RN permission API returns accurate camera and photo library/media permission status.
    * The `onShouldStartLoadWithRequest`, `onOpenWindow`, and `Linking.openURL` combination handles returnUrl/deep links safely.
    * `onError` and `onHttpError` provide recoverable guidance and a diagnostic report.
  </Tab>
</Tabs>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Camera does not open inside LiveForm">
    Check:

    * Was the app-level camera permission requested and granted before opening the WebView?
    * Is `NSCameraUsageDescription` set on iOS?
    * Is `android.permission.CAMERA` set on Android?
    * Are media capture settings enabled in the WebView?

    <Tip>
      If camera blocking persists in an in-app browser (WebView) environment, see [Android & WebView Camera Troubleshooting](/en/idcheck/getting-started/webview-camera-troubleshooting).
    </Tip>
  </Accordion>

  <Accordion title="Image upload does not work">
    Check:

    * Was photo library or media permission requested and granted?
    * Is `NSPhotoLibraryUsageDescription` set on iOS?
    * Are Android media permissions configured correctly for the target SDK and picker implementation?
  </Accordion>

  <Accordion title="WebView shows a blank screen">
    Check:

    * Does the URL include a valid `pid`?
    * Is the device connected to a network?
    * Are JavaScript and DOM storage enabled?
    * Did the WebView callback receive an HTTP or network error?
  </Accordion>
</AccordionGroup>
