mml_face_sdk 0.1.1-dev.8 copy "mml_face_sdk: ^0.1.1-dev.8" to clipboard
mml_face_sdk: ^0.1.1-dev.8 copied to clipboard

Offline, on-device 1:1 face verification and passive liveness SDK.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mml_face_sdk/mml_face_sdk.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  runApp(const DemoApp());
}

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

  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    title: 'MML Face SDK',
    theme: ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xff19c5b6),
        brightness: Brightness.light,
      ),
      scaffoldBackgroundColor: const Color(0xfff4f7fb),
      cardTheme: const CardThemeData(elevation: 0, color: Colors.white),
      inputDecorationTheme: InputDecorationTheme(
        filled: true,
        fillColor: const Color(0xfff3f6fa),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(16),
          borderSide: BorderSide.none,
        ),
      ),
    ),
    home: const DemoHome(),
  );
}

class EnrolledProfile {
  const EnrolledProfile({required this.name, required this.template});

  final String name;
  final FaceTemplate template;

  Map<String, Object> toJson() => {
    'version': 1,
    'name': name,
    'template': template.toJson(),
  };

  factory EnrolledProfile.fromJson(Map<String, Object?> json) {
    if (json['version'] != 1 || json['name'] is! String) {
      throw const FormatException('Invalid profile');
    }
    return EnrolledProfile(
      name: (json['name']! as String).trim(),
      template: FaceTemplate.fromJson(
        (json['template']! as Map).cast<String, Object?>(),
      ),
    );
  }
}

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

  @override
  State<DemoHome> createState() => _DemoHomeState();
}

