Skip to content

Flutter Integration Guide

Overview

BiometridFull is a comprehensive identity verification SDK that provides a complete. This guide explains how to integrate BiometridFull into a Flutter application using a custom plugin with platform channels.

Since BiometridFull is distributed as a native Android AAR (via Maven) and an iOS XCFramework (via CocoaPods), integration in Flutter requires a Flutter Plugin that bridges the native SDKs to Dart through MethodChannel and EventChannel.

Prerequisites

Platform Requirement
Flutter 3.10.0+
Dart 3.0+
Android API Level 26+ (Android 8.0), Kotlin 2.2.0+
iOS 16.0+, Xcode 15+, Swift 5.9+

Step 1: Create the Flutter Plugin

Generate the plugin scaffold:

flutter create --org com.biometrid --template=plugin \
  --platforms=android,ios -a kotlin -i swift \
  biometrid_full_plugin

This creates the following structure:

biometrid_full_plugin/
├── lib/
│   ├── biometrid_full_plugin.dart
│   ├── biometrid_full_plugin_method_channel.dart
│   └── biometrid_full_plugin_platform_interface.dart
├── android/
│   ├── build.gradle
│   └── src/main/kotlin/.../BiometridFullPlugin.kt
├── ios/
│   ├── biometrid_full_plugin.podspec
│   └── Classes/BiometridFullPlugin.swift
├── example/
└── pubspec.yaml

Step 2: Configure Android Native Side

2.1 Add Repositories and Dependencies

Starting with 3.3.3, BiometridFull is distributed as a modular Android library. The main AAR contains the SDK code with native step dependencies declared as compileOnly. To use a step, declare its bridge alongside the main dependency — the bridge is a POM-only artifact that pulls in the right native libraries for that step.

You only need to add the repositories required by the bridges you actually use. If you never use NFC, you don't need the Regula repo. If you never use Liveness, you don't need the face01/iProov repos.

Edit android/build.gradle:

group 'com.biometrid.biometrid_full_plugin'
version '1.0'

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.1.0'
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0'
    }
}

rootProject.allprojects {
    repositories {
        google()
        mavenCentral()

        // BiometridFull main artifact + all bridges
        maven { url "https://dl.cloudsmith.io/public/biometrid/mobile/maven/" }

        // Liveness bridge repositories
        maven { url "https://dl.cloudsmith.io/public/biometrid/face01-liveness/maven/" }
        maven { url "https://raw.githubusercontent.com/iProov/android/master/maven/" }

        // NFC bridge repository
        maven { url "https://maven.regulaforensics.com/RegulaDocumentReader/" }

        // AutoCapture / VideoConference / VideoLiveness bridges need no extra repos — mavenCentral is enough
    }
}

android {
    namespace 'com.biometrid.biometrid_full_plugin'
    compileSdk 35

    defaultConfig {
        minSdk 26
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }

    kotlinOptions {
        jvmTarget = '11'
    }
}

dependencies {
    // BiometridFull main SDK
    implementation "com.biometrid:biometridfull:3.3.3"

    // Bridges — declare one per step your flow uses. Bridges are POM-only and
    // pull in the native libraries for each step. Drop the ones you don't need.
    implementation "com.biometrid:biometridfull-liveness:3.3.3"
    implementation "com.biometrid:biometridfull-nfc:3.3.3"
    implementation "com.biometrid:biometridfull-autocapture:3.3.3"
    implementation "com.biometrid:biometridfull-videoconference:3.3.3"
    implementation "com.biometrid:biometridfull-videoliveness:3.3.3"
}

2.2 Add Required Permissions

Create or edit android/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.biometrid.biometrid_full_plugin">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

    <uses-feature android:name="android.hardware.nfc" android:required="false" />
    <uses-feature android:name="android.hardware.camera" android:required="false" />
    <uses-feature android:name="android.hardware.camera.front" android:required="false" />
    <uses-feature android:name="android.hardware.microphone" android:required="false" />
</manifest>

2.3 Implement the Android Plugin

Edit android/src/main/kotlin/com/biometrid/biometrid_full_plugin/BiometridFullPlugin.kt:

package com.biometrid.biometrid_full_plugin

