Flutter capture
Open the capture from a Flutter app with a platform channel.
Your back-end creates the process in Human Plugin with a Journey flow and a callbackUri that uses your app's URL scheme. A MethodChannel opens the link from the response in ASWebAuthenticationSession on iOS and in Custom Tabs on Android. No external package is needed.
Keep the Human Plugin id from the create response. Your back-end reads the outcome with it.
Prerequisites
- Flutter 3 or later
- Xcode 14 or later for iOS
- Android Studio for Android
- A URL scheme unique to your app, such as
myapp
1. Create the process with your scheme
Your back-end sends the scheme as callbackUri:
{
"flow": "humantoken",
"clientId": "client-a",
"correlationId": "order-1001",
"credential": { "type": "DUI_TYPE_NG_BVN", "value": "<document number>" },
"callbackUri": "myapp://callback"
}
and returns id and link to the app.
2. Create the Dart service
// lib/services/capture_service.dart
import 'package:flutter/services.dart';
class CaptureService {
static const _channel = MethodChannel('capture_plugin');
/// Returns true when the capture ended, false when the person closed it.
Future<bool> open(String link) async {
try {
final result = await _channel.invokeMethod<String>('openSession', {'url': link});
return result != null;
} on PlatformException {
return false;
}
}
}
3. Implement the iOS plugin
// ios/Runner/CapturePlugin.swift
import Flutter
import AuthenticationServices
class CapturePlugin: NSObject, FlutterPlugin, ASWebAuthenticationPresentationContextProviding {
private var session: ASWebAuthenticationSession?
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "capture_plugin", binaryMessenger: registrar.messenger())
let instance = CapturePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard call.method == "openSession",
let args = call.arguments as? [String: Any],
let urlString = args["url"] as? String,
let url = URL(string: urlString) else {
result(FlutterMethodNotImplemented)
return
}
let newSession = ASWebAuthenticationSession(
url: url,
callbackURLScheme: "myapp"
) { callbackURL, error in
self.session = nil
if let callbackURL = callbackURL {
result(callbackURL.absoluteString)
} else {
result(FlutterError(code: "SESSION_ERROR",
message: error?.localizedDescription ?? "Session cancelled",
details: nil))
}
}
newSession.presentationContextProvider = self
newSession.prefersEphemeralWebBrowserSession = true
newSession.start()
session = newSession
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else {
return ASPresentationAnchor()
}
return window
}
}
Register it in AppDelegate.swift:
CapturePlugin.register(with: self.registrar(forPlugin: "CapturePlugin")!)
4. Implement the Android plugin
Add the dependency to android/app/build.gradle:
implementation("androidx.browser:browser:1.5.0")
// android/app/src/main/kotlin/<your.package>/CapturePlugin.kt
package com.yourapp
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class CapturePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var channel: MethodChannel? = null
private var binding: ActivityPluginBinding? = null
private var pendingResult: MethodChannel.Result? = null
override fun onAttachedToEngine(flutterBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterBinding.binaryMessenger, "capture_plugin")
channel?.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "openSession") {
val url = call.argument<String>("url") ?: return result.error("MISSING_URL", "URL is required", null)
pendingResult = result
val activity = binding?.activity ?: return result.error("NO_ACTIVITY", "No activity", null)
CustomTabsIntent.Builder().build().launchUrl(activity, Uri.parse(url))
} else {
result.notImplemented()
}
}
fun handleRedirect(uri: Uri) {
if (uri.scheme == "myapp") {
pendingResult?.success(uri.toString())
pendingResult = null
}
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) { this.binding = binding }
override fun onDetachedFromActivity() { binding = null }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { this.binding = binding }
override fun onDetachedFromActivityForConfigChanges() { binding = null }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = null }
}
Register it and handle the return in MainActivity.kt:
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(CapturePlugin())
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { uri ->
if (uri.scheme == "myapp") {
val plugin = flutterEngine?.plugins?.get(CapturePlugin::class.java) as? CapturePlugin
plugin?.handleRedirect(uri)
}
}
}
5. Register your scheme
- iOS
- Android
In Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
In AndroidManifest.xml, inside your main <activity>:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="callback" />
</intent-filter>
6. Start the capture
final ended = await CaptureService().open(link);
if (ended) {
// The capture ended. Ask your back-end to read the outcome
// with GET /v1/processes/{id}, using the Human Plugin id.
} else {
// The person closed the session.
}
Summary
| File | Purpose |
|---|---|
lib/services/capture_service.dart | Dart MethodChannel service |
ios/Runner/CapturePlugin.swift | iOS: ASWebAuthenticationSession |
ios/Runner/AppDelegate.swift | Plugin registration |
android/.../CapturePlugin.kt | Android: Custom Tabs |
android/.../MainActivity.kt | Plugin registration and return handling |
Info.plist | iOS URL scheme |
AndroidManifest.xml | Android intent filter |