blusalt_liveness_native 2.1.4 copy "blusalt_liveness_native: ^2.1.4" to clipboard
blusalt_liveness_native: ^2.1.4 copied to clipboard

Liveness SDK for Android and IOS

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:blusalt_liveness_native/enums.dart';
import 'package:blusalt_liveness_native/liveness.dart';
import 'package:blusalt_liveness_native/model.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:liveness_example/json_viewer_widget.dart';

void main() {
  // ensureInitialized(), Firebase.initializeApp(), and runApp() must all run
  // in the same zone. Wrapping everything in runZonedGuarded here satisfies
  // that requirement while also catching any async error that escapes all
  // other handlers.
  runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();

    final crashlytics = FirebaseCrashlytics.instance;

    // Disable dashboard uploads in debug so test runs don't pollute reports.
    await crashlytics.setCrashlyticsCollectionEnabled(true);

    await crashlytics.setCustomKey(
      'build_mode',
      kDebugMode ? 'debug' : (kProfileMode ? 'profile' : 'release'),
    );

    // ── Flutter framework errors ───────────────────────────────────────────
    // Widget build failures, rendering errors, animation errors, etc.
    FlutterError.onError = (FlutterErrorDetails details) {
      crashlytics.recordFlutterError(details, fatal: true);
    };

    // ── Dart async / platform-dispatcher errors ────────────────────────────
    // Unhandled isolate throws, platform channel callbacks, Futures with no
    // catchError anywhere in the chain.
    PlatformDispatcher.instance.onError = (Object error, StackTrace stack) {
      crashlytics.recordError(error, stack, fatal: true);
      return true;
    };

    runApp(const MyApp());
  }, (Object error, StackTrace stack) {
    // ── Zone-level catch-all ───────────────────────────────────────────────
    // Plugin/package errors, dart:io errors, unexpected async throws that
    // slipped past the handlers above. Zone caught them → app still alive → non-fatal.
    FirebaseCrashlytics.instance.recordError(
      error,
      stack,
      fatal: false,
      reason: 'Caught by root error zone',
    );
    if (kDebugMode) debugPrint('[ZONE ERROR] $error\n$stack');
  });
}