import android.app.Activity
import com.biometrid.biometridfull.BiometridFull
import com.biometrid.biometridfull.callbacks.BiometridFullCallback
import com.biometrid.biometridfull.helpers.ScreenType
import com.biometrid.biometridstandard.enums.Language
import com.biometrid.biometridstandard.model.BiometridErrorInfo
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.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class BiometridFullPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
    EventChannel.StreamHandler {

    private lateinit var methodChannel: MethodChannel
    private lateinit var eventChannel: EventChannel
    private var eventSink: EventChannel.EventSink? = null
    private var activity: Activity? = null
    private var biometridFull: BiometridFull? = null

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

    // ─── FlutterPlugin ─────────────────────────────────────────────

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel = MethodChannel(
            binding.binaryMessenger,
            "com.biometrid/biometrid_full"
        )
        methodChannel.setMethodCallHandler(this)

        eventChannel = EventChannel(
            binding.binaryMessenger,
            "com.biometrid/biometrid_full_events"
        )
        eventChannel.setStreamHandler(this)
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel.setMethodCallHandler(null)
        eventChannel.setStreamHandler(null)
    }

    // ─── ActivityAware ──────────────────────────────────────────────

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

    override fun onDetachedFromActivity() {
        activity = null
    }

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

    override fun onDetachedFromActivityForConfigChanges() {
        activity = null
    }

    // ─── EventChannel.StreamHandler ─────────────────────────────────

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        eventSink = events
    }

    override fun onCancel(arguments: Any?) {
        eventSink = null
    }

    // ─── MethodCallHandler ──────────────────────────────────────────

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "initialize" -> handleInitialize(call, result)
            "start" -> handleStart(call, result)
            "stop" -> handleStop(result)
            else -> result.notImplemented()
        }
    }

    // ─── Method Handlers ────────────────────────────────────────────

    private fun handleInitialize(call: MethodCall, result: Result) {
        val url = call.argument<String>("url")
        val app = call.argument<String>("app")
        val appUrl = call.argument<String>("appUrl")
        val credential = call.argument<String>("credential")
        val languageCode = call.argument<String>("language") ?: "en-GB"
        val customHeaders = call.argument<Map<String, Any>>("customHeaders")

        if (url == null || app == null || appUrl == null || credential == null) {
            result.error(
                "INVALID_ARGS",
                "Missing required arguments: url, app, appUrl, credential",
                null
            )
            return
        }

        val language = Language.entries.find { it.value == languageCode }
            ?: Language.English

        val callback = createCallback(url, app, appUrl, credential, language, customHeaders)
        biometridFull = BiometridFull(callback)

        scope.launch {
            try {
                biometridFull?.initialize()
                result.success(true)
            } catch (e: Exception) {
                result.error("INIT_ERROR", e.message, null)
            }
        }
    }

    private fun handleStart(call: MethodCall, result: Result) {
        val processId = call.argument<String>("processId")
        val currentActivity = activity

        if (currentActivity == null) {
            result.error("NO_ACTIVITY", "Plugin is not attached to an Activity", null)
            return
        }

        if (biometridFull == null) {
            result.error(
                "NOT_INITIALIZED",
                "BiometridFull is not initialized. Call initialize() first.",
                null
            )
            return
        }

        scope.launch {
            try {
                biometridFull?.start(
                    processId = processId,
                    screenType = ScreenType(currentActivity)
                )
                result.success(true)
            } catch (e: Exception) {
                result.error("START_ERROR", e.message, null)
            }
        }
    }

    private fun handleStop(result: Result) {
        biometridFull?.stop()
        result.success(true)
    }

    // ─── Callback ───────────────────────────────────────────────────

    private fun createCallback(
        url: String,
        app: String,
        appUrl: String,
        credential: String,
        language: Language,
        customHeaders: Map<String, Any>?
    ): BiometridFullCallback {
        return object : BiometridFullCallback {
            override val url: String = url
            override val app: String = app
            override val appUrl: String = appUrl
            override val credential: String = credential
            override val language: Language = language
            override val customHeaders: Map<Any?, *>? = customHeaders

            override fun initialized(status: Boolean, error: BiometridErrorInfo?) {
                sendEvent(mapOf(
                    "type" to "initialized",
                    "status" to status,
                    "error" to error?.toMap()
                ))
            }

            override fun processCreated(processId: String) {
                sendEvent(mapOf(
                    "type" to "processCreated",
                    "processId" to processId
                ))
            }

            override fun processUpdated(
                processId: String,
                stepId: String,
                action: String
            ) {
                sendEvent(mapOf(
                    "type" to "processUpdated",
                    "processId" to processId,
                    "stepId" to stepId,
                    "action" to action
                ))
            }

            override fun processFinished(processId: String) {
                sendEvent(mapOf(
                    "type" to "processFinished",
                    "processId" to processId
                ))
            }

            override fun error(status: Boolean, error: BiometridErrorInfo) {
                sendEvent(mapOf(
                    "type" to "error",
                    "status" to status,
                    "error" to error.toMap()
                ))
            }
        }
    }

    private fun sendEvent(data: Map<String, Any?>) {
        activity?.runOnUiThread {
            eventSink?.success(data)
        }
    }

    private fun BiometridErrorInfo.toMap(): Map<String, Any?> {
        return mapOf(
            "code" to code,
            "message" to message
        )
    }
}

