# iOS Liveness SDK \*New

Our iOS SDK enables seamless integration of real-time liveness detection into your mobile applications. To get started with our SDK, follow the guide below:

## Installation

To install, run:

```sh
pod 'YouverifyLivenessSDKCompat'
```

## Usage

{% stepper %}
{% step %}

### Import the package

```swift
import YouverifyLivenessSDK
```

{% endstep %}

{% step %}

### Initialize the SDK

```swift
private var yvLiveness = YVLiveness(...options)
```

> For a list of the valid options, check the Options section below.
> {% endstep %}

{% step %}

### Start the liveness process

Start with tasks provided during initialization (or pass an array of tasks to override):

```swift
yvLiveness.startSDK(tasks: tasks)
// add the liveness view controller to the current view controller
DispatchQueue.main.async {
    self.addLivenessViewController()
}
```

Example with an explicit task array:

```swift
let livenessTasks = [
    TaskProperties(task:
        YVTask.completeTheCircle(CompleteTheCircleTask(difficulty: .medium))
    )
]

yvLiveness.startSDK(tasks: livenessTasks)
```

{% endstep %}

{% step %}

### Add and remove the liveness view controller

```swift
private var livenessViewController: YVLivenessViewController?

private func addLivenessViewController() {
    guard livenessViewController == nil else { return } // Prevent duplicate adds
    let vc = YVLivenessViewController(sdk: yvLiveness)
    vc.delegate = self
    
    addChild(vc)
    
    view.addSubview(vc.view)
    vc.didMove(toParent: self)
    livenessViewController = vc
}

private func removeLivenessViewController() {
    guard let vc = livenessViewController else { return }
    vc.willMove(toParent: nil)
    vc.view.removeFromSuperview()
    vc.removeFromParent()
    livenessViewController = nil
}
```

{% endstep %}

{% step %}

### Handle closing via delegate

Assign your view controller to the `YVLivenessViewDelegate` to handle closing:

```swift
extension MainViewController: YVLivenessViewDelegate {
    func closeModal() {
        DispatchQueue.main.async {
            self.removeLivenessViewController()
        }
    }
}
```

{% endstep %}
{% endstepper %}

## Full Example

```swift
import YouverifyLivenessSDK

class MainViewController: UIViewController {
    var yvLiveness: YVLiveness?
    
    private var livenessViewController: YVLivenessViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initializeSDK()
        
        // Create a vertical stack view
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 10
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // Create buttons
        let completeCircleButton = createButton(title: "Complete the Circle", action: #selector(completeTheCircleTapped))
        
        // Add buttons to stack view
        stackView.addArrangedSubview(completeCircleButton)
        
        // Add stack view to the main view
        view.addSubview(stackView)
        
        // Set Auto Layout constraints
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
        ])
    }
    
    private func initializeSDK() {
        // start loading...
        ApiClient.shared.generateSessionId(publicMerchantID: publicMerchantID,
                                               apiToken: apiToken) { [weak self] result in
            switch result {
            case .success(let sessionId):
                // Now get token
                ApiClient.shared.generateSessionToken(publicMerchantID: publicMerchantID,
                                                      deviceCorrelationId: sessionId,
                                                      apiToken: apiToken) { result in
                    switch result {
                    case .success(let authToken):
                        // end loading
                        DispatchQueue.main.async {
                            self?.yvLiveness = YVLiveness(
                                publicKey: publicMerchantID,
                                user: YVLivenessUser(firstName: "John"),
                                onSuccess: { data in
                                    print("The success data is \(data)")
                                },
                                onFailure: { errorData in
                                    if let error = errorData.error {
                                        if error.key == "invalid_or_expired_session" {
                                            print("Session expired, retrying...")
                                            self?.initializeSDK()
                                        } else if error.key == "session_token_error" {
                                            print("Session token error, retrying...")
                                            self?.initializeSDK()
                                        }
                                    }
                                },
                                sessionId: sessionId,
                                sessionToken: authToken
                            )
                        }
                    case .failure(let error):
                        print("Token failed:", error)
                    }
                }
            case .failure(let error):
                print("Session failed:", error)
            }
        }
    }
    
    private func addLivenessViewController() {
        guard livenessViewController == nil else { return } // Prevent duplicate adds
        guard let yvLiveness = yvLiveness else { return }
        let vc = YVLivenessViewController(sdk: yvLiveness)
        vc.delegate = self
        
        addChild(vc)
        
        view.addSubview(vc.view)
        vc.didMove(toParent: self)
        livenessViewController = vc
    }

    private func removeLivenessViewController() {
        guard let vc = livenessViewController else { return }
        vc.willMove(toParent: nil)
        vc.view.removeFromSuperview()
        vc.removeFromParent()
        livenessViewController = nil
    }
    
    // Helper function to create buttons with actions
    private func createButton(title: String, action: Selector) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.addTarget(self, action: action, for: .touchUpInside)
        return button
    }
    
    private func startLiveness(with tasks: [TaskProperties]) {
        guard let yvLiveness = yvLiveness else { return }
        yvLiveness.startSDK(tasks: tasks)
        DispatchQueue.main.async {
            self.addLivenessViewController()
        }
    }
    
    @objc private func completeTheCircleTapped() {
        startLiveness(with: [
            TaskProperties(task: .completeTheCircle(CompleteTheCircleTask(difficulty: .medium)))
        ])
    }
}

extension MainViewController: YVLivenessViewDelegate {
    func closeModal() {
        DispatchQueue.main.async {
            self.removeLivenessViewController()
        }
    }
}
```

