# iOS

> Present the verification flow from UIKit or SwiftUI (iOS 15+) and await a typed OkycResult.

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

## Install

No third-party dependencies (UIKit, WebKit, AVFoundation, SwiftUI).

**Swift Package Manager:** in Xcode, choose File → Add Package Dependencies…, enter
`https://github.com/okyc-team/okyc-ios` and add the `OKYC` product. In a `Package.swift`:

```swift title="Package.swift"
// Package.swift of your app (or Xcode → File → Add Package Dependencies… → https://github.com/okyc-team/okyc-ios).
dependencies: [
    .package(url: "https://github.com/okyc-team/okyc-ios.git", from: "0.1.0"),
],
targets: [
    .target(name: "YourApp", dependencies: [.product(name: "OKYC", package: "okyc-ios")]),
]
```

**CocoaPods:**

```ruby title="Podfile"
pod 'OKYC', '~> 0.1'
```

## Permissions

The flow uses the camera (documents, selfie) and may use the microphone. Add both usage strings to your
`Info.plist`:

```xml title="Info.plist"
<!-- The flow uses the camera (documents, selfie) and may ask for the microphone (liveness). -->
<key>NSCameraUsageDescription</key>
<string>We use the camera to capture your documents and a selfie to verify your identity.</string>
<key>NSMicrophoneUsageDescription</key>
<string>The verification flow may request microphone access for the liveness check.</string>
```

## Start the flow

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

```swift title="OkycExampleApp.swift"
let config = OkycConfig(
    sessionURL: url,        // the `url` your backend got from POST /v1/sessions
    locale: nil,            // "en" or "hi"; nil keeps the session's language
    theme: theme,           // .light, .dark or .system (default)
    blockScreenshots: true, // cover the flow in the app switcher and while recording (default)
    autoCloseDelay: 2,      // seconds the result screen stays visible after `complete`; nil = stay open
    presentation: .sheet    // or .fullScreen
)
```

```swift title="OkycExampleApp.swift"
.okycVerification(
    isPresented: $presenting, // set to true to present the flow
    config: config,
    onEvent: { event in handleEvent(event) },
    onResult: { result in handleResult(result) } // called exactly once per presentation
)
```

Also available: `OkycViewController(config:)` with a delegate (`okyc(_:didReceive:)`,
`okyc(_:didFinishWith:)`), the SwiftUI modifier `.okycVerification(isPresented:config:onEvent:onResult:)`, and
`OkycView` to embed the flow in your own container.

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

iOS also has: `presentation` (see the SDK README).

## Results and events

```swift title="OkycExampleApp.swift"
private func handleResult(_ result: OkycResult) {
    switch result {
    case let .completed(sessionId, status):
        // The applicant finished. Confirm the decision on your backend (webhook or GET /v1/sessions/:id).
        show("completed \(sessionId) \(status)")
    case let .cancelled(sessionId):
        // Closed before finishing; the same session URL can be opened again.
        show("cancelled \(sessionId ?? "-")")
    case let .failed(code, message, _):
        // `code` is one of OkycErrorCode (network_error, load_failed, …).
        show("failed \(code) \(message)")
    }
}
```

```swift title="OkycExampleApp.swift"
private func handleEvent(_ event: OkycEvent) {
    // Progress only (ids and statuses, never personal data). The result arrives in handleResult.
    switch event.kind {
    case .stepStarted, .stepCompleted: log.append("\(event.kind.rawValue) \(event.payload["step"] ?? "")")
    case .error: log.append("error \(event.payload["code"] ?? "")") // recoverable; the flow keeps running
    default: log.append(event.kind.rawValue)
    }
}
```

`OkycResult` is `.completed(sessionId, status)`, `.cancelled(sessionId)` or `.failed(code, message, sessionId)` (`sessionId` is `nil` if the flow never loaded). Swipe-down
on the sheet, ✕, or the flow's own exit returns `.cancelled`. Event payload values are strings.

Failure codes on iOS:

| 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.                            |
| `presentation_failed` | No view controller was available to present the flow.                      |

## Theming and locale

`theme` (`.light`, `.dark`, `.system`) is sent as `?theme=` and applied to the native chrome. `locale` is sent
as `?locale=` before the `#t=` fragment and must be one of the workflow's languages. `presentation` is
`.sheet` (swipe down cancels) or `.fullScreen`.

## Security defaults

* The session URL must use https; plain http is accepted only for `localhost`, `127.0.0.1` and `::1`
  (otherwise `invalid_config`).
* Messages are accepted only from the main frame of the session origin and must match protocol v1.
* The flow origin and DigiLocker stay inside the web view; other https links open in Safari; everything else is
  blocked.
* Camera and microphone are granted only to the flow origin, after the iOS permission.
* A non-persistent website data store, wiped when the flow finishes. The session URL is never logged.
* Web Inspector only in DEBUG builds.
* iOS can't block screenshots; `blockScreenshots` hides the flow in the app switcher and while the screen is
  recorded or mirrored.

## Troubleshooting

| Symptom                                       | Fix                                                           |
| --------------------------------------------- | ------------------------------------------------------------- |
| `OkycConfig(sessionURLString:)` returns `nil` | The string isn't a valid session `url`.                       |
| `presentation_failed`                         | Start from a view controller that is in the window hierarchy. |
| Camera prompt never appears                   | `NSCameraUsageDescription` is missing from `Info.plist`.      |

## 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 `.completed`.