2.4 Register Required Activities (Consumer App)

The consumer Flutter app must add these activities to its android/app/src/main/AndroidManifest.xml inside the <application> tag:

<activity
    android:name="com.biometrid.biometridfull.ui.BiometridFullComposeActivity"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
    android:exported="false" />
<activity
    android:name="com.biometrid.autocapture.ui.AutoCaptureActivity"
    android:exported="false" />
<activity
    android:name="com.biometrid.biometridfull.webview.AndroidWebViewActivity"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
    android:exported="false" />
<activity
    android:name="com.biometrid.biometridstandardnfc.activity.BiometridNFCActivity"
    android:exported="false" />

Step 3: Configure iOS Native Side

3.1 Configure the Podspec

Edit ios/biometrid_full_plugin.podspec:

Pod::Spec.new do |s|
  s.name             = 'biometrid_full_plugin'
  s.version          = '3.3.3'
  s.summary          = 'Flutter plugin for BiometridFull identity verification SDK'
  s.description      = 'Integrates BiometridFull KMM SDK into Flutter for iOS'
  s.homepage         = 'https://biometrid.com'
  s.license          = { :file => '../LICENSE' }
  s.author           = { 'Biometrid' => 'dev@biometrid.com' }
  s.source           = { :path => '.' }
  s.source_files     = 'Classes/**/*'

  s.dependency 'Flutter'
  s.dependency 'BiometridFull', '~> 3.3.3'

  s.platform            = :ios, '16.0'
  s.ios.deployment_target = '16.0'
  s.swift_version       = '5.9'

  s.pod_target_xcconfig = {
    'DEFINES_MODULE' => 'YES',
    'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES',
  }
end

Note: On iOS, BiometridFull is published with subspecs (Liveness, NFC, AutoCapture, VideoConference, VideoLiveness) that mirror the Android bridges. They exist as semantic markers — today they all transitively pull in the same Core, which carries the XCFramework and every native step pod. Declaring BiometridFull alone is enough.

3.2 Configure Consumer App Podfile

The consumer Flutter app's ios/Podfile needs the Biometrid spec sources:

platform :ios, '16.0'

# Required CocoaPods spec sources for BiometridFull dependencies
source 'https://cdn.cocoapods.org'
source 'https://dl.cloudsmith.io/public/biometrid/mobile/cocoapods/index.git'
source 'https://dl.cloudsmith.io/public/biometrid/face01-liveness/cocoapods/index.git'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug'   => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(
    File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__
  )
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist."
  end
  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}."
end

require File.expand_path(
  File.join('packages', 'flutter_tools', 'bin', 'podhelper'),
  flutter_root
)

target 'Runner' do
  use_frameworks!
  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
    end
  end
end

3.3 Implement the iOS Plugin

Edit ios/Classes/BiometridFullPlugin.swift:

import Flutter
import UIKit
import BiometridFull

public class BiometridFullPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {

    private var eventSink: FlutterEventSink?
    private var biometridFull: BiometridFull?
    private var callbackHandler: BiometridCallbackHandler?

    // ─── Plugin Registration ────────────────────────────────────────

    public static func register(with registrar: FlutterPluginRegistrar) {
        let methodChannel = FlutterMethodChannel(
            name: "com.biometrid/biometrid_full",
            binaryMessenger: registrar.messenger()
        )
        let eventChannel = FlutterEventChannel(
            name: "com.biometrid/biometrid_full_events",
            binaryMessenger: registrar.messenger()
        )

        let instance = BiometridFullPlugin()
        registrar.addMethodCallDelegate(instance, channel: methodChannel)
        eventChannel.setStreamHandler(instance)
    }

