# Flutter

> Okyc.start pushes a full-screen verification route (Flutter 3.24+, Android and iOS) and returns a typed result.

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

## Install

Add [`okyc_flutter`](https://pub.dev/packages/okyc_flutter) from pub.dev. It is built on `webview_flutter` 4.x,
plus a small native plugin for screenshots, the Android camera permission and the Android file chooser.

```bash title="Terminal"
flutter pub add okyc_flutter
```

Requirements: Flutter ≥ 3.24, Dart ≥ 3.5, Android minSdk 24, iOS 15.

## Permissions

**iOS:** set the minimum iOS version to 15.0 and add camera and photo-library strings to `Info.plist`:

```xml title="Info.plist"
<key>NSCameraUsageDescription</key>
<string>Take a selfie and photograph your documents to verify your identity.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Some verification steps may record a short video.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose a photo of your document to verify your identity.</string>
```

**Android:** nothing to add. The plugin's manifest declares `INTERNET`, `CAMERA`, the `<queries>` for camera and
document pickers, and a `FileProvider` scoped to `cache/okyc/`.

## Start the flow

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

```dart title="docs_snippets.dart"
OkycConfig makeConfig(String sessionUrl) => OkycConfig(
      sessionUrl: sessionUrl, // the `url` returned by POST /v1/sessions
      locale: 'en', // optional; defaults to the session's language
      theme: OkycTheme.system, // light | dark | system
      allowedHosts: const [], // extra hosts, only when self-hosting
      blockScreenshots: true,
      autoCloseDelay: const Duration(seconds: 2), // result screen stays visible after `complete`
      // Flutter only:
      confirmExit: true, // ask "Exit verification?" on ✕ / back
      title: 'Verify your identity',
    );
```

```dart title="docs_snippets.dart"
class VerifyButton extends StatelessWidget {
  const VerifyButton({super.key});

  Future<void> _start(BuildContext context) async {
    final sessionUrl = await fetchSessionUrlFromYourBackend();
    if (!context.mounted) return;
    final result = await Okyc.start(context, makeConfig(sessionUrl), onEvent: onEvent);
    handleResult(result);
  }

  @override
  Widget build(BuildContext context) =>
      FilledButton(onPressed: () => _start(context), child: const Text('Verify identity'));
}
```

`Okyc.start` pushes a full-screen route with an ✕ button; ✕ and Android back ask "Exit verification?". To place
the flow in your own layout, embed `OkycVerification(config:, onResult:)` — your app then owns navigation and
the exit confirmation:

```dart title="docs_snippets.dart"
class VerifyScreen extends StatelessWidget {
  const VerifyScreen({super.key, required this.sessionUrl});
  final String sessionUrl;

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('Verification')),
        body: OkycVerification(
          config: makeConfig(sessionUrl),
          onEvent: onEvent,
          onResult: (result) {
            Navigator.of(context).pop();
            handleResult(result);
          },
        ),
      );
}
```

### 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). |

Flutter also has: `confirmExit`, `title` (see the SDK README).

## Results and events

```dart title="docs_snippets.dart"
void handleResult(OkycResult result) {
  switch (result) {
    case OkycCompleted(:final sessionId, :final status):
      // UX only: confirm the decision on your backend (webhook or GET /v1/sessions/:id).
      print('finished $sessionId $status');
    case OkycCancelled():
      print('user closed the flow');
    case OkycFailed(:final code, :final message):
      if (code == OkycFailureCode.networkError) print('offline?');
      print('verification failed: $code $message');
  }
}
```

```dart title="docs_snippets.dart"
void onEvent(OkycEvent event) {
  // Payloads carry ids and statuses only — never personal data.
  track('okyc_${event.type.name}', {'sessionId': event.sessionId, ...event.payload});
}
```

`OkycResult` is `OkycCompleted(sessionId, status)`, `OkycCancelled(sessionId)` or `OkycFailed(code, message)`.
The route closes about 2 seconds after `complete`, so the applicant sees the result screen.

Failure codes in Flutter:

| 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.                                  |

## Theming and locale

`theme` (`light`, `dark`, `system`) forces the flow's colour scheme. `locale` is sent as `?locale=` (the token
stays in the fragment); also pass `locale` when you create the session so the flow starts in that language.
`title` sets the app bar title. The SDK's own screens (app bar, loading bar, load-error screen, exit dialog) follow
`theme` too, in the flow's neutral colours rather than your app's theme, because the flow itself uses your brand
colour.

## Security defaults

* Bridge messages arrive on the `OkycFlutter` channel and are dropped unless the WebView is showing the session's
  own origin (not DigiLocker or `allowedHosts`), and when larger than 10 KB, not `{source:'okyc', v:1}`, of an
  unknown type, for another session, or nested.
* The WebView visits only the session origin, DigiLocker and `allowedHosts`; other https links on the top-level
  page open in the system browser; links inside frames, plain http and every other scheme are refused. More than 5 refused navigations in 3 s ends with
  `navigation_blocked`.
* Camera access only while the session origin is shown (and, on Android, after the OS permission).
* On finish: the flow's `sessionStorage` and `localStorage`, the WebView cache and (Android) the camera photos
  are cleared, and screenshot blocking is turned off. Android WebView file access is off. Debugging only in debug
  builds.
* The WebView shares cookies with your app, so the SDK leaves them. If your app has no other web views, call
  `WebViewCookieManager().clearCookies()` after the result to also sign out of DigiLocker.
* `blockScreenshots`: `FLAG_SECURE` on Android; on iOS a privacy cover in the app switcher and while recording.

## Troubleshooting

| Symptom                                  | Fix                                                                             |
| ---------------------------------------- | ------------------------------------------------------------------------------- |
| Local http URL refused                   | Plain http is accepted only for `localhost`, `127.0.0.1`, `10.0.2.2` and `::1`. |
| iOS build fails on the deployment target | Set `platform :ios, '15.0'` in the Podfile (or the Runner target).              |
| `clearCache` affects other WebViews      | On Android the WebView cache is app-wide.                                       |

## Going live

* Create sessions with a `sk_live_…` key on your backend; nothing changes in the app.
* Decide on the [webhook](https://docs.okyc.stream/backend/webhooks.md), not on `OkycCompleted`.