class _DemoHomeState extends State<DemoHome> {
  static const storage = FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
  );
  static const publicKeyB64 = String.fromEnvironment(
    'MML_LICENSE_PUBLIC_KEY',
    defaultValue: 'vJZyhTYJIS4JWop8tIlY1Juyzpyv434M_BUQhgHzgDY',
  );
  static const activationUrl = String.fromEnvironment(
    'MML_ACTIVATION_URL',
    defaultValue:
        'https://mml-face-license.hamzaasif19974-69b.workers.dev/v1/activate',
  );
  static const demoActivationKey = String.fromEnvironment(
    'MML_DEMO_ACTIVATION_KEY',
  );

  final activationCode = TextEditingController();
  MmlFaceSdk? sdk;
  EnrolledProfile? profile;
  String status = 'Starting secure face engine…';
  bool ready = false;
  bool busy = true;

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

  Future<void> _start() async {
    sdk = MmlFaceSdk(
      matchThreshold: .75,
      publicLicenseKey: SimplePublicKey(
        base64Url.decode(base64Url.normalize(publicKeyB64)),
        type: KeyPairType.ed25519,
      ),
    );
    try {
      final token = await storage.read(key: 'license');
      if (token != null) {
        await sdk!.initialize(license: token);
      } else if (demoActivationKey.isNotEmpty) {
        await _activateWithCode(demoActivationKey);
      }
      await _loadProfile();
      if (!mounted) return;
      setState(() {
        ready = token != null || demoActivationKey.isNotEmpty;
        busy = false;
        status = ready ? 'On-device engine ready' : 'Activation required';
      });
    } catch (_) {
      if (demoActivationKey.isNotEmpty) {
        try {
          await _activateWithCode(demoActivationKey);
          await _loadProfile();
          if (!mounted) return;
          setState(() {
            ready = true;
            busy = false;
            status = 'On-device engine ready';
          });
          return;
        } catch (_) {}
      }
      if (!mounted) return;
      setState(() {
        ready = false;
        busy = false;
        status = 'Activation required';
      });
    }
  }

  Future<void> _loadProfile() async {
    final savedProfile = await storage.read(key: 'profile');
    if (savedProfile != null) {
      profile = EnrolledProfile.fromJson(jsonDecode(savedProfile));
      return;
    }
    final legacyTemplate = await storage.read(key: 'template');
    if (legacyTemplate != null) {
      profile = EnrolledProfile(
        name: 'Enrolled user',
        template: FaceTemplate.fromJson(jsonDecode(legacyTemplate)),
      );
      await storage.write(key: 'profile', value: jsonEncode(profile));
      await storage.delete(key: 'template');
    }
  }

  Future<void> _activateWithCode(String code) async {
    final token = await sdk!.activate(
      endpoint: Uri.parse(activationUrl),
      activationCode: code,
    );
    await storage.write(key: 'license', value: token);
  }

  Future<void> _activate() async {
    if (busy || activationCode.text.trim().isEmpty) return;
    setState(() {
      busy = true;
      status = 'Activating licence and installing models…';
    });
    try {
      await _activateWithCode(activationCode.text.trim());
      if (!mounted) return;
      setState(() {
        ready = true;
        status = 'On-device engine ready';
      });
    } catch (_) {
      if (mounted) setState(() => status = 'Activation failed');
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> _openEnrollment() async {
    final enrolled = await Navigator.of(context).push<EnrolledProfile>(
      MaterialPageRoute(
        builder: (_) =>
            EnrollmentCameraPage(sdk: sdk!, existingName: profile?.name),
      ),
    );
    if (enrolled == null) return;
    await storage.write(key: 'profile', value: jsonEncode(enrolled));
    if (!mounted) return;
    setState(() {
      profile = enrolled;
      status = '${enrolled.name} is enrolled on this device';
    });
  }

  Future<void> _openVerification(bool liveness) async {
    final enrolled = profile;
    if (!ready || enrolled == null) return;
    await Navigator.of(context).push<void>(
      MaterialPageRoute(
        builder: (_) => LiveVerificationPage(
          sdk: sdk!,
          profile: enrolled,
          requireLiveness: liveness,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final enrolled = profile;
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverToBoxAdapter(
            child: Container(
              padding: EdgeInsets.fromLTRB(
                22,
                MediaQuery.paddingOf(context).top + 22,
                22,
                30,
              ),
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                  colors: [Color(0xff071a2e), Color(0xff123b4a)],
                ),
                borderRadius: BorderRadius.vertical(
                  bottom: Radius.circular(34),
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Container(
                        width: 46,
                        height: 46,
                        decoration: BoxDecoration(
                          color: const Color(0xff19c5b6),
                          borderRadius: BorderRadius.circular(15),
                        ),
                        child: const Icon(
                          Icons.face_retouching_natural,
                          color: Colors.white,
                        ),
                      ),
                      const SizedBox(width: 13),
                      const Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              'MML FACE SDK',
                              style: TextStyle(
                                color: Colors.white,
                                fontWeight: FontWeight.w800,
                                letterSpacing: 1.1,
                              ),
                            ),
                            Text(
                              'Private, on-device verification',
                              style: TextStyle(color: Color(0xffa8c3cd)),
                            ),
                          ],
                        ),
                      ),
                      const Icon(Icons.shield_outlined, color: Colors.white70),
                    ],
                  ),
                  const SizedBox(height: 32),
                  const Text(
                    'Face verification\nthat stays on device.',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 31,
                      height: 1.12,
                      fontWeight: FontWeight.w800,
                    ),
                  ),
                  const SizedBox(height: 18),
                  Row(
                    children: [
                      if (busy)
                        const SizedBox(
                          width: 18,
                          height: 18,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Color(0xff19c5b6),
                          ),
                        )
                      else
                        Icon(
                          ready ? Icons.check_circle : Icons.info_outline,
                          size: 19,
                          color: ready ? const Color(0xff56dfbd) : Colors.amber,
                        ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          status,
                          style: const TextStyle(
                            color: Color(0xffd7e6ea),
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          SliverPadding(
            padding: const EdgeInsets.fromLTRB(18, 22, 18, 32),
            sliver: SliverList.list(
              children: [
                if (demoActivationKey.isEmpty && !ready) ...[
                  _SectionCard(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const _CardTitle(
                          icon: Icons.key_outlined,
                          title: 'Activate SDK',
                          subtitle: 'Enter the key issued for this app.',
                        ),
                        const SizedBox(height: 18),
                        TextField(
                          controller: activationCode,
                          decoration: const InputDecoration(
                            labelText: 'Customer app licence key',
                          ),
                        ),
                        const SizedBox(height: 12),
                        SizedBox(
                          width: double.infinity,
                          child: FilledButton(
                            onPressed: busy ? null : _activate,
                            child: const Text('Activate securely'),
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 16),
                ],
                _SectionCard(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _CardTitle(
                        icon: enrolled == null
                            ? Icons.person_add_alt_1
                            : Icons.verified_user_outlined,
                        title: enrolled == null
                            ? 'Register a face'
                            : enrolled.name,
                        subtitle: enrolled == null
                            ? 'Capture one clear photo and add a name.'
                            : 'Face profile saved locally on this device.',
                      ),
                      const SizedBox(height: 18),
                      SizedBox(
                        width: double.infinity,
                        child: FilledButton.icon(
                          onPressed: ready && !busy ? _openEnrollment : null,
                          icon: Icon(
                            enrolled == null
                                ? Icons.camera_alt_outlined
                                : Icons.refresh,
                          ),
                          label: Text(
                            enrolled == null
                                ? 'Open registration camera'
                                : 'Replace registration',
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 16),
                _SectionCard(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const _CardTitle(
                        icon: Icons.center_focus_strong,
                        title: 'Live verification',
                        subtitle:
                            'Keep the camera open while results update automatically.',
                      ),
                      const SizedBox(height: 18),
                      _ActionTile(
                        icon: Icons.face_outlined,
                        title: 'Face recognition',
                        subtitle: 'Match the live face to the saved profile',
                        color: const Color(0xff246bfd),
                        enabled: ready && enrolled != null,
                        onTap: () => _openVerification(false),
                      ),
                      const SizedBox(height: 12),
                      _ActionTile(
                        icon: Icons.shield_outlined,
                        title: 'Liveness + recognition',
                        subtitle: 'Check for a live face before matching',
                        color: const Color(0xff11a985),
                        enabled: ready && enrolled != null,
                        onTap: () => _openVerification(true),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 18),
                const Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(
                      Icons.lock_outline,
                      size: 16,
                      color: Color(0xff637487),
                    ),
                    SizedBox(width: 6),
                    Flexible(
                      child: Text(
                        'Photos and templates never leave this device',
                        style: TextStyle(
                          color: Color(0xff637487),
                          fontSize: 12,
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    activationCode.dispose();
    sdk?.dispose();
    super.dispose();
  }
}

class EnrollmentCameraPage extends StatefulWidget {
  const EnrollmentCameraPage({super.key, required this.sdk, this.existingName});

  final MmlFaceSdk sdk;
  final String? existingName;

  @override
  State<EnrollmentCameraPage> createState() => _EnrollmentCameraPageState();
}

class _EnrollmentCameraPageState extends State<EnrollmentCameraPage> {
  CameraController? controller;
  CameraImage? latestFrame;
  String message = 'Position one face inside the guide';
  bool processing = false;
  bool cameraReady = false;

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

  Future<void> _initializeCamera() async {
    try {
      controller = await _createFrontCamera(ResolutionPreset.high);
      if (Platform.isAndroid) {
        await controller!.startImageStream((frame) {
          latestFrame = frame;
          if (mounted && !cameraReady) {
            setState(() {
              cameraReady = true;
              message = 'Position one face inside the guide';
            });
          }
        });
      } else if (mounted) {
        setState(() => cameraReady = true);
      }
    } catch (_) {
      if (mounted) setState(() => message = 'Camera permission is required');
    }
  }

  Future<void> _capture() async {
    final camera = controller;
    if (!cameraReady || processing || camera == null) return;
    setState(() {
      processing = true;
      message = 'Checking face quality…';
    });
    try {
      final FaceTemplate template;
      if (Platform.isAndroid) {
        final frame = latestFrame;
        if (frame == null) throw StateError('Camera frame unavailable');
        template = await widget.sdk.createTemplateFromFrame(
          nv21: _mergeNv21(frame),
          width: frame.width,
          height: frame.height,
          rotationDegrees: _rotationDegrees(camera),
        );
      } else {
        final photo = await camera.takePicture();
        template = await widget.sdk.createTemplate(await photo.readAsBytes());
      }
      if (!mounted) return;
      final name = await _askForName(widget.existingName);
      if (name == null || !mounted) {
        setState(() {
          processing = false;
          message = 'Add a name to finish registration';
        });
        return;
      }
      Navigator.of(
        context,
      ).pop(EnrolledProfile(name: name, template: template));
    } on FaceSdkException catch (error) {
      if (!mounted) return;
      setState(() {
        processing = false;
        message = _friendlyError(error.code);
      });
    } catch (_) {
      if (!mounted) return;
      setState(() {
        processing = false;
        message = 'Could not capture the face. Please try again.';
      });
    }
  }

  Future<String?> _askForName(String? existing) async {
    final input = TextEditingController(
      text: existing == 'Enrolled user' ? '' : existing,
    );
    String? error;
    final result = await showDialog<String>(
      context: context,
      barrierDismissible: false,
      builder: (context) => StatefulBuilder(
        builder: (context, setDialogState) => AlertDialog(
          icon: const Icon(
            Icons.check_circle,
            color: Color(0xff11a985),
            size: 42,
          ),
          title: const Text('Face captured'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text(
                'Add a name for this local face profile.',
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 18),
              TextField(
                controller: input,
                autofocus: true,
                textCapitalization: TextCapitalization.words,
                decoration: InputDecoration(
                  labelText: 'Person name',
                  prefixIcon: const Icon(Icons.person_outline),
                  errorText: error,
                ),
                onSubmitted: (_) {
                  final value = input.text.trim();
                  if (value.isEmpty) {
                    setDialogState(() => error = 'Please enter a name');
                  } else {
                    Navigator.pop(context, value);
                  }
                },
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Retake'),
            ),
            FilledButton(
              onPressed: () {
                final value = input.text.trim();
                if (value.isEmpty) {
                  setDialogState(() => error = 'Please enter a name');
                } else {
                  Navigator.pop(context, value);
                }
              },
              child: const Text('Save profile'),
            ),
          ],
        ),
      ),
    );
    input.dispose();
    return result;
  }

  @override
  Widget build(BuildContext context) => _CameraShell(
    title: 'Register face',
    controller: controller,
    ready: cameraReady,
    guideColor: processing ? Colors.amber : const Color(0xff31e3c0),
    footer: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(
          message,
          textAlign: TextAlign.center,
          style: const TextStyle(
            color: Colors.white,
            fontSize: 16,
            fontWeight: FontWeight.w600,
          ),
        ),
        const SizedBox(height: 18),
        GestureDetector(
          onTap: processing ? null : _capture,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 180),
            width: 76,
            height: 76,
            padding: const EdgeInsets.all(6),
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              border: Border.all(color: Colors.white, width: 3),
              color: processing ? Colors.white24 : Colors.transparent,
            ),
            child: DecoratedBox(
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: processing ? Colors.amber : const Color(0xff19c5b6),
              ),
              child: processing
                  ? const Padding(
                      padding: EdgeInsets.all(18),
                      child: CircularProgressIndicator(
                        strokeWidth: 3,
                        color: Colors.white,
                      ),
                    )
                  : const Icon(Icons.camera_alt, color: Colors.white, size: 30),
            ),
          ),
        ),
        const SizedBox(height: 10),
        const Text(
          'Look straight · Good light · No glasses glare',
          style: TextStyle(color: Colors.white60, fontSize: 12),
        ),
      ],
    ),
  );

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}

class LiveVerificationPage extends StatefulWidget {
  const LiveVerificationPage({
    super.key,
    required this.sdk,
    required this.profile,
    required this.requireLiveness,
  });

  final MmlFaceSdk sdk;
  final EnrolledProfile profile;
  final bool requireLiveness;

  @override
  State<LiveVerificationPage> createState() => _LiveVerificationPageState();
}

class _LiveVerificationPageState extends State<LiveVerificationPage> {
  CameraController? controller;
  bool cameraReady = false;
  bool active = true;
  String message = 'Starting live camera…';
  Color statusColor = const Color(0xff31e3c0);
  double? similarity;
  double? liveness;
  double? distanceRatio;
  int livenessSamples = 0;
  bool processingFrame = false;

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

  Future<void> _initialize() async {
    try {
      controller = await _createFrontCamera(ResolutionPreset.medium);
      if (!mounted) return;
      setState(() {
        cameraReady = true;
        message = 'Place your face in front of the camera';
      });
      if (Platform.isAndroid) {
        await controller!.startImageStream((frame) {
          if (!active || processingFrame) {
            return;
          }
          processingFrame = true;
          unawaited(
            _processStreamFrame(
              frame,
            ).whenComplete(() => processingFrame = false),
          );
        });
      } else {
        unawaited(_scanLoop());
      }
    } catch (_) {
      if (mounted) setState(() => message = 'Camera permission is required');
    }
  }

  Future<void> _processStreamFrame(CameraImage frame) async {
    final camera = controller;
    if (camera == null || !mounted || !active) return;
    try {
      final result = widget.requireLiveness
          ? await widget.sdk.verifyFrame(
              nv21: _mergeNv21(frame),
              width: frame.width,
              height: frame.height,
              rotationDegrees: _rotationDegrees(camera),
              template: widget.profile.template,
            )
          : await widget.sdk.recognizeFrame(
              nv21: _mergeNv21(frame),
              width: frame.width,
              height: frame.height,
              rotationDegrees: _rotationDegrees(camera),
              template: widget.profile.template,
            );
      if (!mounted || !active) return;
      await _applyResult(result);
    } on FaceSdkException catch (error) {
      _showFrameError(
        _friendlyError(error.code),
        distanceRatio: error.code == FaceSdkError.faceTooSmall ? .15 : null,
      );
    } catch (_) {
      _showFrameError('Camera frame unavailable — retrying…');
    }
  }

  Future<void> _scanLoop() async {
    await Future<void>.delayed(const Duration(milliseconds: 450));
    while (mounted && active) {
      final camera = controller;
      if (camera == null || !camera.value.isInitialized) return;
      try {
        final photo = await camera.takePicture();
        final bytes = await photo.readAsBytes();
        final result = widget.requireLiveness
            ? await widget.sdk.verify(
                encodedImage: bytes,
                template: widget.profile.template,
              )
            : await widget.sdk.recognize(
                encodedImage: bytes,
                template: widget.profile.template,
              );
        if (!mounted || !active) return;
        if (await _applyResult(result)) return;
      } on FaceSdkException catch (error) {
        _showFrameError(
          _friendlyError(error.code),
          distanceRatio: error.code == FaceSdkError.faceTooSmall ? .15 : null,
        );
      } catch (_) {
        _showFrameError('Camera frame unavailable — retrying…');
      }
      await Future<void>.delayed(const Duration(milliseconds: 650));
    }
  }

  Future<bool> _applyResult(VerificationResult result) async {
    if (!mounted || !active) return true;
    similarity = result.similarity;
    liveness = result.livenessScore;
    if (!widget.requireLiveness) {
      if (result.matched) {
        await _showSuccess(result);
        return true;
      }
      final guidance = _distanceGuidance(
        result.quality.faceRatio,
        requiredRatio: .60,
      );
      setState(() {
        distanceRatio = guidance == null ? null : result.quality.faceRatio;
        message =
            guidance ?? 'Face not recognized — look directly at the camera';
        statusColor = const Color(0xffffb44b);
      });
    } else if (result.spoofLatched) {
      setState(() {
        distanceRatio = null;
        message = 'Spoof detected — present a live face';
        statusColor = const Color(0xffff5b6e);
        livenessSamples = 0;
      });
      await Future<void>.delayed(const Duration(milliseconds: 900));
      await widget.sdk.resetLiveness();
    } else if (result.livenessPassed == true) {
      if (result.matched) {
        await _showSuccess(result);
        return true;
      }
      setState(() {
        distanceRatio = null;
        message = 'Live face confirmed, but not recognized';
        statusColor = const Color(0xffffb44b);
        livenessSamples = 0;
      });
      await widget.sdk.resetLiveness();
    } else if (result.livenessScore == null) {
      final guidance = _distanceGuidance(
        result.quality.faceRatio,
        requiredRatio: .72,
      );
      setState(() {
        distanceRatio = guidance == null ? null : result.quality.faceRatio;
        message = guidance ?? 'Hold still — checking liveness';
        statusColor = const Color(0xffffb44b);
      });
    } else {
      livenessSamples = math.min(4, livenessSamples + 1);
      setState(() {
        distanceRatio = null;
        message = livenessSamples < 4
            ? 'Checking liveness · $livenessSamples of 4'
            : 'Liveness not confirmed — keep a live face steady';
        statusColor = livenessSamples < 4
            ? const Color(0xff31e3c0)
            : const Color(0xffffb44b);
      });
    }
    return false;
  }

  void _showFrameError(String value, {double? distanceRatio}) {
    if (!mounted || !active) return;
    setState(() {
      this.distanceRatio = distanceRatio;
      message = value;
      statusColor = const Color(0xffffb44b);
    });
  }

  Future<void> _showSuccess(VerificationResult result) async {
    active = false;
    if (!mounted) return;
    await Navigator.of(context).pushReplacement<void, void>(
      MaterialPageRoute(
        builder: (_) => VerificationSuccessPage(
          name: widget.profile.name,
          livenessChecked: widget.requireLiveness,
          similarity: result.similarity,
          livenessScore: result.livenessScore,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) => _CameraShell(
    title: widget.requireLiveness
        ? 'Liveness + recognition'
        : 'Face recognition',
    controller: controller,
    ready: cameraReady,
    guideColor: statusColor,
    showGuide: false,
    overlay: distanceRatio == null
        ? null
        : _DistancePrompt(
            message: message,
            progress: (distanceRatio! / (widget.requireLiveness ? .72 : .60))
                .clamp(0.0, 1.0),
          ),
    footer: Container(
      width: double.infinity,
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        color: const Color(0xdd091725),
        borderRadius: BorderRadius.circular(22),
        border: Border.all(color: statusColor.withValues(alpha: .55)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          AnimatedSwitcher(
            duration: const Duration(milliseconds: 280),
            transitionBuilder: (child, animation) => FadeTransition(
              opacity: animation,
              child: SlideTransition(
                position:
                    Tween<Offset>(
                      begin: const Offset(0, .22),
                      end: Offset.zero,
                    ).animate(
                      CurvedAnimation(parent: animation, curve: Curves.easeOut),
                    ),
                child: child,
              ),
            ),
            child: Text(
              message,
              key: ValueKey(message),
              textAlign: TextAlign.center,
              style: TextStyle(
                color: statusColor,
                fontSize: 16,
                fontWeight: FontWeight.w800,
              ),
            ),
          ),
          if (similarity != null || liveness != null) ...[
            const SizedBox(height: 14),
            Row(
              children: [
                if (similarity != null)
                  Expanded(
                    child: _Metric(
                      label: 'Similarity',
                      value: similarity!.toStringAsFixed(3),
                    ),
                  ),
                if (similarity != null && liveness != null)
                  const SizedBox(width: 10),
                if (liveness != null)
                  Expanded(
                    child: _Metric(
                      label: 'Liveness',
                      value: liveness!.toStringAsFixed(3),
                    ),
                  ),
              ],
            ),
          ],
        ],
      ),
    ),
  );

  @override
  void dispose() {
    active = false;
    controller?.dispose();
    if (widget.requireLiveness) unawaited(widget.sdk.resetLiveness());
    super.dispose();
  }
}

class VerificationSuccessPage extends StatelessWidget {
  const VerificationSuccessPage({
    super.key,
    required this.name,
    required this.livenessChecked,
    required this.similarity,
    required this.livenessScore,
  });

  final String name;
  final bool livenessChecked;
  final double? similarity;
  final double? livenessScore;

  @override
  Widget build(BuildContext context) => Scaffold(
    body: SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            const Spacer(),
            Container(
              width: 112,
              height: 112,
              decoration: const BoxDecoration(
                shape: BoxShape.circle,
                color: Color(0xffdff9f2),
              ),
              child: const Icon(
                Icons.verified_rounded,
                size: 64,
                color: Color(0xff0aaf82),
              ),
            ),
            const SizedBox(height: 26),
            Text(
              'Welcome, $name',
              textAlign: TextAlign.center,
              style: const TextStyle(
                fontSize: 30,
                fontWeight: FontWeight.w800,
                color: Color(0xff10283a),
              ),
            ),
            const SizedBox(height: 10),
            Text(
              livenessChecked
                  ? 'Live face verified and identity recognized.'
                  : 'Face recognized successfully.',
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 16, color: Color(0xff637487)),
            ),
            const SizedBox(height: 28),
            Container(
              padding: const EdgeInsets.all(18),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(22),
              ),
              child: Row(
                children: [
                  Expanded(
                    child: _ResultValue(
                      label: 'Similarity',
                      value: similarity?.toStringAsFixed(3) ?? '—',
                    ),
                  ),
                  if (livenessChecked) ...[
                    Container(
                      width: 1,
                      height: 42,
                      color: const Color(0xffe5ebf0),
                    ),
                    Expanded(
                      child: _ResultValue(
                        label: 'Liveness',
                        value: livenessScore?.toStringAsFixed(3) ?? '—',
                      ),
                    ),
                  ],
                ],
              ),
            ),
            const Spacer(),
            SizedBox(
              width: double.infinity,
              child: FilledButton.icon(
                style: FilledButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 17),
                ),
                onPressed: () => Navigator.of(context).pop(),
                icon: const Icon(Icons.home_outlined),
                label: const Text('Return to home'),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

class _CameraShell extends StatelessWidget {
  const _CameraShell({
    required this.title,
    required this.controller,
    required this.ready,
    required this.guideColor,
    required this.footer,
    this.showGuide = true,
    this.overlay,
  });

  final String title;
  final CameraController? controller;
  final bool ready;
  final Color guideColor;
  final Widget footer;
  final bool showGuide;
  final Widget? overlay;

  @override
  Widget build(BuildContext context) => Scaffold(
    backgroundColor: const Color(0xff050d15),
    body: Stack(
      fit: StackFit.expand,
      children: [
        if (ready && controller != null)
          _CoverCamera(controller: controller!)
        else
          const Center(
            child: CircularProgressIndicator(color: Color(0xff19c5b6)),
          ),
        if (ready && showGuide)
          IgnorePointer(
            child: CustomPaint(painter: _FaceGuidePainter(guideColor)),
          ),
        if (ready && overlay != null)
          Align(
            alignment: const Alignment(0, .12),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 28),
              child: overlay,
            ),
          ),
        SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(18),
            child: Column(
              children: [
                Row(
                  children: [
                    IconButton.filledTonal(
                      style: IconButton.styleFrom(
                        backgroundColor: const Color(0xaa07131f),
                        foregroundColor: Colors.white,
                      ),
                      onPressed: () => Navigator.of(context).pop(),
                      icon: const Icon(Icons.arrow_back),
                    ),
                    const SizedBox(width: 12),
                    Text(
                      title,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 19,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                  ],
                ),
                const Spacer(),
                footer,
              ],
            ),
          ),
        ),
      ],
    ),
  );
}

class _DistancePrompt extends StatefulWidget {
  const _DistancePrompt({required this.message, required this.progress});

  final String message;
  final double progress;

  @override
  State<_DistancePrompt> createState() => _DistancePromptState();
}

class _DistancePromptState extends State<_DistancePrompt> {
  bool pulse = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) setState(() => pulse = true);
    });
  }

  @override
  Widget build(BuildContext context) => Container(
    padding: const EdgeInsets.fromLTRB(20, 16, 20, 18),
    decoration: BoxDecoration(
      color: const Color(0xe60a1723),
      borderRadius: BorderRadius.circular(22),
      boxShadow: const [
        BoxShadow(color: Colors.black45, blurRadius: 24, offset: Offset(0, 8)),
      ],
    ),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        AnimatedScale(
          scale: pulse ? 1.18 : .88,
          duration: const Duration(milliseconds: 650),
          curve: Curves.easeInOut,
          onEnd: () {
            if (mounted) setState(() => pulse = !pulse);
          },
          child: const Icon(
            Icons.keyboard_double_arrow_up_rounded,
            color: Color(0xffffc34d),
            size: 46,
          ),
        ),
        const SizedBox(height: 4),
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 250),
          child: Text(
            widget.message,
            key: ValueKey(widget.message),
            textAlign: TextAlign.center,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 18,
              height: 1.2,
              fontWeight: FontWeight.w800,
            ),
          ),
        ),
        const SizedBox(height: 14),
        ClipRRect(
          borderRadius: BorderRadius.circular(20),
          child: TweenAnimationBuilder<double>(
            duration: const Duration(milliseconds: 320),
            curve: Curves.easeOut,
            tween: Tween<double>(begin: 0, end: widget.progress),
            builder: (context, value, _) => LinearProgressIndicator(
              minHeight: 9,
              value: value,
              backgroundColor: Colors.white24,
              valueColor: const AlwaysStoppedAnimation(Color(0xffffc34d)),
            ),
          ),
        ),
        const SizedBox(height: 7),
        const Text(
          'Move toward the phone camera',
          style: TextStyle(color: Colors.white70, fontSize: 12),
        ),
      ],
    ),
  );
}