    // ─── FlutterStreamHandler ───────────────────────────────────────

    public func onListen(
        withArguments arguments: Any?,
        eventSink events: @escaping FlutterEventSink
    ) -> FlutterError? {
        self.eventSink = events
        return nil
    }

    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
        self.eventSink = nil
        return nil
    }

    // ─── Method Handling ────────────────────────────────────────────

    public func handle(
        _ call: FlutterMethodCall,
        result: @escaping FlutterResult
    ) {
        switch call.method {
        case "initialize":
            handleInitialize(call, result: result)
        case "start":
            handleStart(call, result: result)
        case "stop":
            handleStop(result: result)
        default:
            result(FlutterMethodNotImplemented)
        }
    }

    // ─── Initialize ─────────────────────────────────────────────────

    private func handleInitialize(
        _ call: FlutterMethodCall,
        result: @escaping FlutterResult
    ) {
        guard let args = call.arguments as? [String: Any],
              let url = args["url"] as? String,
              let app = args["app"] as? String,
              let appUrl = args["appUrl"] as? String,
              let credential = args["credential"] as? String else {
            result(FlutterError(
                code: "INVALID_ARGS",
                message: "Missing required arguments: url, app, appUrl, credential",
                details: nil
            ))
            return
        }

        let languageCode = args["language"] as? String ?? "en-GB"
        let customHeaders = args["customHeaders"] as? [AnyHashable: Any]

        let language = mapLanguage(languageCode)

        let callback = BiometridCallbackHandler(
            url: url,
            app: app,
            appUrl: appUrl,
            credential: credential,
            language: language,
            customHeaders: customHeaders,
            eventSink: { [weak self] event in
                DispatchQueue.main.async {
                    self?.eventSink?(event)
                }
            }
        )
        self.callbackHandler = callback
        self.biometridFull = BiometridFull(callback: callback)

        Task {
            do {
                try await self.biometridFull?.initialize()
                DispatchQueue.main.async {
                    result(true)
                }
            } catch {
                DispatchQueue.main.async {
                    result(FlutterError(
                        code: "INIT_ERROR",
                        message: error.localizedDescription,
                        details: nil
                    ))
                }
            }
        }
    }

    // ─── Start ──────────────────────────────────────────────────────

    private func handleStart(
        _ call: FlutterMethodCall,
        result: @escaping FlutterResult
    ) {
        let args = call.arguments as? [String: Any]
        let processId = args?["processId"] as? String

        guard biometridFull != nil else {
            result(FlutterError(
                code: "NOT_INITIALIZED",
                message: "BiometridFull is not initialized. Call initialize() first.",
                details: nil
            ))
            return
        }

        guard let viewController = findTopMostViewController() else {
            result(FlutterError(
                code: "NO_VIEW_CONTROLLER",
                message: "Could not find top view controller",
                details: nil
            ))
            return
        }

        Task {
            do {
                try await self.biometridFull?.start(
                    processId: processId,
                    screenType: ScreenType(controller: viewController)
                )
                DispatchQueue.main.async {
                    result(true)
                }
            } catch {
                DispatchQueue.main.async {
                    result(FlutterError(
                        code: "START_ERROR",
                        message: error.localizedDescription,
                        details: nil
                    ))
                }
            }
        }
    }

    // ─── Stop ───────────────────────────────────────────────────────

    private func handleStop(result: @escaping FlutterResult) {
        biometridFull?.stop()
        result(true)
    }

    // ─── Helpers ────────────────────────────────────────────────────

    private func findTopMostViewController() -> UIViewController? {
        guard let windowScene = UIApplication.shared.connectedScenes
                .compactMap({ $0 as? UIWindowScene }).first,
              var topController = windowScene.windows
                .first(where: { $0.isKeyWindow })?.rootViewController
        else {
            return nil
        }
        while let presented = topController.presentedViewController {
            topController = presented
        }
        return topController
    }

    private func mapLanguage(_ code: String) -> SharedLanguage {
        switch code {
        case "pt-PT": return .portuguese
        case "fr-FR": return .french
        case "es-ES": return .spanish
        case "it-IT": return .italian
        default:      return .english
        }
    }
}

