## What is a WebView?

A **WebView** allows a mobile app to display web content inside the app—without redirecting the user to Safari or another browser. On iOS, this functionality comes from `WKWebView`, a modern and secure web rendering engine.

Using a WebView lets your app load and interact with Socure’s Hosted Flow **as if it were a native screen**, while still benefiting from all the power and flexibility of a web-based identity verification experience.

## Why use a WebView for Hosted Flows?

### Seamless Integration

Embed Socure Hosted Flows without building native user forms and document capture UI.

### Keep Users In-App

Keep users in-app while leveraging a fully hosted, compliant, and frequently updated web experience.

### Single Code Path

A single code path across platforms: reuse your web-based identity flow logic and configurations.

### Rapid Iteration

Rapid iteration on copy, styling, or flow variants without redeploying the app.

## Before you start

Make sure you have the following:

**Xcode 14+** and **iOS 14+** _(iOS 15+ recommended for the async media-permission API used below)_

A **Socure Hosted Flow URL** (Sandbox or Production), for example:

`let hostedFlowURL = "[https://riskos.sandbox.socure.com/hosted/{flow-id}](https://riskos.sandbox.socure.com/hosted/{flow-id})"`

## Required app capabilities

1. **Camera permissions**:
   - Add the following key to your app’s **Info.plist**, along with a clear reason for requesting access:

- `NSCameraUsageDescription` — required for capturing document images and selfies.
   - iOS will display this message the moment the Hosted Flow requests camera access in the WebView.
2. **HTTPS**:
   - Socure RiskOS™ endpoints use HTTPS.
   - No App Transport Security (ATS) exceptions or domain whitelisting are required for Sandbox or Production.
3. **User gesture for media**:
   - iOS will not show a camera prompt automatically on page load.
   - A user must tap something first (e.g., “Verify Identity”).
   - Avoid loading the Hosted Flow immediately on app launch without interaction.

> 👍
>
> **Tip**:
>
> Test on a physical device. iOS simulators cannot access the camera.

## Minimal WebView configuration

At a minimum, your WebView setup should:

1. Use `WKWebView` with mobile-friendly settings.
2. Allow inline media playback for camera and video streams.
3. Assign navigation & UI delegates.
4. Load the Socure Hosted Flow URL.
5. (Recommended) Handle external links in Safari.
6. (For iOS 15+) Grant media capture permissions programmatically

A minimal configuration looks like the following example:

```swift
let webConfiguration = WKWebViewConfiguration()
let pref = WKWebpagePreferences()
pref.preferredContentMode = .mobile
webConfiguration.defaultWebpagePreferences = pref
webConfiguration.allowsInlineMediaPlayback = true

if #available(iOS 10.0, *) {
    webConfiguration.mediaTypesRequiringUserActionForPlayback = []
}

webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
webView.navigationDelegate = self
```

Then, when you’re ready to start the Hosted Flow:

```swift
guard let url = URL(string: hostedFlowURL) else { return }
webView.load(URLRequest(url: url))
```

### Key behavior summary

| Feature | Description |
| --- | --- |
| **In-app hosting** | The Socure Hosted Flow runs directly inside a full-screen `WKWebView`, delivering a seamless, native-feeling experience. |
| **External links** | Terms, privacy, and legal links open in `SFSafariViewController` to preserve user context and avoid disrupting the verification flow. |
| **Media capture** | Camera access is granted natively, enabling document and selfie capture directly within the app. |
| **New-window support** | `target="_blank"` links are handled safely, ensuring navigation does not break the Hosted Flow experience. |
| **Close button** | Users can exit verification at any time and return cleanly to the app’s landing screen. |
| **Minimal UI flow** | Verification begins immediately after tapping **Verify Identity**, with no intermediate or unnecessary screens. |
| **Mobile rendering** | Web content is forced into mobile mode to ensure correct layout, scaling, and interaction. |
| **Inline media** | Inline camera and video streams are enabled to support capture steps within the Hosted Flow. |

### Recommended UX enhancements

Not strictly required, but recommended:

