Hosted Flows for iOS

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:
  1. HTTPS:
    • Socure RiskOS™ endpoints use HTTPS.
    • No App Transport Security (ATS) exceptions or domain whitelisting are required for Sandbox or Production.
  2. 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:

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:

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:

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

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:

viewDidLoad: Initial setup

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

Configuring the WKWebView

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)

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

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

@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

@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

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+)

@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

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