Hosted Flows for Android

This section shows how to embed a Socure Hosted Flow in an Android app using WebView with a minimal, in-app integration.

What is WebView?

A WebView allows a mobile app to display web content inside the app—without redirecting the user to Chrome or another external browser. On Android, this functionality comes from the android.webkit.WebView component.

Using a WebView lets your app load and interact with Socure’s Hosted Flow as if it were a native screen, while 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

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:

Android Studio installed and Min SDK 21+
Camera permission added to AndroidManifest.xml

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


Required app capabilities

  1. Camera permissions:
    • Add the following to your AndroidManifest.xml:

      <uses-permission android:name="android.permission.CAMERA" />
    • Android will show the permission prompt when the Hosted Flow attempts to access the camera inside the WebView.

    • A clear, in-app explanation should be provided to users describing why camera access is required (document and selfie capture).

  2. HTTPS:
    • Socure RiskOS™ endpoints use HTTPS.
    • No network security exceptions or cleartext traffic settings need to be configured.
  3. User gesture for media:
    • Android requires a user interaction before enabling camera or media capture.
    • Trigger Hosted Flow navigation from a button tap (e.g., “Verify Identity”).
    • Avoid loading the Hosted Flow immediately on app launch without a user gesture.

Layout structure

Your activity_main.xml should define the core UI elements needed to launch and host the Socure Hosted Flow:

  • A landing screen container (e.g., LinearLayout)
  • A “Verify Identity” button
  • A WebView (initially hidden)
  • A FloatingActionButton to close the Hosted Flow

A simplified example layout might look like this:

<LinearLayout
    android:id="@+id/landingScreen"
    android:orientation="vertical"
    ... >

    <!-- Branding, instructions, etc. -->

    <Button
        android:id="@+id/verifyButton"
        android:text="Verify Identity"
        ... />
</LinearLayout>

<WebView
    android:id="@+id/webview"
    android:visibility="gone"
    ... />

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/closeButton"
    android:visibility="gone"
    ... />

The Kotlin code assumes the following view IDs exist in the layout: landingScreen, verifyButton, webview, and closeButton.


Main activity overview (MainActivity.kt)

Class and properties

Your main entry point is a standard AppCompatActivity that wires together the landing screen, WebView, and close controls:

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView
    private lateinit var landingScreen: LinearLayout
    private lateinit var verifyButton: Button
    private lateinit var closeButton: FloatingActionButton

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

What this does:

  • landingScreen – your app’s own UI and explanation before verification starts.
  • verifyButton – the call-to-action that launches the Socure Hosted Flow.
  • webView – the container that renders the Hosted Flow in-app.
  • closeButton – a floating action button that exits the Hosted Flow and returns the user to landingScreen.

onCreate – Initialization

In onCreate, you initialize the views, configure the WebView, and attach click handlers for the verify and close buttons:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // Initialize views
    webView = findViewById(R.id.webview)
    landingScreen = findViewById(R.id.landingScreen)
    verifyButton = findViewById(R.id.verifyButton)
    closeButton = findViewById(R.id.closeButton)

    // Setup WebView
    setupWebView()

    // Setup button listeners
    verifyButton.setOnClickListener {
        if (isCameraPermissionGranted()) {
            startVerification()
        } else {
            requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
    }

    closeButton.setOnClickListener {
        closeWebView()
    }
}

What this does:

  • On launch, only landingScreen is visible; the WebView and close button are hidden.
  • When the user taps Verify Identity:
    • If camera permission is already granted → startVerification() loads the Hosted Flow in the WebView.
    • Otherwise → requestPermissionLauncher prompts the user for camera access.
  • Tapping the close FAB calls closeWebView(), hides the Hosted Flow, and returns the user to landingScreen.

WebView configuration

The setupWebView() function prepares the WebView to run a modern, JavaScript-heavy, single-page Hosted Flow.