- A full-screen WebView anchored to the safe area.
- A landing screen with branding and a clear **“Verify Identity”** call-to-action.
- A close button to exit the Hosted Flow and return to your app.
- Logic to open legal/terms/privacy links in Safari instead of inside the Hosted Flow WebView.

## Implementation walkthrough (`ViewController.swift`)

This section provides a production-ready walkthrough of a simple iOS integration that embeds Socure Hosted Flows inside a WKWebView.

### Class definition and properties

```swift
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
    var webView: WKWebView!
    var landingView: UIView!
    var verifyButton: UIButton!
    var closeButton: UIButton!

let hostedFlowURL = "https://riskos.sandbox.socure.com/hosted/<flow-id>"
}
```

**What this does**:

- `WKUIDelegate`: Handles UI-related behaviors such as new windows (`target="_blank"`).
- `WKNavigationDelegate`: Intercepts navigation events so you can:

- Keep users inside the Hosted Flow.
  - Redirect legal or external links to Safari.

### `viewDidLoad`: Initial setup

```swift
override func viewDidLoad() {
    super.viewDidLoad()
    self.view.backgroundColor = .systemBackground
    setupWebView()
    setupLandingScreen()
}
```

### Configuring the `WKWebView`

```swift
func setupWebView() {
    let webConfiguration = WKWebViewConfiguration()
    let pref = WKWebpagePreferences()
    pref.preferredContentMode = .mobile
    webConfiguration.defaultWebpagePreferences = pref
    webConfiguration.allowsInlineMediaPlayback = true

if #available(iOS 10.0, *) {
        webConfiguration.mediaTypesRequiringUserActionForPlayback = []
    }

webView = WKWebView(frame: .zero, configuration: webConfiguration)
    webView.uiDelegate = self
    webView.navigationDelegate = self
    webView.isHidden = true
    webView.backgroundColor = .white
    webView.translatesAutoresizingMaskIntoConstraints = false

self.view.addSubview(webView)

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

addCloseButton()
}
```

### Close button (exit Hosted Flow)

```swift
func addCloseButton() {
    closeButton = UIButton(type: .system)
    closeButton.setTitle("✕", for: .normal)
    closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 24, weight: .bold)
    closeButton.backgroundColor = UIColor.black.withAlphaComponent(0.7)
    closeButton.setTitleColor(.white, for: .normal)
    closeButton.translatesAutoresizingMaskIntoConstraints = false
    closeButton.addTarget(self, action: #selector(closeWebView), for: .touchUpInside)
    closeButton.isHidden = true

self.view.addSubview(closeButton)

NSLayoutConstraint.activate([\
        closeButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10),\
        closeButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),\
        closeButton.widthAnchor.constraint(equalToConstant: 40),\
        closeButton.heightAnchor.constraint(equalToConstant: 40)\
    ])
}
```

### Landing screen and “Verify Identity” button

```swift
func setupLandingScreen() {
    landingView = UIView(frame: self.view.bounds)
    landingView.backgroundColor = .systemBackground
    landingView.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(landingView)

// Layout constraints ...

let titleLabel = UILabel()
    titleLabel.text = "Your App Name"

let subtitleLabel = UILabel()
    subtitleLabel.text = "Your App Description."

verifyButton = UIButton(type: .system)
    verifyButton.setTitle("Verify Identity", for: .normal)
    verifyButton.addTarget(self, action: #selector(startVerification), for: .touchUpInside)

// Add and constrain subviews...
}
```

### Launching the Hosted Flow

```swift
@objc func startVerification() {
    landingView.isHidden = true
    webView.isHidden = false
    closeButton.isHidden = false
    self.view.bringSubviewToFront(closeButton)

guard let url = URL(string: hostedFlowURL) else {
        return
    }

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

### Closing the Hosted Flow

```swift
@objc func closeWebView() {
    landingView.isHidden = false
    webView.isHidden = true
    closeButton.isHidden = true
    webView.stopLoading()
    self.view.bringSubviewToFront(landingView)
}
```

### Navigation control and external links

#### Intercepting navigation

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

// Check if this should open in Safari
    if shouldOpenInSafari(url: url, navigationType: navigationAction.navigationType) {
        let safariVC = SFSafariViewController(url: url)
        safariVC.modalPresentationStyle = .pageSheet
        present(safariVC, animated: true)
        decisionHandler(.cancel)
    } else {
        decisionHandler(.allow)
    }
}
```