enum VerificationCompletedType { comparison, detection, none }

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _livenessPlugin = BlusaltLivenessNative();

  String? imageUri;
  TextEditingController clientIdController = TextEditingController(text: '');
  TextEditingController appNameController = TextEditingController(text: '');
  TextEditingController apiKeyController = TextEditingController(text: '');
  TextEditingController webHookUrlController = TextEditingController();
  TextEditingController referenceController = TextEditingController();
  TextEditingController thresholdController = TextEditingController();
  TextEditingController timeoutDurationController = TextEditingController();
  bool isDev = true;

  bool isShowScore = false;
  bool isShowThreshold = false;
  bool startProcessOnGettingToFirstScreen = false;
  bool showLivenessResult = false;
  bool collectDeviceLocation = false;
  bool collectDeviceId = false;
  bool enableEncryption = false;
  ThresholdPriority thresholdPriority = ThresholdPriority.localOnly;

  LivenessFacialComparisonType facialComparisonType =
      LivenessFacialComparisonType.action;
  LivenessDetectionOnlyType detectionOnlyType =
      LivenessDetectionOnlyType.action;

  VerificationCompletedType verificationType = VerificationCompletedType.none;
  final ImagePicker _picker = ImagePicker();

  BlusaltLivenessResultResponse? resultResponse;

  @override
  void dispose() {
    super.dispose();
    clientIdController.dispose();
    appNameController.dispose();
    apiKeyController.dispose();
    webHookUrlController.dispose();
    referenceController.dispose();
    thresholdController.dispose();
    timeoutDurationController.dispose();
  }

  /// Attaches SDK-call context keys so crashes show what was in flight.
  Future<void> _setSdkContext(String operation) async {
    final crashlytics = FirebaseCrashlytics.instance;
    await crashlytics.setCustomKey('last_sdk_operation', operation);
    await crashlytics.setCustomKey('is_dev_environment', isDev.toString());
    await crashlytics.setCustomKey(
        'client_id', clientIdController.text.isEmpty ? 'empty' : 'set');
    await crashlytics.setCustomKey(
        'api_key', apiKeyController.text.isEmpty ? 'empty' : 'set');
  }

  Future<BlusaltLivenessResultResponse?> startFaceComparison() async {
    try {
      final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
      if (image == null) return null;

      setState(() => imageUri = image.path);
      await _setSdkContext('facial_comparison');

      resultResponse = await _livenessPlugin.startFacialComparisonSDK(
        apiKey: apiKeyController.text,
        appName: appNameController.text,
        clientId: clientIdController.text,
        isDev: isDev,
        webhookUrl: webHookUrlController.text.isEmpty
            ? null
            : webHookUrlController.text,
        reference: referenceController.text,
        imageData: await image.readAsBytes(),
        livenessFacialComparisonType: facialComparisonType,
        startProcessOnGettingToFirstScreen: startProcessOnGettingToFirstScreen,
        showLivenessResult: showLivenessResult,
        collectDeviceLocation: collectDeviceLocation,
        collectDeviceId: collectDeviceId,
        thresholdPriority: thresholdPriority,
        showScore: isShowScore,
        showThreshold: isShowThreshold,
        enableEncryption: enableEncryption,
        thresholdInPercent: validateNumber(thresholdController.text),
        timeoutDurationInSec:
            validateDurationNumber(timeoutDurationController.text),
      );

      if (resultResponse?.blusaltLivenessProcess ==
          BlusaltLivenessProcess.completed) {
        setState(() => verificationType = VerificationCompletedType.comparison);
      } else {
        // Non-completed result is not a crash, but log it for visibility.
        await FirebaseCrashlytics.instance.log(
          'FacialComparison non-completed: '
          'code=${resultResponse?.code} '
          'msg=${resultResponse?.message}',
        );
        debugPrint(resultResponse?.code ?? '');
        debugPrint(resultResponse?.message ?? '');
      }
      return resultResponse;
    } on PlatformException catch (e, stack) {
      // Plugin threw a native-side exception (permission denied, camera failure, etc.)
      await FirebaseCrashlytics.instance.recordError(
        e,
        stack,
        fatal: false,
        reason: 'PlatformException during facial comparison',
        information: ['code: ${e.code}', 'message: ${e.message}'],
      );
      debugPrint(
          '[PlatformException] facial_comparison: ${e.code} – ${e.message}');
      return null;
    } catch (e, stack) {
      await FirebaseCrashlytics.instance.recordError(
        e,
        stack,
        fatal: false,
        reason: 'Unexpected error during facial comparison',
      );
      debugPrint('[ERROR] facial_comparison: $e');
      return null;
    }
  }

  Future<BlusaltLivenessResultResponse?> startLivenessDetectionOnly() async {
    try {
      await _setSdkContext('liveness_detection');

      resultResponse = await _livenessPlugin.startLivenessDetectionOnlySDK(
        apiKey: apiKeyController.text,
        appName: appNameController.text,
        clientId: clientIdController.text,
        isDev: isDev,
        livenessDetectionOnlyType: detectionOnlyType,
        webhookUrl: webHookUrlController.text.isEmpty
            ? null
            : webHookUrlController.text,
        reference: referenceController.text,
        startProcessOnGettingToFirstScreen: startProcessOnGettingToFirstScreen,
        showLivenessResult: showLivenessResult,
        collectDeviceLocation: collectDeviceLocation,
        collectDeviceId: collectDeviceId,
        enableEncryption: enableEncryption,
        timeoutDurationInSec:
            validateDurationNumber(timeoutDurationController.text),
      );

      if (resultResponse?.blusaltLivenessProcess ==
          BlusaltLivenessProcess.completed) {
        setState(() => verificationType = VerificationCompletedType.detection);
      } else {
        await FirebaseCrashlytics.instance.log(
          'LivenessDetection non-completed: '
          'code=${resultResponse?.code} '
          'msg=${resultResponse?.message}',
        );
        debugPrint(resultResponse?.code ?? '');
        debugPrint(resultResponse?.message ?? '');
      }
      return resultResponse;
    } on PlatformException catch (e, stack) {
      await FirebaseCrashlytics.instance.recordError(
        e,
        stack,
        fatal: false,
        reason: 'PlatformException during liveness detection',
        information: ['code: ${e.code}', 'message: ${e.message}'],
      );
      debugPrint(
          '[PlatformException] liveness_detection: ${e.code} – ${e.message}');
      return null;
    } catch (e, stack) {
      await FirebaseCrashlytics.instance.recordError(
        e,
        stack,
        fatal: false,
        reason: 'Unexpected error during liveness detection',
      );
      debugPrint('[ERROR] liveness_detection: $e');
      return null;
    }
  }

  double? validateNumber(String value) {
    if (value.isEmpty) return null;
    return double.tryParse(value);
  }

  int? validateDurationNumber(String value) {
    if (value.isEmpty) return null;
    return int.tryParse(value);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              if (imageUri != null)
                Image.file(
                  File(imageUri!),
                  width: 100,
                  height: 100,
                )
              else
                const Padding(
                  padding: EdgeInsets.only(top: 20),
                  child: Text("No image selected"),
                ),
              const SizedBox(height: 14),
              TextField(
                controller: clientIdController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter client id",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                onChanged: (text) => setState(() {}),
              ),
              const SizedBox(height: 14),
              TextField(
                controller: appNameController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter app name",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                onChanged: (text) => setState(() {}),
              ),
              const SizedBox(height: 14),
              TextField(
                controller: apiKeyController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter api key",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                onChanged: (text) => setState(() {}),
              ),
              const SizedBox(height: 14),
              TextField(
                controller: webHookUrlController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter webhook url (Optional)",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                onChanged: (text) => setState(() {}),
              ),
              const SizedBox(height: 14),
              TextField(
                controller: referenceController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter reference (Optional)",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                onChanged: (text) => setState(() {}),
              ),
              const SizedBox(height: 14),
              TextField(
                controller: timeoutDurationController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText:
                      "Enter a duration you want SDK to timeout in seconds (Optional)",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                keyboardType: TextInputType.number,
                inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                maxLines: 2,
              ),
              const SizedBox(height: 4),
              CustomCheckbox(
                label: "Point to Development Environment?",
                value: isDev,
                onCheck: (value) => setState(() => isDev = value),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Start Process On Getting To First Screen?",
                value: startProcessOnGettingToFirstScreen,
                onCheck: (value) =>
                    setState(() => startProcessOnGettingToFirstScreen = value),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Show Liveness Result Screen",
                value: showLivenessResult,
                onCheck: (value) => setState(() => showLivenessResult = value),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Collect Device Location?",
                value: collectDeviceLocation,
                onCheck: (value) =>
                    setState(() => collectDeviceLocation = value),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Collect Device Id?",
                value: collectDeviceId,
                onCheck: (value) => setState(() => collectDeviceId = value),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Enable Encryption?",
                value: enableEncryption,
                onCheck: (value) => setState(() => enableEncryption = value),
              ),
              const SizedBox(height: 14),
              DropdownButtonFormField<LivenessDetectionOnlyType>(
                initialValue: detectionOnlyType,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: "Liveness Detection Type",
                ),
                items: LivenessDetectionOnlyType.values.map((type) {
                  return DropdownMenuItem(
                    value: type,
                    child: Text(type.name.toUpperCase()),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null) setState(() => detectionOnlyType = value);
                },
              ),
              const SizedBox(height: 34),
              const Text(
                'More Options Specific to Facial Comparison:',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Show Percent on completion",
                value: isShowScore,
                onCheck: (value) => setState(() => isShowScore = value),
              ),
              const SizedBox(height: 2),
              CustomCheckbox(
                label: "Show Threshold on completion",
                value: isShowThreshold,
                onCheck: (value) => setState(() => isShowThreshold = value),
              ),
              const SizedBox(height: 14),
              DropdownButtonFormField<ThresholdPriority>(
                initialValue: thresholdPriority,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: "Threshold Priority",
                ),
                items: ThresholdPriority.values.map((priority) {
                  return DropdownMenuItem(
                    value: priority,
                    child: Text(priority.name.toUpperCase()),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null) setState(() => thresholdPriority = value);
                },
              ),
              const SizedBox(height: 14),
              DropdownButtonFormField<LivenessFacialComparisonType>(
                initialValue: facialComparisonType,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: "Facial Comparison Type",
                ),
                items: LivenessFacialComparisonType.values.map((type) {
                  return DropdownMenuItem(
                    value: type,
                    child: Text(type.name.toUpperCase()),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null)
                    setState(() => facialComparisonType = value);
                },
              ),
              const SizedBox(height: 14),
              TextField(
                controller: thresholdController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText:
                      "Enter a threshold number for facial comparison which ranges from 0-100 (Optional)",
                  hintStyle: TextStyle(color: Color(0xFFCCCCCC)),
                ),
                keyboardType: TextInputType.number,
                inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                maxLines: 2,
              ),
              const SizedBox(height: 24),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: startLivenessDetectionOnly,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.blue,
                        minimumSize: const Size(double.infinity, 50),
                      ),
                      child: const Text(
                        "Start SDK Liveness Detection",
                        textAlign: TextAlign.center,
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: startFaceComparison,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.blue,
                        minimumSize: const Size(double.infinity, 50),
                      ),
                      child: const Text(
                        "Start SDK Facial Comparison",
                        textAlign: TextAlign.center,
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                ],
              ),
              if (verificationType != VerificationCompletedType.none)
                const SizedBox(height: 34),
              if (verificationType != VerificationCompletedType.none)
                Text(
                  "Result: (${verificationType == VerificationCompletedType.comparison ? 'Facial Comparison' : 'Face Detection'})",
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontWeight: FontWeight.w500),
                ),
              if (verificationType != VerificationCompletedType.none)
                const SizedBox(height: 4),
              if (verificationType != VerificationCompletedType.none)
                JsonViewerWidget(jsonData: resultResponse?.toJson()),
              const SizedBox(height: 34),
              Image.asset(
                'assets/images/blusalt_logo.png',
                width: 100,
                height: 100,
                color: Colors.black,
              ),
              const SizedBox(height: 24),
            ],
          ),
        ),
      ),
    );
  }
}

class CustomCheckbox extends StatelessWidget {
  final String label;
  final bool value;
  final Function(bool) onCheck;

  const CustomCheckbox({
    Key? key,
    required this.label,
    required this.value,
    required this.onCheck,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Checkbox(
          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
          value: value,
          onChanged: (newValue) => onCheck(newValue ?? false),
        ),
        Expanded(
          child: Text(
            label,
            style: const TextStyle(fontWeight: FontWeight.w500),
          ),
        ),
      ],
    );
  }
}
2
likes
130
points
597
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Liveness SDK for Android and IOS

Homepage

License

BSD-3-Clause (license)

Dependencies

flutter, flutter_web_plugins

More

Packages that depend on blusalt_liveness_native

Packages that implement blusalt_liveness_native