// ─── Callback Handler ───────────────────────────────────────────────

class BiometridCallbackHandler: BiometridFullCallback {

    var url: String
    var app: String
    var appUrl: String
    var credential: String
    var language: SharedLanguage
    var customHeaders: [AnyHashable: Any]?

    private let sendEvent: ([String: Any?]) -> Void

    init(
        url: String,
        app: String,
        appUrl: String,
        credential: String,
        language: SharedLanguage,
        customHeaders: [AnyHashable: Any]?,
        eventSink: @escaping ([String: Any?]) -> Void
    ) {
        self.url = url
        self.app = app
        self.appUrl = appUrl
        self.credential = credential
        self.language = language
        self.customHeaders = customHeaders
        self.sendEvent = eventSink
    }

    func initialized(status: Bool, error: SharedBiometridErrorInfo?) {
        sendEvent([
            "type": "initialized",
            "status": status,
            "error": error.map { [
                "code": $0.code as Any?,
                "message": $0.message as Any?
            ] }
        ])
    }

    func processCreated(processId: String) {
        sendEvent([
            "type": "processCreated",
            "processId": processId
        ])
    }

    func processUpdated(processId: String, stepId: String, action: String) {
        sendEvent([
            "type": "processUpdated",
            "processId": processId,
            "stepId": stepId,
            "action": action
        ])
    }

    func processFinished(processId: String) {
        sendEvent([
            "type": "processFinished",
            "processId": processId
        ])
    }

    func error(status: Bool, error: SharedBiometridErrorInfo) {
        sendEvent([
            "type": "error",
            "status": status,
            "error": [
                "code": error.code as Any?,
                "message": error.message as Any?
            ]
        ])
    }
}

3.4 Add Required Info.plist Keys (Consumer App)

The consumer Flutter app must add these to ios/Runner/Info.plist:

<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>

<key>NSCameraUsageDescription</key>
<string>Camera access is required for identity verification</string>

<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for identity verification</string>

<key>NFCReaderUsageDescription</key>
<string>NFC is required to read identity documents</string>

<key>com.apple.developer.nfc.readersession.formats</key>
<array>
    <string>TAG</string>
</array>

<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
</array>

Step 4: Dart API Layer

4.1 Platform Interface

Edit lib/biometrid_full_plugin_platform_interface.dart:

import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'biometrid_full_plugin_method_channel.dart';

abstract class BiometridFullPluginPlatform extends PlatformInterface {
  BiometridFullPluginPlatform() : super(token: _token);

  static final Object _token = Object();

  static BiometridFullPluginPlatform _instance =
      MethodChannelBiometridFullPlugin();

  static BiometridFullPluginPlatform get instance => _instance;