class _CoverCamera extends StatelessWidget {
  const _CoverCamera({required this.controller});
  final CameraController controller;

  @override
  Widget build(BuildContext context) {
    final previewSize = controller.value.previewSize!;
    return SizedBox.expand(
      child: ClipRect(
        child: FittedBox(
          fit: BoxFit.cover,
          child: SizedBox(
            width: previewSize.height,
            height: previewSize.width,
            child: CameraPreview(controller),
          ),
        ),
      ),
    );
  }
}

class _FaceGuidePainter extends CustomPainter {
  const _FaceGuidePainter(this.color);
  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    final oval = Rect.fromCenter(
      center: Offset(size.width / 2, size.height * .43),
      width: size.width * .68,
      height: size.height * .46,
    );
    final mask = Path()
      ..fillType = PathFillType.evenOdd
      ..addRect(Offset.zero & size)
      ..addOval(oval);
    canvas.drawPath(mask, Paint()..color = const Color(0x66030b12));
    canvas.drawOval(
      oval,
      Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 3
        ..color = color,
    );
    final cornerPaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 6
      ..strokeCap = StrokeCap.round
      ..color = color;
    const length = 28.0;
    for (final point in [
      Offset(oval.left, oval.top),
      Offset(oval.right, oval.top),
      Offset(oval.left, oval.bottom),
      Offset(oval.right, oval.bottom),
    ]) {
      final left = point.dx == oval.left;
      final top = point.dy == oval.top;
      canvas.drawLine(
        point,
        point.translate(left ? length : -length, 0),
        cornerPaint,
      );
      canvas.drawLine(
        point,
        point.translate(0, top ? length : -length),
        cornerPaint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant _FaceGuidePainter oldDelegate) =>
      oldDelegate.color != color;
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context) => Container(
    padding: const EdgeInsets.all(20),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(24),
      border: Border.all(color: const Color(0xffe5ebf0)),
      boxShadow: const [
        BoxShadow(
          color: Color(0x0a10283a),
          blurRadius: 20,
          offset: Offset(0, 8),
        ),
      ],
    ),
    child: child,
  );
}