### Breaking change: session token generation now external

* The SDK no longer generates session tokens internally.
* Partners must call their backend to generate both `sessionId` and `sessionToken` and pass them to the SDK via the respective options.

## Base URL Configuration

All API endpoints use the following base URL:

Sandbox base URL:

```
https://api.sandbox.youverify.co
```

Live base URL:

```
https://api.youverify.co
```

## Session ID Generation

Before initializing the SDK, you must generate a sessionId by calling your backend API. Your backend should make the following request:

Endpoint: `POST /v2/api/identity/sdk/session/generate`

Headers:

* Content-Type: "application/json"
* Token: "YOUR\_API\_KEY"

Request Body:

* publicMerchantID: "your\_public\_merchant\_id",
* metadata": \[:]

Response:

```swift
[
  "sessionId": "generated_session_id_here"
]
```

## Session Token Generation

Additionally, you need to generate a sessionToken for liveness verification.

Endpoint: `POST /v2/api/identity/sdk/liveness/token`

Headers:

* Content-Type: "application/json",
* Token: "YOUR\_API\_KEY"

Request Body:

* publicMerchantID: "your\_public\_merchant\_id",
* deviceCorrelationId: "unique\_device\_identifier"

Response:

```
[
  "authToken": "generated_auth_token_here"
]
```

The authToken from the response should be passed as `sessionToken` to the SDK constructor.

## Complete Integration Flow

* Generate Session ID: Call your backend to generate `sessionId` using the session generation endpoint.
* Generate Session Token: Call your backend to generate `sessionToken` using the liveness token endpoint.
* Initialize SDK: Pass both `sessionId` and `sessionToken` to the SDK constructor.
* SDK Validation: The SDK validates the `sessionId` before initialization.
* Error Handling: If validation fails, `onFailure` is called with key `invalid_or_expired_session` and `session_token_error` for both.
* Success: Upon successful initialization, the SDK uses the `sessionToken` for liveness verification.

### Error Keys

* invalid\_or\_expired\_session: Returned when the sessionId is invalid or expired
* session\_token\_error: Returned when there's an issue with the sessionToken during liveness verification

### Retry Logic

* If session validation fails, generate a new sessionId and retry.
* If liveness fails, users may retry while the current sessionId remains valid.
* If the sessionId expires, create a new session and restart the entire process.

## Token generation

Here's an example of the `ApiClient` class housing the `generateSessionId` and the `generateSessionToken`.