  static set instance(BiometridFullPluginPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<bool> initialize({
    required String url,
    required String app,
    required String appUrl,
    required String credential,
    String language = 'en-GB',
    Map<String, dynamic>? customHeaders,
  });

  Future<bool> start({String? processId});

  Future<bool> stop();

  Stream<BiometridEvent> get onEvent;
}

4.2 Method Channel Implementation

Edit lib/biometrid_full_plugin_method_channel.dart:

import 'package:flutter/services.dart';
import 'biometrid_full_plugin_platform_interface.dart';
import 'biometrid_full_plugin.dart';

class MethodChannelBiometridFullPlugin
    extends BiometridFullPluginPlatform {

  static const MethodChannel _methodChannel =
      MethodChannel('com.biometrid/biometrid_full');

  static const EventChannel _eventChannel =
      EventChannel('com.biometrid/biometrid_full_events');

  Stream<BiometridEvent>? _eventStream;

  @override
  Future<bool> initialize({
    required String url,
    required String app,
    required String appUrl,
    required String credential,
    String language = 'en-GB',
    Map<String, dynamic>? customHeaders,
  }) async {
    final result = await _methodChannel.invokeMethod<bool>('initialize', {
      'url': url,
      'app': app,
      'appUrl': appUrl,
      'credential': credential,
      'language': language,
      if (customHeaders != null) 'customHeaders': customHeaders,
    });
    return result ?? false;
  }

  @override
  Future<bool> start({String? processId}) async {
    final result = await _methodChannel.invokeMethod<bool>('start', {
      if (processId != null) 'processId': processId,
    });
    return result ?? false;
  }

  @override
  Future<bool> stop() async {
    final result = await _methodChannel.invokeMethod<bool>('stop');
    return result ?? false;
  }

  @override
  Stream<BiometridEvent> get onEvent {
    _eventStream ??= _eventChannel
        .receiveBroadcastStream()
        .map((event) => BiometridEvent.fromMap(
            Map<String, dynamic>.from(event as Map)));
    return _eventStream!;
  }
}

4.3 Public API and Models

Edit lib/biometrid_full_plugin.dart:

import 'biometrid_full_plugin_platform_interface.dart';

export 'biometrid_full_plugin.dart'
    show BiometridFullPlugin, BiometridEvent, BiometridErrorInfo;

/// Flutter plugin for BiometridFull identity verification SDK.
///
/// Provides biometric verification capabilities including Liveness,
/// NFC, AutoCapture, OTP, Video Conference, and Video Liveness.
class BiometridFullPlugin {
  /// Initializes the BiometridFull SDK.
  ///
  /// Must be called before [start]. Connects to the Biometrid backend
  /// and prepares all required services.
  ///
  /// Parameters:
  /// - [url]: The Biometrid API base URL (e.g., `https://api.biometrid.com/`)
  /// - [app]: Your application identifier provided by Biometrid
  /// - [appUrl]: The Biometrid web app URL (e.g., `https://app.biometrid.com/`)
  /// - [credential]: Your credential key provided by Biometrid
  /// - [language]: Language code for the verification interface (default: `en-GB`)
  ///   Supported: `en-GB`, `pt-PT`, `fr-FR`, `es-ES`, `it-IT`
  /// - [customHeaders]: Optional custom HTTP headers for API requests
  static Future<bool> initialize({
    required String url,
    required String app,
    required String appUrl,
    required String credential,
    String language = 'en-GB',
    Map<String, dynamic>? customHeaders,
  }) {
    return BiometridFullPluginPlatform.instance.initialize(
      url: url,
      app: app,
      appUrl: appUrl,
      credential: credential,
      language: language,
      customHeaders: customHeaders,
    );
  }

  /// Starts the identity verification process.
  ///
  /// Presents the WebView-based verification interface with native
  /// biometric capabilities.
  ///
  /// Parameters:
  /// - [processId]: Optional process ID. Pass `null` to create a new
  ///   process, or provide an existing ID to resume a previous process.
  static Future<bool> start({String? processId}) {
    return BiometridFullPluginPlatform.instance.start(processId: processId);
  }

  /// Stops the current verification process and cleans up resources.
  ///
  /// Cancels any active WebView sessions and pending native operations.
  /// Safe to call at any point.
  static Future<bool> stop() {
    return BiometridFullPluginPlatform.instance.stop();
  }

  /// Stream of SDK lifecycle events.
  ///
  /// Emits events for initialization, process creation, step updates,
  /// process completion, and errors.
  static Stream<BiometridEvent> get onEvent {
    return BiometridFullPluginPlatform.instance.onEvent;
  }
}

/// Represents an event emitted by the BiometridFull SDK.
class BiometridEvent {
  /// The event type: `initialized`, `processCreated`, `processUpdated`,
  /// `processFinished`, or `error`.
  final String type;

  /// Process ID (available for process-related events).
  final String? processId;

  /// Step ID (available for `processUpdated` events).
  final String? stepId;

  /// Action type (available for `processUpdated` events).
  final String? action;

  /// Initialization/error status.
  final bool? status;

  /// Error information (available for `initialized` and `error` events).
  final BiometridErrorInfo? error;

  BiometridEvent({
    required this.type,
    this.processId,
    this.stepId,
    this.action,
    this.status,
    this.error,
  });

  factory BiometridEvent.fromMap(Map<String, dynamic> map) {
    return BiometridEvent(
      type: map['type'] as String,
      processId: map['processId'] as String?,
      stepId: map['stepId'] as String?,
      action: map['action'] as String?,
      status: map['status'] as bool?,
      error: map['error'] != null
          ? BiometridErrorInfo.fromMap(
              Map<String, dynamic>.from(map['error'] as Map))
          : null,
    );
  }

