Skip to main content
A minimal integration that loads a web-based identity journey inside a native Android app. This example compiles and runs as a standalone Jetpack Compose app.

What This Example Demonstrates

  • Initializing a BridgeHost with typed capability slots
  • Declaring document capture support via handler assignment
  • Displaying a journey by attaching a WebView in Compose
  • Observing bridge errors through a BridgeHostDelegate

Complete Source

package com.example.hellojourney

import android.os.Bundle
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.gbg.gbgbridge.capabilities.CameraDetector
import com.gbg.gbgbridge.core.BridgeHost
import com.gbg.gbgbridge.core.BridgeHostDelegate

// App Entry Point

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MaterialTheme {
        JourneyScreen()
      }
    }
  }
}

// Journey Screen

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JourneyScreen() {
  val context = LocalContext.current

  // 1. Create the bridge host β€” held in remember so it survives recomposition
  val host = remember {
    BridgeHost(hostVersion = "1.0.0").apply {
      // 2. Declare document capture support by setting a handler
      documentCapture.handler = { _ ->
        documentCapture.awaitCompletion()
      }

      // 3. Detect and report camera permission state
      documentCapture.permissionState = CameraDetector.check(context).permissionState
    }
  }

  // 4. Set the journey URL
  val journeyUrl = "https://journey.example.com"

  // 5. Observe bridge errors via the delegate, mirrored into Compose state.
  //    BridgeHost holds its delegate weakly, so keep this strong reference.
  var lastError by remember { mutableStateOf<String?>(null) }
  val delegate = remember {
    object : BridgeHostDelegate {
      override fun onError(host: BridgeHost, error: Throwable) {
        lastError = error.message ?: error.toString()
      }
    }
  }

  DisposableEffect(Unit) {
    host.delegate = delegate
    onDispose { host.detach() }
  }

  Scaffold(
    topBar = { TopAppBar(title = { Text("Identity Verification") }) }
  ) { innerPadding ->
    Column(
      modifier = Modifier
        .fillMaxSize()
        .padding(innerPadding)
    ) {
      // 6. Embed the WebView, attach it to the host, and load the journey
      AndroidView(
        modifier = Modifier.weight(1f),
        factory = { ctx ->
          WebView(ctx).also { webView ->
            host.attach(webView)
            webView.loadUrl(journeyUrl)
          }
        }
      )

      // 7. Show errors if any
      lastError?.let { message ->
        ErrorBanner(message = message) {
          lastError = null
          host.clearError()
        }
      }
    }
  }
}

// Error Banner

@Composable
fun ErrorBanner(message: String, onDismiss: () -> Unit) {
  Row(
    modifier = Modifier
      .fillMaxWidth()
      .background(MaterialTheme.colorScheme.error)
      .padding(horizontal = 16.dp, vertical = 4.dp),
    verticalAlignment = Alignment.CenterVertically
  ) {
    Text(
      text = message,
      style = MaterialTheme.typography.bodySmall,
      color = MaterialTheme.colorScheme.onError,
      maxLines = 2,
      overflow = TextOverflow.Ellipsis,
      modifier = Modifier.weight(1f)
    )
    IconButton(onClick = onDismiss) {
      Icon(
        imageVector = Icons.Default.Close,
        contentDescription = "Dismiss",
        tint = MaterialTheme.colorScheme.onError
      )
    }
  }
}

How It Works

  1. BridgeHost(hostVersion = ...) creates a host with typed capability slots. No separate configuration object is needed. BridgeHost is main-thread-only, so construct it and call its methods on the main thread β€” composition already runs there.
  2. Setting a handler on documentCapture declares the capability as supported. The handler uses awaitCompletion() to suspend until the UI layer calls complete().
  3. CameraDetector.check(context) detects camera hardware and permission state. The permissionState is included in capability query responses.
  4. host.attach(webView) configures the WebView (enables JavaScript and DOM storage), installs a bootstrap-injecting WebViewClient, and registers the window.GBGBridge JavaScript interface. There is no BridgeWebView wrapper on Android β€” you create a plain WebView (here via AndroidView), attach it to the host, and call loadUrl() yourself.
  5. Error observation β€” unlike iOS, where host.lastError is @Published, the Android lastError property is not observable. Instead, implement BridgeHostDelegate.onError and mirror errors into Compose state. The host holds its delegate in a weak reference, so keep a strong reference to it (here via remember) or it will be garbage-collected and silently stop firing.
  6. DisposableEffect.onDispose calls host.detach() when the screen leaves composition. Detach cancels any in-flight capture, removes the JavaScript interface, and clears the message buffers, so the host is ready for a clean re-attach.

What Happens at Runtime

1. App launches -> JourneyScreen composes
2. AndroidView creates the WebView; host.attach() configures it
3. Journey URL loads; bootstrap script injected at page start (window.GBGBridge namespace created)
4. Web journey sends: capability.query request via window.GBGBridge.postMessage(...)
5. Built-in handler responds: { environment: "android", capabilities: { camera.document: { supported: true, permissionState: "granted" } } }
6. Web journey adapts its flow based on available capabilities and permissions

Required Project Configuration

To compile this example, add the SDK dependency to your module’s build.gradle.kts (no repository setup is needed beyond mavenCentral()):
dependencies {
  implementation("com.gbg:gbgbridge-sdk:0.1.0-alpha01")
}
The only manifest entry this minimal example needs is the Internet permission:
<uses-permission android:name="android.permission.INTERNET" />
If your journey URL uses HTTP during development, note that Android blocks cleartext traffic by default. Add a network security configuration scoped to your local development hosts (from the emulator, your host machine is 10.0.2.2, not localhost):
<!-- res/xml/network_security_config.xml β€” debug builds only -->
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="false">10.0.2.2</domain>
    <domain includeSubdomains="false">localhost</domain>
  </domain-config>
</network-security-config>
Reference it from the manifest with android:networkSecurityConfig="@xml/network_security_config" on the <application> element. Never ship an unscoped android:usesCleartextTraffic="true" in production.

Next Steps