private fun setupWebView() {
    Log.d("MainActivity", "📱 Setting up WebView")

    // Clear cache
    webView.clearCache(true)
    webView.clearHistory()
    webView.clearFormData()

    // Enable JavaScript
    val webSettings = webView.settings
    webSettings.javaScriptEnabled = true
    webSettings.domStorageEnabled = true
    webSettings.mediaPlaybackRequiresUserGesture = false
    webSettings.allowFileAccess = true
    webSettings.allowContentAccess = true

What this does:

  • javaScriptEnabled = true – required for the Hosted Flow’s client-side logic.
  • domStorageEnabled = true – enables session/local storage for SPA-style flows.
  • mediaPlaybackRequiresUserGesture = false – reduces friction when starting camera/video streams initiated by the Hosted Flow.
  • allowFileAccess / allowContentAccess – allows the web app to access any required resources.

You can extend this function (not shown here) to add the WebChromeClient and WebViewClient definitions described below.


WebChromeClient – camera / JS dialogs

WebChromeClient handles browser-like features such as camera permission requests and JavaScript dialogs:

webView.webChromeClient = object : WebChromeClient() {
    override fun onPermissionRequest(request: PermissionRequest?) {
        Log.d("MainActivity", "📷 Camera permission requested - GRANTED")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            request?.grant(request.resources)
        } else {
            request?.deny()
        }
    }

    override fun onJsConfirm(
        view: WebView?,
        url: String?,
        message: String?,
        result: android.webkit.JsResult?
    ): Boolean {
        Log.d("MainActivity", "⚠️ JS Confirm: $message")
        return super.onJsConfirm(view, url, message, result)
    }
}

What this does:

  • When the Hosted Flow requests access to the camera or other media devices, onPermissionRequest is invoked.
    • If OS-level permission has already been granted, this implementation grants all requested resources to the web content.
  • JavaScript confirm() dialogs are logged via onJsConfirm, which can help during debugging or troubleshooting.

WebViewClient – navigation and logging

WebViewClient lets you intercept navigations, apply routing rules, and log activity:

webView.webViewClient = object : WebViewClient() {

    override fun shouldOverrideUrlLoading(
        view: WebView?,
        request: android.webkit.WebResourceRequest?
    ): Boolean {
        val url = request?.url?.toString() ?: return false

        Log.d("MainActivity", "🔗 Navigation requested: $url")
        Log.d("MainActivity", "   Has gesture: ${request.hasGesture()}")

        // Check if this is an external link that should open in browser
        return if (shouldOpenInBrowser(url, request.hasGesture())) {
            Log.d("MainActivity", "🌐 Opening in external browser")
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
            true  // Prevent WebView from loading it
        } else {
            Log.d("MainActivity", "📱 Loading in WebView")
            false  // Let WebView handle it
        }
    }

    override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
        super.onPageStarted(view, url, favicon)
        Log.d("MainActivity", "🔄 WebView started loading: $url")
    }

    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        Log.d("MainActivity", "✅ WebView finished loading: $url")

        // Inject JavaScript to detect link clicks
        injectLinkDetectionScript()
    }
}

What this does:

  • Every navigation is logged (including whether it was triggered by a user gesture).
  • shouldOverrideUrlLoading decides whether a link:
    • Stays in WebView (normal flow steps and in-flow navigation), or
    • Opens in the device browser (legal / external content), via shouldOpenInBrowser(...).
  • onPageFinished calls injectLinkDetectionScript(), which adds metadata to certain links and buttons in the DOM for future customization.

External links and domain handling

JavaScript injection for link classification

To better distinguish between “flow actions” and “external/legal” links, you can inject a small JavaScript helper after each page load:

