# Android

> Run the verification flow in a hardened WebView from Kotlin (minSdk 24) and get a typed OkycResult.

Source: https://docs.okyc.stream/mobile/android

## Install

The Android library (namespace `in.okyc.sdk`) ships as an AAR, `okyc-release.aar`; ask us at
[support@okyc.stream](mailto:support@okyc.stream) for the current build. Put it in `app/libs` and add it with the
AndroidX libraries it uses:

```kotlin title="install.gradle.kts"
// app/build.gradle.kts — the OKYC library (okyc-release.aar in app/libs) and the AndroidX libraries it uses
dependencies {
    implementation(files("libs/okyc-release.aar"))
    implementation("androidx.core:core-ktx:1.18.0")
    implementation("androidx.appcompat:appcompat:1.8.0")
    implementation("androidx.webkit:webkit:1.17.1")
    implementation("com.google.android.material:material:1.14.0")
}
```

Consumer R8/ProGuard rules ship inside the AAR, so you don't need your own keep rules.

## Permissions

The SDK's manifest merges `INTERNET` and `CAMERA` (with `android.hardware.camera` not required), a
`FileProvider` for camera capture from file inputs, and `<queries>` for image capture and https links. The
camera is requested at runtime only when a step needs it; the microphone is never requested. The sample's
manifest:

```xml title="AndroidManifest.xml"
<!-- The SDK's manifest already merges both; declare them yourself only to make them explicit.
     CAMERA is requested at runtime, and only when the flow needs it (selfie, document capture). -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
```

## Start the flow

Create the session on your backend and pass its `url` to the app.

```kotlin title="MainActivity.kt"
val config = OkycConfig(
    sessionUrl = sessionUrl, // the `url` your backend got from POST /v1/sessions
    locale = locale, // "en" or "hi"; null keeps the session's language
    theme = theme, // LIGHT, DARK or SYSTEM (default)
    blockScreenshots = blockScreenshots, // FLAG_SECURE (default true)
    autoCloseDelay = 2000, // ms the result screen stays visible after `complete`; null = stay open
)
```

```kotlin title="MainActivity.kt"
Okyc.start(
    this, // any Activity
    config,
    onResult = { result -> handleResult(result) },
    onEvent = { event -> handleEvent(event) },
)
```

`Okyc.start` keeps callbacks in memory. To survive process death, launch with the Activity Result contract
instead (results only, no progress events): `registerForActivityResult(OkycContract()) { result -> … }` and
`launch(OkycConfig(sessionUrl))`.

### Options

