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?
Embed Socure Hosted Flows without building native user forms and document capture UI.
Keep users in-app while leveraging a fully hosted, compliant, and frequently updated web experience.
Single code path across platforms: reuse your web-based identity flow logic and configurations.
Rapid iteration on copy, styling, or flow variants without redeploying the app.
Before you start
Make sure you have the following:
AndroidManifest.xmlA Socure Hosted Flow URL (Sandbox or Production), for example:
private val hostedFlowURL = "https://riskos.sandbox.socure.com/hosted/{flow-id}"Required app capabilities
- 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).
-
- HTTPS:
- Socure RiskOS™ endpoints use HTTPS.
- No network security exceptions or cleartext traffic settings need to be configured.
- 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
FloatingActionButtonto 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)
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 tolandingScreen.
onCreate – Initialization
onCreate – InitializationIn 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
landingScreenis 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 →
requestPermissionLauncherprompts the user for camera access.
- If camera permission is already granted →
- Tapping the close FAB calls
closeWebView(), hides the Hosted Flow, and returns the user tolandingScreen.
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 = trueWhat 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 – camera / JS dialogsWebChromeClient 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,
onPermissionRequestis invoked.- If OS-level permission has already been granted, this implementation grants all requested resources to the web content.
- JavaScript
confirm()dialogs are logged viaonJsConfirm, which can help during debugging or troubleshooting.
WebViewClient – navigation and logging
WebViewClient – navigation and loggingWebViewClient 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).
shouldOverrideUrlLoadingdecides whether a link:- Stays in WebView (normal flow steps and in-flow navigation), or
- Opens in the device browser (legal / external content), via
shouldOpenInBrowser(...).
onPageFinishedcallsinjectLinkDetectionScript(), 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 withdata-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
shouldOverrideUrlLoadingimplementation does not yet read thesedata-*attributes, but this script makes your integration future-proof if you want to extend behavior later.
shouldOpenInBrowser – When to leave the WebView
shouldOpenInBrowser – When to leave the WebViewshouldOpenInBrowser 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):
- No user gesture → stay in WebView
- Covers automatic redirects and form submissions that are part of the Hosted Flow.
- URL contains legal/documentation keywords like
terms,privacy,policy,legal, etc. → open in external browser. - 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.comsubdomains).
- If the destination host differs from the current host, treat it as external,
except when both hosts are Socure domains (e.g., different
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()
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()
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_GRANTEDWhat this does:
- Returns
trueif the camera permission has already been granted, andfalseotherwise. - Used in
onCreateto 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().
- The Hosted Flow is started immediately by calling
- If the user denies permission:
- A persistent
Snackbarexplains 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.
- A persistent
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
| 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. |
Updated about 1 month ago