class _CardTitle extends StatelessWidget {
  const _CardTitle({
    required this.icon,
    required this.title,
    required this.subtitle,
  });
  final IconData icon;
  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) => Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Container(
        width: 44,
        height: 44,
        decoration: BoxDecoration(
          color: const Color(0xffe5faf6),
          borderRadius: BorderRadius.circular(14),
        ),
        child: Icon(icon, color: const Color(0xff0a9f7a)),
      ),
      const SizedBox(width: 13),
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: const TextStyle(
                fontSize: 17,
                fontWeight: FontWeight.w800,
                color: Color(0xff10283a),
              ),
            ),
            const SizedBox(height: 3),
            Text(subtitle, style: const TextStyle(color: Color(0xff637487))),
          ],
        ),
      ),
    ],
  );
}

class _ActionTile extends StatelessWidget {
  const _ActionTile({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.color,
    required this.enabled,
    required this.onTap,
  });
  final IconData icon;
  final String title;
  final String subtitle;
  final Color color;
  final bool enabled;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) => InkWell(
    onTap: enabled ? onTap : null,
    borderRadius: BorderRadius.circular(18),
    child: AnimatedOpacity(
      duration: const Duration(milliseconds: 180),
      opacity: enabled ? 1 : .42,
      child: Container(
        padding: const EdgeInsets.all(15),
        decoration: BoxDecoration(
          color: const Color(0xfff5f8fb),
          borderRadius: BorderRadius.circular(18),
        ),
        child: Row(
          children: [
            Container(
              width: 46,
              height: 46,
              decoration: BoxDecoration(
                color: color.withValues(alpha: .12),
                borderRadius: BorderRadius.circular(14),
              ),
              child: Icon(icon, color: color),
            ),
            const SizedBox(width: 13),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: const TextStyle(fontWeight: FontWeight.w800),
                  ),
                  const SizedBox(height: 2),
                  Text(
                    subtitle,
                    style: const TextStyle(
                      fontSize: 12,
                      color: Color(0xff637487),
                    ),
                  ),
                ],
              ),
            ),
            const Icon(Icons.chevron_right, color: Color(0xff8b9aaa)),
          ],
        ),
      ),
    ),
  );
}