| Option             | Type                          | Default    | Description                                                                                                                                                                |
| ------------------ | ----------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionUrl`       | `string`                      | required   | The `url` returned by `POST /v1/sessions`. Never embed a secret API key in an app.                                                                                         |
| `locale`           | `string`                      | `null`     | Flow language (`en`, `hi`); must be one of the workflow's languages. Sent as `?locale=`.                                                                                   |
| `theme`            | `light` \| `dark` \| `system` | `"system"` | Colour scheme of the flow. Sent as `?theme=`.                                                                                                                              |
| `allowedHosts`     | `string[]`                    | `[]`       | Extra hosts the WebView may load (self-hosting).                                                                                                                           |
| `blockScreenshots` | `boolean`                     | `true`     | Android: FLAG\_SECURE. iOS: cover the flow in the app switcher and while the screen is recorded.                                                                           |
| `autoCloseDelay`   | `duration_ms`                 | 2000 ms    | How long the result screen stays visible after `complete` before the SDK closes and returns `completed`. `0` or `null` = return the result immediately (no result screen). |

Android also has: `title` (see the SDK README).

## Results and events

```kotlin title="MainActivity.kt"
private fun handleResult(result: OkycResult) {
    when (result) {
        // The applicant finished. Confirm the decision on your backend (webhook or GET /v1/sessions/:id).
        is OkycResult.Completed -> showResult("completed · ${result.sessionId} · ${result.status}")
        // Closed before finishing; the same session URL can be opened again.
        is OkycResult.Cancelled -> showResult("cancelled · ${result.sessionId ?: "—"}")
        // result.code is one of OkycResult.FailureCode (network_error, load_failed, …).
        is OkycResult.Failed -> showResult("failed · ${result.code} · ${result.message}")
    }
}
```

```kotlin title="MainActivity.kt"
private fun handleEvent(event: OkycEvent) {
    // Progress only (ids and statuses, never personal data). The result arrives in handleResult.
    when (event.type) {
        OkycEvent.Type.STEP_STARTED, OkycEvent.Type.STEP_COMPLETED -> logEvent("${event.type.wire} ${event.step}")
        OkycEvent.Type.ERROR -> logEvent("error ${event.payload["code"]}") // recoverable; the flow keeps running
        else -> logEvent(event.type.wire)
    }
}
```

`OkycResult` is `Completed(sessionId, status)`, `Cancelled(sessionId?)` or `Failed(code, message)`. `Completed`
comes after the flow's `complete` event and the auto-close delay; back or ✕ asks "Exit verification?" before
returning `Cancelled`. `OkycEvent` has `type` (`.wire` is the event name), `sessionId` and `payload`.

Failure codes on Android:

| Code                  | Meaning                                                                    |
| --------------------- | -------------------------------------------------------------------------- |
| `invalid_config`      | Missing or malformed session URL or configuration.                         |
| `load_failed`         | The flow page failed to load (HTTP error or blank page).                   |
| `network_error`       | No connection or DNS/TLS failure while loading.                            |
| `navigation_blocked`  | Repeated navigation to non-allowlisted hosts.                              |
| `permission_denied`   | The camera permission was permanently denied and the flow cannot continue. |
| `flow_error`          | The flow reported an unrecoverable error.                                  |
| `webview_unavailable` | The WebView renderer crashed or is unavailable.                            |

## Theming and locale

`theme` (`LIGHT`, `DARK`, `SYSTEM`) is sent to the flow as `?theme=` and also styles the native toolbar and
dialog. `locale` (`en`, `hi`) is sent as `?locale=` and sets the toolbar and dialog language; it must be one of
the workflow's languages. Brand colours and logo come from your dashboard branding.

## Security defaults

* Only the session origin, DigiLocker and `allowedHosts` load in the WebView; other https links open in the
  browser on tap; `intent:`, `file:`, `javascript:` and plain http elsewhere are blocked.
* JavaScript, DOM storage and Safe Browsing on; file/content access, mixed content and third-party cookies off.
  Remote debugging only when your app is debuggable.
* Camera capture is granted only to the flow origin, after the OS permission; audio never.
* `OkycAndroid.postMessage(json)` is the only method exposed to JavaScript, and messages from other origins or
  sessions are dropped.
* `FLAG_SECURE` by default (`blockScreenshots`): blank card in Recents, black screen capture.
* On finish, cookies and storage for the flow and DigiLocker origins, WebView history, form data and cache, and
  the SDK's capture directory are cleared. `clearCache` is app-wide for WebView.
* The session URL is never logged; `OkycConfig.toString()` hides it.

## Troubleshooting

| Symptom               | Fix                                                                                              |
| --------------------- | ------------------------------------------------------------------------------------------------ |
| `invalid_config`      | The URL must be the session `url` (`https://…/s/ses_…#t=st_…`).                                  |
| `permission_denied`   | The user permanently denied the camera; ask them to enable it in Settings.                       |
| `webview_unavailable` | The WebView renderer crashed or was killed; start again.                                         |
| `navigation_blocked`  | The flow tried to load a host that isn't allowlisted; add self-hosted domains to `allowedHosts`. |

## Going live

* Create sessions with a `sk_live_…` key on your backend; nothing changes in the app.
* Keep `blockScreenshots` on and don't log the session URL.
* Decide on the [webhook](https://docs.okyc.stream/backend/webhooks.md), not on `Completed`.
* Remove any cleartext or debug network config from release builds.