private fun injectLinkDetectionScript() {
    val script = """
        (function() {
            // Mark all links as external by default
            var links = document.querySelectorAll('a[href]');

            for (var i = 0; i < links.length; i++) {
                var link = links[i];
                var href = link.getAttribute('href');

                // Check if this is likely an external documentation link
                if (href && (
                    href.toLowerCase().includes('term') ||
                    href.toLowerCase().includes('privacy') ||
                    href.toLowerCase().includes('policy') ||
                    href.toLowerCase().includes('legal') ||
                    href.toLowerCase().includes('disclosure') ||
                    link.textContent.toLowerCase().includes('term') ||
                    link.textContent.toLowerCase().includes('privacy') ||
                    link.textContent.toLowerCase().includes('policy') ||
                    link.target === '_blank' ||
                    link.getAttribute('rel') === 'external'
                )) {
                    // Mark as external link
                    link.setAttribute('data-external-link', 'true');
                }
            }

            // Mark buttons and form submissions as internal navigation
            var buttons = document.querySelectorAll('button[type="submit"], button:not([type]), input[type="submit"]');
            for (var j = 0; j < buttons.length; j++) {
                buttons[j].setAttribute('data-flow-button', 'true');
            }
        })();
    """.trimIndent()

    webView.evaluateJavascript(script, null)
}

What this does:

  • Iterates over all <a href="..."> elements and tags likely documentation / legal links with data-external-link="true".
  • Tags buttons and submit elements with data-flow-button="true" to indicate they’re part of the verification flow.
  • Gives you a hook to build more advanced routing in the future (e.g., inspecting dataset flags in custom JS bridges).
📘

Note:

The sample shouldOverrideUrlLoading implementation does not yet read these data-* attributes, but this script makes your integration future-proof if you want to extend behavior later.


shouldOpenInBrowser – When to leave the WebView

shouldOpenInBrowser centralizes the logic that decides whether to stay in the Hosted Flow or open a link in the device’s default browser:

private fun shouldOpenInBrowser(url: String, hasGesture: Boolean): Boolean {
    // If no user gesture (automatic redirect/form submission), stay in WebView
    if (!hasGesture) {
        return false
    }

    // Check URL patterns for external documentation
    val urlLower = url.lowercase()
    val externalPatterns = listOf(
        "terms",
        "privacy",
        "policy",
        "legal",
        "disclosure",
        "agreement",
        "eula"
    )

    // If URL contains external documentation keywords, open in browser
    if (externalPatterns.any { urlLower.contains(it) }) {
        return true
    }

    // Parse URL to check domain
    val uri = Uri.parse(url)
    val host = uri.host?.lowercase() ?: ""
    val currentHost = Uri.parse(webView.url ?: "").host?.lowercase() ?: ""

    // If navigating to different domain (cross-origin), likely external
    if (host.isNotEmpty() && currentHost.isNotEmpty() && host != currentHost) {
        // Exception: Allow navigation within socure subdomains
        if (host.contains("socure.com") && currentHost.contains("socure.com")) {
            return false
        }
        return true
    }

    // Default: stay in WebView
    return false
}

What this does (routing rules):

  1. No user gesture → stay in WebView
    • Covers automatic redirects and form submissions that are part of the Hosted Flow.
  2. URL contains legal/documentation keywords like terms, privacy, policy, legal, etc. → open in external browser.
  3. Cross-domain navigation:
    • If the destination host differs from the current host, treat it as external, except when both hosts are Socure domains (e.g., different *.socure.com subdomains).

This pattern keeps the primary verification experience in the app while offloading legal or unrelated content to the system browser.


Starting and closing the Hosted Flow

Entry point – startVerification()

private fun startVerification() {
    Log.d("MainActivity", "🚀 Starting document verification...")

    // Hide landing screen
    landingScreen.visibility = View.GONE

    // Show webview and close button
    webView.visibility = View.VISIBLE
    closeButton.visibility = View.VISIBLE

    // Load URL
    webView.loadUrl(hostedFlowURL)
}