class _Metric extends StatelessWidget {
  const _Metric({required this.label, required this.value});
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) => Container(
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
    decoration: BoxDecoration(
      color: Colors.white10,
      borderRadius: BorderRadius.circular(13),
    ),
    child: Column(
      children: [
        Text(
          label,
          style: const TextStyle(color: Colors.white60, fontSize: 11),
        ),
        const SizedBox(height: 2),
        Text(
          value,
          style: const TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.w800,
          ),
        ),
      ],
    ),
  );
}

class _ResultValue extends StatelessWidget {
  const _ResultValue({required this.label, required this.value});
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) => Column(
    children: [
      Text(label, style: const TextStyle(color: Color(0xff637487))),
      const SizedBox(height: 4),
      Text(
        value,
        style: const TextStyle(
          fontSize: 21,
          fontWeight: FontWeight.w800,
          color: Color(0xff10283a),
        ),
      ),
    ],
  );
}

Future<CameraController> _createFrontCamera(ResolutionPreset resolution) async {
  final cameras = await availableCameras();
  if (cameras.isEmpty) throw StateError('No camera available');
  final description = cameras.firstWhere(
    (camera) => camera.lensDirection == CameraLensDirection.front,
    orElse: () => cameras.first,
  );
  final controller = CameraController(
    description,
    resolution,
    enableAudio: false,
    imageFormatGroup: Platform.isAndroid
        ? ImageFormatGroup.nv21
        : ImageFormatGroup.bgra8888,
  );
  await controller.initialize();
  await controller.lockCaptureOrientation(DeviceOrientation.portraitUp);
  return controller;
}