```swift
enum Endpoint {
    case generateSessionId
    case generateSessionToken
    
    var url: URL {
        switch self {
        case .generateSessionId:
            return URL(string: "https://api.sandbox.youverify.co/v2/api/identity/sdk/session/generate")!
            
        case .generateSessionToken:
            return URL(string: "https://api.sandbox.youverify.co/v2/api/identity/sdk/liveness/token")!
        }
    }
}

class ApiClient {
    
    static let shared = ApiClient()
    private init() {}
    
    private let jsonDecoder = JSONDecoder()
    
    // Generic request
    func post<T: Codable>(to endpoint: Endpoint,
                          apiToken: String,
                          body: [String: Any],
                          completion: @escaping (Result<T, Error>) -> Void) {
        
        var request = URLRequest(url: endpoint.url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiToken, forHTTPHeaderField: "Token")
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: body)
        } catch {
            completion(.failure(error))
            return
        }
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            
            if let error = error {
                completion(.failure(error))
                return
            }
        
            guard let data = data else {
                completion(.failure(URLError(.badServerResponse)))
                return
            }
            
            do {
                let model = try self.jsonDecoder.decode(T.self, from: data)
                completion(.success(model))
            } catch {
                completion(.failure(error))
            }
            
        }.resume()
    }
}

extension ApiClient {
    
    func generateSessionId(publicMerchantID: String,
                           apiToken: String,
                           completion: @escaping (Result<String, Error>) -> Void) {
        
        let body: [String: Any] = [
            "publicMerchantID": publicMerchantID,
            "metadata": [:]
        ]
        
        post(to: .generateSessionId, apiToken: apiToken, body: body) { (result: Result<SessionIdResponse, Error>) in
            switch result {
            case .success(let response):
                completion(.success(response.data.sessionId))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
    
    func generateSessionToken(publicMerchantID: String,
                              deviceCorrelationId: String,
                              apiToken: String,
                              completion: @escaping (Result<String, Error>) -> Void) {
        
        let body: [String: Any] = [
            "publicMerchantID": publicMerchantID,
            "deviceCorrelationId": deviceCorrelationId
        ]
        
        post(to: .generateSessionToken, apiToken: apiToken, body: body) { (result: Result<AuthTokenResponse, Error>) in
            switch result {
            case .success(let response):
                completion(.success(response.data.authToken))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

```

## Options

| Option                      | Type           | Required       | Description                                                                                                                          | Default Value | Possible Values            |
| --------------------------- | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | -------------------------- |
| `publicKey`                 | String         | No             | Your Youverify Public Merchant Key                                                                                                   | nil           | Valid Youverify Public Key |
| `sandboxEnvironment`        | Bool           | No             | Sets whether session should run in sandbox or live mode                                                                              | `true`        | `true`, `false`            |
| `tasks`                     | Array          | No             | Sets tasks that need to be performed for liveness to be confirmed                                                                    | nil           | See tasks section          |
| `user`                      | YVLivenessUser | No             | Sets details of user for which liveness check is being performed                                                                     | nil           | See nested options below   |
| `user.firstName`            | String         | Yes            | First name of user                                                                                                                   | -             | Any string                 |
| `user.lastName`             | String         | No             | Last name of user                                                                                                                    | nil           | Any string                 |
| `user.email`                | String         | No             | Email of user                                                                                                                        | nil           | Any string                 |
| `branding`                  | Branding       | No             | Customizes UI to fit your brand                                                                                                      | nil           | See nested options below   |
| `branding.name`             | String         | No             | The name of the brand                                                                                                                | nil           | Any string                 |
| `branding.color`            | String         | No             | Sets your branding color                                                                                                             | nil           | Valid hex string           |
| `branding.logo`             | String         | No             | Sets your logo                                                                                                                       | nil           | Valid image link           |
| `branding.logoAlt`          | String         | No             | Alternative text for the brand logo                                                                                                  | "Youverify"   | Any string                 |
| `branding.hideLogoOnMobile` | Bool           | No             | Hides logo in mobile webview                                                                                                         | `false`       | `true`, `false`            |
| `branding.showPoweredBy`    | Bool           | No             | Displays "Powered by" footer text                                                                                                    | `false`       | `true`, `false`            |
| `branding.poweredByText`    | String         | No             | Customizes the "Powered by" text                                                                                                     | "Powered by"  | Any string                 |
| `branding.poweredByLogo`    | String         | No             | Customizes the "Powered by" logo                                                                                                     | nil           | Valid image link           |
| `allowAudio`                | Bool           | No             | Sets whether to narrate information to user during tasks                                                                             | `false`       | `true`, `false`            |
| `onClose`                   | Function       | No             | Callback function that gets triggered when modal is closed                                                                           | nil           | Any valid function         |
| `onSuccess`                 | Function       | No             | Callback function that gets triggered when all tasks have been completed and passed. Called with completion data (see Liveness Data) | nil           | Any valid function         |
| `onFailure`                 | Function       | No             | Callback function that gets triggered when at least one task fails. Called with completion data (see Liveness Data)                  | nil           | Any valid function         |
| `sessionId`                 | String         | Yes (required) | ID generated by your backend using your API key. Validated before SDK init and attached to submissions                               | -             | Any valid session ID       |
| `sessionToken`              | String         | Yes (required) | Token generated by your backend for liveness verification                                                                            | -             | Any valid session token    |