  @override
  String toString() =>
      'BiometridEvent(type: $type, processId: $processId, '
      'stepId: $stepId, action: $action, status: $status, error: $error)';
}

/// Error information from the BiometridFull SDK.
class BiometridErrorInfo {
  /// Error code identifier (e.g., `"MBF001"`).
  /// Codes follow the prefix convention: `MBF` = BiometridFull module errors.
  final String? code;

  /// Human-readable error description.
  final String? message;

  BiometridErrorInfo({this.code, this.message});

  factory BiometridErrorInfo.fromMap(Map<String, dynamic> map) {
    return BiometridErrorInfo(
      code: map['code'] as String?,
      message: map['message'] as String?,
    );
  }

  @override
  String toString() => 'BiometridErrorInfo(code: $code, message: $message)';
}

Step 5: Usage in a Flutter App

5.1 Add the Plugin Dependency

In your Flutter app's pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  biometrid_full_plugin:
    path: ../biometrid_full_plugin # Or a git/pub reference

5.2 Complete Example

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:biometrid_full_plugin/biometrid_full_plugin.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Biometrid Verification',
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const VerificationScreen(),
    );
  }
}

class VerificationScreen extends StatefulWidget {
  const VerificationScreen({super.key});

  @override
  State<VerificationScreen> createState() => _VerificationScreenState();
}

class _VerificationScreenState extends State<VerificationScreen> {
  bool _isInitialized = false;
  bool _isLoading = false;
  bool _isProcessCompleted = false;
  String? _processId;
  String? _errorMessage;
  StreamSubscription<BiometridEvent>? _eventSubscription;

  @override
  void initState() {
    super.initState();
    _listenToEvents();
  }

  @override
  void dispose() {
    _eventSubscription?.cancel();
    BiometridFullPlugin.stop();
    super.dispose();
  }

  void _listenToEvents() {
    _eventSubscription = BiometridFullPlugin.onEvent.listen((event) {
      setState(() {
        switch (event.type) {
          case 'initialized':
            _isInitialized = event.status ?? false;
            if (event.error != null) {
              _errorMessage = event.error!.message;
            }
            break;
          case 'processCreated':
            _processId = event.processId;
            break;
          case 'processUpdated':
            // Step completed: event.stepId, event.action
            debugPrint(
              'Step updated: ${event.stepId} - ${event.action}',
            );
            break;
          case 'processFinished':
            _isProcessCompleted = true;
            _isLoading = false;
            break;
          case 'error':
            _errorMessage = event.error?.message ?? 'Unknown error';
            _isLoading = false;
            break;
        }
      });
    });
  }

  Future<void> _initialize() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      await BiometridFullPlugin.initialize(
        url: 'https://api.biometrid.com/',
        app: 'your-app-id',
        appUrl: 'https://app.biometrid.com/',
        credential: 'your-credential',
        language: 'en-GB',
      );
    } catch (e) {
      setState(() {
        _errorMessage = 'Initialization failed: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _startVerification() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
      _isProcessCompleted = false;
    });

    try {
      await BiometridFullPlugin.start();
    } catch (e) {
      setState(() {
        _errorMessage = 'Start failed: $e';
        _isLoading = false;
      });
    }
  }

  Future<void> _stop() async {
    await BiometridFullPlugin.stop();
    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Biometrid Verification')),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Status Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'SDK Status',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    _StatusRow(
                      label: 'Initialized',
                      value: _isInitialized,
                    ),
                    if (_processId != null)
                      Text('Process: $_processId'),
                    if (_isProcessCompleted)
                      const Text(
                        'Verification Complete!',
                        style: TextStyle(
                          color: Colors.green,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Error Banner
            if (_errorMessage != null)
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.red.shade50,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text(
                  _errorMessage!,
                  style: TextStyle(color: Colors.red.shade700),
                ),
              ),

            const SizedBox(height: 16),

            // Loading Indicator
            if (_isLoading) const Center(child: CircularProgressIndicator()),

            const Spacer(),

            // Action Buttons
            ElevatedButton(
              onPressed: (!_isInitialized && !_isLoading)
                  ? _initialize
                  : null,
              child: const Text('Initialize SDK'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: (_isInitialized && !_isLoading)
                  ? _startVerification
                  : null,
              child: const Text('Start Verification'),
            ),
            const SizedBox(height: 12),
            OutlinedButton(
              onPressed: _isLoading ? _stop : null,
              child: const Text('Stop'),
            ),
          ],
        ),
      ),
    );
  }
}