Uint8List _mergeNv21(CameraImage image) {
  final length = image.planes.fold<int>(
    0,
    (total, plane) => total + plane.bytes.length,
  );
  final bytes = Uint8List(length);
  var offset = 0;
  for (final plane in image.planes) {
    bytes.setRange(offset, offset + plane.bytes.length, plane.bytes);
    offset += plane.bytes.length;
  }
  return bytes;
}

int _rotationDegrees(CameraController controller) {
  const orientations = <DeviceOrientation, int>{
    DeviceOrientation.portraitUp: 0,
    DeviceOrientation.landscapeLeft: 90,
    DeviceOrientation.portraitDown: 180,
    DeviceOrientation.landscapeRight: 270,
  };
  final deviceRotation = orientations[controller.value.deviceOrientation] ?? 0;
  final sensor = controller.description.sensorOrientation;
  return controller.description.lensDirection == CameraLensDirection.front
      ? (sensor + deviceRotation) % 360
      : (sensor - deviceRotation + 360) % 360;
}

String? _distanceGuidance(double faceRatio, {required double requiredRatio}) {
  if (faceRatio < .28) return 'Face detected — move much closer';
  if (faceRatio < .42) return 'Getting closer — keep moving toward camera';
  if (faceRatio < .58) return 'Good — move a little closer';
  if (faceRatio < requiredRatio) return 'Almost there — just a little closer';
  return null;
}

String _friendlyError(FaceSdkError error) => switch (error) {
  FaceSdkError.noFace => 'No face detected — look at the camera',
  FaceSdkError.multipleFaces => 'Only one face can be visible',
  FaceSdkError.faceTooSmall => 'Face detected — move much closer',
  FaceSdkError.faceNotFrontal => 'Look directly at the camera',
  FaceSdkError.landmarksMissing =>
    'Face details are unclear — improve lighting',
  FaceSdkError.poorQuality => 'Image quality is too low — improve lighting',
  FaceSdkError.spoofDetected => 'Spoof detected — present a live face',
  FaceSdkError.busy => 'Processing — hold still',
  FaceSdkError.licenseInvalid => 'SDK licence is not ready',
  _ => 'Face could not be processed — try again',
};