## Tasks

A task is a series of instructions for users to follow to confirm liveness. We aim to frequently add to this list.

### Complete The Circle

User passes task by completing imaginary circle with head movement.

#### CompleteTheCircleTask option

| Option       | Type           | Required | Description                                                    | Default Value | Possible Values             |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`    | Number         | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds  |

### Yes Or No

User passes task by answering a list of arbitrary questions set by you with the tilting of the head; right for a `yes` and left for a `no`.

#### YesOrNoTask option

| Option                   | Type           | Required | Description                                                                            | Default Value | Possible Values             |
| ------------------------ | -------------- | -------- | -------------------------------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty`             | TaskDifficulty | No       | Sets difficulty of task                                                                | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`                | Number         | No       | Sets time in milliseconds after which task automatically fails                         | nil           | Any number in milliseconds  |
| `questions`              | Array          | No       | A set of questions to ask user                                                         | nil           | See nested options below    |
| `questions.question`     | String         | Yes      | Question to ask user. Should be a `true` or `false` type question. Eg: "Are you ready" | -             | Any string                  |
| `questions.answer`       | Bool           | Yes      | Answer to the question                                                                 | -             | `true`, `false`             |
| `questions.errorMessage` | String         | No       | Error message to display if user gets question wrong                                   | nil           | Any string                  |

### Motions

User passes task by performing random motions in random sequences, which include `nodding`, `blinking` and `opening of mouth`.

#### MotionsTaskClass option

| Option       | Type           | Required | Description                                                    | Default Value | Possible Values             |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`    | TimeInterval   | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds  |
| `maxNods`    | Int            | No       | Maximum amount of nods a user is asked to perform              | 5             | Any number                  |
| `maxBlinks`  | Int            | No       | Maximum amount of nods a user is asked to perform              | 5             | Any number                  |

### Blink

User passes task by blinking their eyelids based on number of times set.

#### BlinkTaskClass option

| Option       | Type           | Required | Description                                                    | Default Value | Possible Values             |
| ------------ | -------------- | -------- | -------------------------------------------------------------- | ------------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Sets difficulty of task                                        | `.medium`     | `.easy`, `.medium`, `.hard` |
| `timeout`    | TimeInterval   | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds  |
| `maxBlinks`  | Int            | No       | Maximum amount of nods a user is asked to perform              | 5             | Any number                  |

### TaskProperties

User passes in the class and preferred settings for each task.

| Option    | Type         | Required | Description                                                    | Default Value | Possible Values                                                  |
| --------- | ------------ | -------- | -------------------------------------------------------------- | ------------- | ---------------------------------------------------------------- |
| `task`    | YVTask       | Yes      | Id of task                                                     | -             | `YVTask.completeTheCircle(Task_class eg. CompleteTheCircleTask)` |
| `timeout` | TimeInterval | No       | Sets time in milliseconds after which task automatically fails | nil           | Any number in milliseconds                                       |

## Liveness Data

The `onSuccess` and `onFailure` callbacks (if supplied) are passed the following data:

| Option              | Type            | Description                                                                                                                      |
| ------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `data`              | `LivenessData`  | Data passed through callback                                                                                                     |
| `data.faceImage`    | String          | Face Image of user performing liveness check                                                                                     |
| `data.livenessClip` | String          | Video of user performing liveness check                                                                                          |
| `data.passed`       | Bool            | Indicator on whether liveness check passed or failed                                                                             |
| `data.metadata`     | Any             | Metadata passed in during initialization                                                                                         |
| `data.error`        | `LivenessError` | Error object containing error key (eyes\_closed, image\_capture\_failed, API\_ERROR) and message (only present in failure cases) |

## Credits

This SDK is developed and maintained solely by [Youverify](https://youverify.co/)