class _StatusRow extends StatelessWidget {
  final String label;
  final bool value;

  const _StatusRow({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(
          value ? Icons.check_circle : Icons.radio_button_unchecked,
          color: value ? Colors.green : Colors.grey,
          size: 18,
        ),
        const SizedBox(width: 8),
        Text(label),
      ],
    );
  }
}

Step 6: Consumer App Build Configuration

Android build.gradle (app-level)

Ensure the consumer app has the required repositories in android/build.gradle:

allprojects {
    repositories {
        google()
        mavenCentral()

        // BiometridFull main artifact + all bridges
        maven { url "https://dl.cloudsmith.io/public/biometrid/mobile/maven/" }

        // Liveness bridge
        maven { url "https://dl.cloudsmith.io/public/biometrid/face01-liveness/maven/" }
        maven { url "https://raw.githubusercontent.com/iProov/android/master/maven/" }

        // NFC bridge
        maven { url "https://maven.regulaforensics.com/RegulaDocumentReader/" }
    }
}

Only add the bridge repositories for the steps you actually use. AutoCapture, VideoConference, and VideoLiveness bridges resolve from mavenCentral and need no extra repo.

Set minSdkVersion in android/app/build.gradle:

android {
    defaultConfig {
        minSdk 26
    }
}

API Reference Summary

Dart API

Method Description
BiometridFullPlugin.initialize(...) Initializes SDK with credentials. Must be called first.
BiometridFullPlugin.start({processId}) Starts verification. Pass null for new process or an ID to resume.
BiometridFullPlugin.stop() Stops the current process and cleans up resources.
BiometridFullPlugin.onEvent Stream of BiometridEvent for lifecycle callbacks.

Event Types

Event Type Fields Description
initialized status, error? SDK initialization completed
processCreated processId New verification process created
processUpdated processId, stepId, action A step was completed
processFinished processId Verification process completed
error status, error An error occurred

Configuration Parameters

Parameter Type Required Description
url String Yes Biometrid API base URL
app String Yes Application identifier
appUrl String Yes Biometrid web application URL
credential String Yes Credential key
language String No Language code (default: en-GB)
customHeaders Map<String, dynamic>? No Custom HTTP headers

Supported Languages

Code Language
en-GB English
pt-PT Portuguese
fr-FR French
es-ES Spanish
it-IT Italian

Troubleshooting

Android

Issue Solution
Duplicate class errors Add packagingOptions { exclude 'META-INF/*.kotlin_module' } to your app's build.gradle
minSdk version conflict Ensure minSdk 26 in your app's build.gradle
Missing repositories Verify all Maven repository URLs are added to the project-level build.gradle
Activity not found Ensure required activities are declared in AndroidManifest.xml
ProGuard issues Add -keep class com.biometrid.** { *; } to your ProGuard rules

iOS

Issue Solution
Pod install fails Ensure all source URLs are added to Podfile
Deployment target mismatch Set IPHONEOS_DEPLOYMENT_TARGET = '16.0' in post_install
Missing permissions Verify Info.plist keys for Camera, Microphone, and NFC
Framework not found Run pod install --repo-update to refresh pod specs
Build library for distribution Add BUILD_LIBRARY_FOR_DISTRIBUTION = 'YES' in post_install
NFC entitlement missing Enable "Near Field Communication Tag Reading" capability in Xcode

General

Issue Solution
Events not received Ensure onEvent stream is subscribed before calling initialize()
NOT_INITIALIZED error Call initialize() and wait for completion before calling start()
Process not resuming Pass the exact processId received from processCreated event

ProGuard Rules (Android)

If your app uses ProGuard/R8, add these rules to android/app/proguard-rules.pro:

# BiometridFull SDK
-keep class com.biometrid.** { *; }
-keep interface com.biometrid.** { *; }

# Kotlin Serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt

# Kotlin Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}

Version Compatibility

Component Version
BiometridFull Android (Maven) com.biometrid:biometridfull:3.3.3
BiometridFull Android bridges com.biometrid:biometridfull-{liveness,nfc,autocapture,videoconference,videoliveness}:3.3.3
BiometridFull iOS (CocoaPods) BiometridFull ~> 3.3.3
iOS Deployment Target 16.0
Android Min SDK 26
Android Compile SDK 35
Kotlin 2.2.0+
Swift 5.9+