What this does:

  • The landing screen is hidden, and the WebView becomes visible.
  • The close FAB is shown so users can exit if needed.
  • The Socure Hosted Flow URL is loaded and the identity verification journey begins.

Exit point – closeWebView()

private fun closeWebView() {
    Log.d("MainActivity", "❌ Closing webview, returning to landing screen")

    // Show landing screen
    landingScreen.visibility = View.VISIBLE

    // Hide webview and close button
    webView.visibility = View.GONE
    closeButton.visibility = View.GONE

    // Stop loading
    webView.stopLoading()
}

What this does:

  • The Hosted Flow is stopped and the WebView is hidden.
  • The landing screen is shown again.
  • Any in-flight network or rendering work in the WebView is cancelled to keep the app responsive and clean for the next verification attempt.

Camera permissions flow

Runtime permission check

Before launching the flow, you check whether the app already has camera permission:

private fun isCameraPermissionGranted() = ContextCompat.checkSelfPermission(
    this,
    Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED

What this does:

  • Returns true if the camera permission has already been granted, and false otherwise.
  • Used in onCreate to decide whether to immediately start verification or prompt the user for permission.

Request and handling result

Runtime permission handling is implemented with ActivityResultContracts.RequestPermission:

private val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
    if (isGranted) {
        Log.d("MainActivity", "✅ Camera permission granted")
        startVerification()
    } else {
        Log.d("MainActivity", "❌ Camera permission denied")
        Snackbar.make(
            findViewById(R.id.webview),
            "Camera access required for verification",
            Snackbar.LENGTH_INDEFINITE
        )
            .setAction("Allow") {
                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                intent.data = Uri.fromParts("package", packageName, null)
                startActivity(intent)
            }
            .show()
    }
}

What this does:

  • If the user grants camera permission:
    • The Hosted Flow is started immediately by calling startVerification().
  • If the user denies permission:
    • A persistent Snackbar explains that camera access is required for verification.
    • The “Allow” action deep-links users into the app settings screen so they can grant camera access manually.

Combined with WebChromeClient.onPermissionRequest, this gives the Hosted Flow the camera access it needs once the user has opted in at the OS level.


Back button behavior and lifecycle logs

Custom back handling

To provide a predictable back behavior while the Hosted Flow is active, onBackPressed is overridden:

override fun onBackPressed() {
    if (webView.visibility == View.VISIBLE) {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            closeWebView()
        }
    } else {
        super.onBackPressed()
    }
}

What this does:

  • If the WebView is visible:
    • And the Hosted Flow has navigation history → go back one step inside the flow.
    • Otherwise → close the WebView and show the landing screen.
  • If the WebView is hidden, the default system back behavior is used.

Lifecycle logging

Simple lifecycle logs help you understand how users interact with the app during verification:

override fun onPause() {
    super.onPause()
    Log.d("MainActivity", "🔽 [APP LIFECYCLE] App paused (minimized)")
}

override fun onResume() {
    super.onResume()
    Log.d("MainActivity", "✅ [APP LIFECYCLE] App resumed")
}

What this does:

  • Logs when the app is minimized (onPause) and when it returns to the foreground (onResume).
  • Makes it easier to debug flows where users background the app during verification and then come back later.

Key behavior summary

FeatureDescription
Landing screenShows your branding, context, and a “Verify Identity” call-to-action.
Hosted Flow in WebViewThe Socure Hosted Flow runs full-screen inside the app via WebView.
Camera permissionsUses runtime permission + WebView media permissions to enable document/selfie capture.
External linksTerms, privacy, legal, and cross-domain links are opened in the system browser.
Back handlingNavigates back within the flow first; exits to the landing screen when history is exhausted.
Close buttonA floating action button allows users to exit verification at any time.
JS & DOM storageJavaScript and DOM storage are enabled for SPA-style flows and stateful experiences.
Clean resetsWebView cache/history are cleared and loading is stopped when closing the Hosted Flow.