### Camera permissions (iOS 15+)

```swift
@available(iOS 15.0, *)
func webView(_ webView: WKWebView,
             decideMediaCapturePermissionsFor origin: WKSecurityOrigin,
             initiatedBy frame: WKFrameInfo,
             type: WKMediaCaptureType) async -> WKPermissionDecision {

return .grant
}
```

## Summary

To embed Socure Hosted Flows in an iOS app using WKWebView, you need:

1. A Socure **Hosted Flow URL**.
2. A properly configured `WKWebView` with:
   - Mobile content mode
   - Inline media playback
   - Navigation and UI delegates
3. Camera permissions declared in `Info.plist`
4. (Recommended) UX controls for:
   - Explicit entry into verification.
   - Exiting the flow.
   - Handling **external legal/terms/privacy links** in Safari.
   - Grant **media capture permissions** (iOS 15+).

Sample `ViewController.swift` already implements a clean, production-style pattern you can generalize in the documentation.

## Full example implementation

```swift
import UIKit
import WebKit
import SafariServices

class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {

// MARK: - UI Elements

private var webView: WKWebView!
    private var landingView: UIView!
    private var verifyButton: UIButton!
    private var closeButton: UIButton!

// MARK: - Hosted Flow URL

/// TODO: Replace with your actual Socure Hosted Flow URL
    private let hostedFlowURL = "https://riskos.sandbox.socure.com/hosted/<flow-id>"

// MARK: - Lifecycle

override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

setupWebView()
        setupLandingScreen()
    }

// MARK: - WebView Setup

private func setupWebView() {
        let config = WKWebViewConfiguration()
        let prefs = WKWebpagePreferences()
        prefs.preferredContentMode = .mobile
        config.defaultWebpagePreferences = prefs
        config.allowsInlineMediaPlayback = true

if #available(iOS 10.0, *) {
                config.mediaTypesRequiringUserActionForPlayback = []
                }

webView = WKWebView(frame: .zero, configuration: config)
        webView.uiDelegate = self
        webView.navigationDelegate = self
        webView.isHidden = true
        webView.backgroundColor = .white
        webView.scrollView.contentInsetAdjustmentBehavior = .never
        webView.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(webView)

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

setupCloseButton()
    }

// MARK: - Close Button

private func setupCloseButton() {
        closeButton = UIButton(type: .system)
        closeButton.setTitle("✕", for: .normal)
        closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 24, weight: .bold)
        closeButton.setTitleColor(.white, for: .normal)
        closeButton.backgroundColor = UIColor.black.withAlphaComponent(0.7)
        closeButton.layer.cornerRadius = 20
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        closeButton.isHidden = true
        closeButton.addTarget(self, action: #selector(closeWebView), for: .touchUpInside)

view.addSubview(closeButton)

NSLayoutConstraint.activate([\
            closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),\
            closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),\
            closeButton.widthAnchor.constraint(equalToConstant: 40),\
            closeButton.heightAnchor.constraint(equalToConstant: 40)\
        ])
    }

// MARK: - Landing Screen

private func setupLandingScreen() {
        landingView = UIView()
        landingView.backgroundColor = .systemBackground
        landingView.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(landingView)

NSLayoutConstraint.activate([\
            landingView.topAnchor.constraint(equalTo: view.topAnchor),\
            landingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),\
            landingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),\
            landingView.trailingAnchor.constraint(equalTo: view.trailingAnchor)\
        ])

// Title label
        let titleLabel = UILabel()
        titleLabel.text = "Your App Name"
        titleLabel.font = UIFont.systemFont(ofSize: 42, weight: .bold)
        titleLabel.textAlignment = .center
        titleLabel.translatesAutoresizingMaskIntoConstraints = false

// Subtitle label
        let subtitleLabel = UILabel()
        subtitleLabel.text = "Your App Description."
        subtitleLabel.font = UIFont.systemFont(ofSize: 16, weight: .regular)
        subtitleLabel.textColor = .secondaryLabel
        subtitleLabel.textAlignment = .center
        subtitleLabel.numberOfLines = 0
        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false

// Verify Identity button
        verifyButton = UIButton(type: .system)
        verifyButton.setTitle("Verify Identity", for: .normal)
        verifyButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
        verifyButton.backgroundColor = .systemBlue
        verifyButton.setTitleColor(.white, for: .normal)
        verifyButton.layer.cornerRadius = 25
        verifyButton.translatesAutoresizingMaskIntoConstraints = false
        verifyButton.addTarget(self, action: #selector(startVerification), for: .touchUpInside)

landingView.addSubview(titleLabel)
        landingView.addSubview(subtitleLabel)
        landingView.addSubview(verifyButton)

NSLayoutConstraint.activate([\
            titleLabel.centerXAnchor.constraint(equalTo: landingView.centerXAnchor),\
            titleLabel.centerYAnchor.constraint(equalTo: landingView.centerYAnchor, constant: -120),\
\
            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),\
            subtitleLabel.centerXAnchor.constraint(equalTo: landingView.centerXAnchor),\
            subtitleLabel.leadingAnchor.constraint(equalTo: landingView.leadingAnchor, constant: 40),\
            subtitleLabel.trailingAnchor.constraint(equalTo: landingView.trailingAnchor, constant: -40),\
\
            verifyButton.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 50),\
            verifyButton.centerXAnchor.constraint(equalTo: landingView.centerXAnchor),\
            verifyButton.widthAnchor.constraint(equalToConstant: 260),\
            verifyButton.heightAnchor.constraint(equalToConstant: 50)\
        ])
    }

// MARK: - Start / Close Hosted Flow

@objc private func startVerification() {
        guard let url = URL(string: hostedFlowURL) else {
            print("Invalid Hosted Flow URL")
            return
        }

landingView.isHidden = true
        webView.isHidden = false
        closeButton.isHidden = false
        view.bringSubviewToFront(closeButton)

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

@objc private func closeWebView() {
        webView.stopLoading()
        webView.isHidden = true
        closeButton.isHidden = true
        landingView.isHidden = false
        view.bringSubviewToFront(landingView)
    }

// MARK: - WKNavigationDelegate (External Links)

func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }

// Only handle user-tapped links
        if navigationAction.navigationType == .linkActivated,
           shouldOpenInSafari(url: url) {

let safariVC = SFSafariViewController(url: url)
            safariVC.modalPresentationStyle = .pageSheet
            present(safariVC, animated: true)
            decisionHandler(.cancel)
            return
        }

decisionHandler(.allow)
    }

/// Decide whether a URL should open externally in Safari
    private func shouldOpenInSafari(url: URL) -> Bool {
        let s = url.absoluteString.lowercased()
        return s.contains("privacy")
            || s.contains("policy")
            || s.contains("legal")
            || s.contains("term")
    }

// MARK: - WKUIDelegate (target="_blank" Support)

func webView(_ webView: WKWebView,
                 createWebViewWith configuration: WKWebViewConfiguration,
                 for navigationAction: WKNavigationAction,
                 windowFeatures: WKWindowFeatures) -> WKWebView? {

// Handle links that try to open in a new window (target="_blank")
        if navigationAction.targetFrame == nil,
           let url = navigationAction.request.url {

if shouldOpenInSafari(url: url) {
                let safariVC = SFSafariViewController(url: url)
                present(safariVC, animated: true)
            } else {
                webView.load(navigationAction.request)
            }
        }
        return nil
    }

// MARK: - Camera / Media Permissions (iOS 15+)

@available(iOS 15.0, *)
    func webView(_ webView: WKWebView,
                 decideMediaCapturePermissionsFor origin: WKSecurityOrigin,
                 initiatedBy frame: WKFrameInfo,
                 type: WKMediaCaptureType) async -> WKPermissionDecision {

// You can add origin checks here if desired
        return .grant
    }
}
```
