Hosted Flows for Android

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:

private val 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 to your AndroidManifest.xml:

      <uses-permission android:name="android.permission.CAMERA" />
      
  1. HTTPS:

    • Socure RiskOS™ endpoints use HTTPS.
    • No network security exceptions or cleartext traffic settings need to be configured.
  2. 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 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:

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:

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:

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:

WebViewClient – navigation and logging

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:

External links and domain handling

JavaScript injection for link classification

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:

shouldOpenInBrowser – When to leave the WebView

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
}

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

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

Camera permissions flow

Runtime permission check

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

Request and handling result

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

Back button behavior and lifecycle logs

Custom back handling

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

Lifecycle logging

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

Key behavior summary

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