biometric_guard 1.0.2 copy "biometric_guard: ^1.0.2" to clipboard
biometric_guard: ^1.0.2 copied to clipboard

Flutter plugin for Android biometric and device-credential authentication with session management, widget/navigation-level guards, and reactive UI state.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:biometric_guard/biometric_guard.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Biometric Guard',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6C63FF),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFF0F0F1A),
        cardColor: const Color(0xFF1C1C2E),
      ),
      home:  HomeScreen(),
    );
  }
}

// ─── Home Screen ─────────────────────────────────────────────────────────────

class HomeScreen extends StatelessWidget {
   HomeScreen({super.key});

  ValueNotifier<List<String>?> values = ValueNotifier(null);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: CustomScrollView(
          slivers: [
            _buildAppBar(),
            SliverPadding(
              padding: const EdgeInsets.fromLTRB(20, 0, 20, 32),
              sliver: SliverList(
                delegate: SliverChildListDelegate([
                  const SizedBox(height: 8),
                  _SessionStatusCard(),
                  const SizedBox(height: 24),
                  _SectionLabel('Authentication Methods'),
                  const SizedBox(height: 12),
                  _FeatureTile(
                    icon: Icons.fingerprint,
                    color: const Color(0xFF6C63FF),
                    title: 'Raw Auth — Secure Intent',
                    subtitle: 'Biometric + device credential fallback',
                    onTap: () => _rawAuth(context, AuthIntent.secure, biometricOnly: false),
                  ),
                  _FeatureTile(
                    icon: Icons.payment,
                    color: const Color(0xFF00BFA5),
                    title: 'Raw Auth — Payment Intent',
                    subtitle: 'Biometric only, no fallback',
                    onTap: () => _rawAuth(context, AuthIntent.payment, biometricOnly: true),
                  ),
                  _FeatureTile(
                    icon: Icons.manage_accounts,
                    color: const Color(0xFFFF6B6B),
                    title: 'Raw Auth — Credentials',
                    subtitle: 'Resume on foreground enabled',
                    onTap: () => _rawAuth(context, AuthIntent.credentials, biometricOnly: false),
                  ),
                  const SizedBox(height: 24),
                  _SectionLabel('Protected Widget'),
                  const SizedBox(height: 12),
                  _FeatureTile(
                    icon: Icons.security,
                    color: const Color(0xFFFFB300),
                    title: 'BiometricGuard Widget',
                    subtitle: 'Guards content with 30s session timeout',
                    onTap: () => _openGuardedWidget(context),
                  ),
                  const SizedBox(height: 12),

                  _SectionLabel('Protected Navigation'),
                  const SizedBox(height: 12),
                  _FeatureTile(
                    icon: Icons.route,
                    color: const Color(0xFF29B6F6),
                    title: 'BiometricRoute',
                    subtitle: 'Authenticates before the route is pushed',
                    onTap: () => _openGuardedRoute(context),
                  ),
                  const SizedBox(height: 12),
                  _SectionLabel('Protected Navigation'),
                  const SizedBox(height: 12),
                  ValueListenableBuilder(
                    valueListenable: values,
                    builder: (context, value, child) {
                      return _FeatureTile(
                        icon: Icons.phone,
                        color: const Color(0xFF29B6F6),
                        title: 'Available Biometric Types',
                        subtitle: value != null ?
                            value.isEmpty ?
                        "None" : value.map((e) => e,).toString() : "Tap to know Available Types",
                        onTap: () async {
                            values.value = await BiometricSessionManager.instance.getAvailableTypes();
                           
                        },
                      );
                    }
                  ),

                  const SizedBox(height: 24),
                  _SectionLabel('Session Control'),
                  const SizedBox(height: 12),
                  _SessionControlPanel(),
                ]),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAppBar() {
    return SliverAppBar(
      expandedHeight: 140,
      pinned: true,
      backgroundColor: const Color(0xFF0F0F1A),
      flexibleSpace: FlexibleSpaceBar(
        titlePadding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
        title: Row(
          children: [
            Container(
              padding: const EdgeInsets.all(6),
              decoration: BoxDecoration(
                color: const Color(0xFF6C63FF).withOpacity(0.2),
                borderRadius: BorderRadius.circular(10),
              ),
              child: const Icon(Icons.shield_rounded, color: Color(0xFF6C63FF), size: 20),
            ),
            const SizedBox(width: 10),
            const Text('Biometric Guard', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
          ],
        ),
      ),
      actions: [
        Padding(
          padding: const EdgeInsets.only(right: 16, top: 8),
          child: SessionStateBuilder(
            sessionTimeout: const Duration(minutes: 5),
            builder: (context, state) => _SessionIcon(state: state),
          ),
        ),
      ],
    );
  }

  Future<void> _rawAuth(BuildContext context, AuthIntent intent, {required bool biometricOnly}) async {
    final result = await BiometricSessionManager.instance.authenticate(
      options: AuthOptions(resumeOnForeground: true, biometricOnly: biometricOnly),
      intent: intent,
    );
    if (context.mounted) _showResultSnackbar(context, result);
  }

  void _openGuardedWidget(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => BiometricGuard(
          options: AuthOptions(resumeOnForeground: true),
          intent: AuthIntent.secure,
          sessionTimeout: const Duration(seconds: 30),
          onSuccess: () => debugPrint('Guard: Success'),
          onFailure: () => debugPrint('Guard: Failure'),
          lockedBuilder: (context, result) => _LockedScreen(
            message: result.message,
            onRetry: () => BiometricSessionManager.instance.invalidateSession(),
            onCancel: () => Navigator.pop(context),
          ),
          child: const ProtectedContentScreen(title: 'Guarded Widget'),
        ),
      ),
    );
  }

  Future<void> _openGuardedRoute(BuildContext context) async {
    await Navigator.push<AuthResult>(
      context,
      BiometricRoute(
        intent: AuthIntent.payment,
        builder: (_) => const ProtectedContentScreen(title: 'Guarded Route'),
        onAuthFailure: (result) {
          if (context.mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('Route blocked: ${result.message}'),
                backgroundColor: Colors.redAccent,
                behavior: SnackBarBehavior.floating,
              ),
            );
          }
        },
      ),
    );
  }

  void _showResultSnackbar(BuildContext context, AuthResult result) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      backgroundColor: result.isSuccess ? const Color(0xFF00BFA5) : Colors.redAccent,
      behavior: SnackBarBehavior.floating,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      content: Row(children: [
        Icon(result.isSuccess ? Icons.check_circle : Icons.cancel, color: Colors.white, size: 18),
        const SizedBox(width: 10),
        Expanded(
          child: Text(
            result.isSuccess ? 'Authenticated via ${result.type}' : result.message,
            style: const TextStyle(color: Colors.white),
          ),
        ),
      ]),
    ));
  }
}

// ─── Session Status Card ──────────────────────────────────────────────────────

class _SessionStatusCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SessionStateBuilder(
      sessionTimeout: const Duration(minutes: 5),
      builder: (context, state) {
        final (label, sublabel, color, icon) = switch (state) {
          SessionActive() => ('Session Active', 'You are authenticated', const Color(0xFF00BFA5), Icons.verified_user),
          SessionExpired() => ('Session Expired', 'Re-authentication required', Colors.redAccent, Icons.lock_rounded),
          SessionAuthenticating() => ('Authenticating…', 'Please complete biometric prompt', const Color(0xFFFFB300), Icons.fingerprint),
          SessionFailed() => ('Auth Failed', 'Authentication was unsuccessful', Colors.orange, Icons.error_rounded),
        };

        return Container(
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: color.withOpacity(0.08),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: color.withOpacity(0.3)),
          ),
          child: Row(
            children: [
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(color: color.withOpacity(0.15), shape: BoxShape.circle),
                child: state is SessionAuthenticating
                    ? SizedBox(
                  width: 22,
                  height: 22,
                  child: CircularProgressIndicator(strokeWidth: 2.5, color: color),
                )
                    : Icon(icon, color: color, size: 22),
              ),
              const SizedBox(width: 16),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(label, style: TextStyle(color: color, fontWeight: FontWeight.w700, fontSize: 15)),
                  const SizedBox(height: 2),
                  Text(sublabel, style: TextStyle(color: color.withOpacity(0.7), fontSize: 12)),
                ],
              ),
              const Spacer(),
              Container(
                width: 8,
                height: 8,
                decoration: BoxDecoration(
                  color: state is SessionAuthenticating ? Colors.transparent : color,
                  shape: BoxShape.circle,
                  boxShadow: [BoxShadow(color: color.withOpacity(0.5), blurRadius: 6)],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

// ─── Session Control Panel ────────────────────────────────────────────────────

class _SessionControlPanel extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: _ActionButton(
            label: 'Invalidate Session',
            icon: Icons.logout,
            color: Colors.redAccent,
            onTap: () {
              BiometricSessionManager.instance.invalidateSession();
              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                content: Text('Session invalidated'),
                behavior: SnackBarBehavior.floating,
              ));
            },
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: _ActionButton(
            label: 'Check Availability',
            icon: Icons.devices,
            color: const Color(0xFF6C63FF),
            onTap: () async {
              final available = await BiometricSessionManager.instance.isDeviceSupported();
              if (context.mounted) {
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(available ? 'Biometrics (or) Device Authentication available ✓' : 'Biometrics (or) Device Authentication not available'),
                  behavior: SnackBarBehavior.floating,
                ));
              }
            },
          ),
        ),
      ],
    );
  }
}

// ─── Shared Small Widgets ─────────────────────────────────────────────────────

class _SectionLabel extends StatelessWidget {
  final String text;
  const _SectionLabel(this.text);

  @override
  Widget build(BuildContext context) => Text(
    text,
    style: TextStyle(
      color: Colors.white.withOpacity(0.45),
      fontSize: 11,
      fontWeight: FontWeight.w600,
      letterSpacing: 1.2,
    ),
  );
}

class _SessionIcon extends StatelessWidget {
  final SessionState state;
  const _SessionIcon({required this.state});

  @override
  Widget build(BuildContext context) => switch (state) {
    SessionActive() => const Icon(Icons.lock_open_rounded, color: Color(0xFF00BFA5)),
    SessionExpired() => const Icon(Icons.lock_rounded, color: Colors.redAccent),
    SessionAuthenticating() => const SizedBox(
      width: 20,
      height: 20,
      child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFFFFB300)),
    ),
    SessionFailed() => const Icon(Icons.error_rounded, color: Colors.orange),
  };
}

class _FeatureTile extends StatelessWidget {
  final IconData icon;
  final Color color;
  final String title;
  final String subtitle;
  final VoidCallback onTap;

  const _FeatureTile({
    required this.icon,
    required this.color,
    required this.title,
    required this.subtitle,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Material(
        color: const Color(0xFF1C1C2E),
        borderRadius: BorderRadius.circular(16),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(16),
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
            child: Row(
              children: [
                Container(
                  padding: const EdgeInsets.all(10),
                  decoration: BoxDecoration(
                    color: color.withOpacity(0.12),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Icon(icon, color: color, size: 22),
                ),
                const SizedBox(width: 14),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
                      const SizedBox(height: 2),
                      Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.white.withOpacity(0.45))),
                    ],
                  ),
                ),
                Icon(Icons.chevron_right_rounded, color: Colors.white.withOpacity(0.25)),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ActionButton extends StatelessWidget {
  final String label;
  final IconData icon;
  final Color color;
  final VoidCallback onTap;

  const _ActionButton({required this.label, required this.icon, required this.color, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return Material(
      color: color.withOpacity(0.1),
      borderRadius: BorderRadius.circular(14),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(14),
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
          child: Column(
            children: [
              Icon(icon, color: color, size: 22),
              const SizedBox(height: 6),
              Text(label, textAlign: TextAlign.center,
                  style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w600)),
            ],
          ),
        ),
      ),
    );
  }
}

// ─── Locked Screen (used by BiometricGuard's lockedBuilder) ──────────────────

class _LockedScreen extends StatelessWidget {
  final String message;
  final VoidCallback onRetry;
  final VoidCallback onCancel;

  const _LockedScreen({required this.message, required this.onRetry, required this.onCancel});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(40),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                padding: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  color: Colors.redAccent.withOpacity(0.1),
                  shape: BoxShape.circle,
                ),
                child: const Icon(Icons.lock_person_rounded, size: 52, color: Colors.redAccent),
              ),
              const SizedBox(height: 24),
              const Text('Access Denied', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
              const SizedBox(height: 8),
              Text(message, textAlign: TextAlign.center, style: TextStyle(color: Colors.white.withOpacity(0.55))),
              const SizedBox(height: 32),
              SizedBox(
                width: double.infinity,
                child: FilledButton.icon(
                  onPressed: onRetry,
                  icon: const Icon(Icons.fingerprint),
                  label: const Text('Try Again'),
                  style: FilledButton.styleFrom(
                    backgroundColor: const Color(0xFF6C63FF),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
                  ),
                ),
              ),
              const SizedBox(height: 10),
              TextButton(onPressed: onCancel, child: const Text('Go Back')),
            ],
          ),
        ),
      ),
    );
  }
}

// ─── Protected Content Screen ─────────────────────────────────────────────────

class ProtectedContentScreen extends StatelessWidget {
  final String title;
  const ProtectedContentScreen({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        backgroundColor: const Color(0xFF0F0F1A),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                padding: const EdgeInsets.all(24),
                decoration: BoxDecoration(
                  color: const Color(0xFF00BFA5).withOpacity(0.1),
                  shape: BoxShape.circle,
                ),
                child: const Icon(Icons.verified_user_rounded, size: 64, color: Color(0xFF00BFA5)),
              ),
              const SizedBox(height: 24),
              const Text('Access Granted', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              const SizedBox(height: 10),
              Text(
                'This content is protected by biometric_guard.\nAuthentication was successful.',
                textAlign: TextAlign.center,
                style: TextStyle(color: Colors.white.withOpacity(0.5), height: 1.5),
              ),
              const SizedBox(height: 32),
              OutlinedButton.icon(
                onPressed: () => Navigator.pop(context),
                icon: const Icon(Icons.arrow_back_rounded),
                label: const Text('Go Back'),
                style: OutlinedButton.styleFrom(
                  foregroundColor: const Color(0xFF00BFA5),
                  side: const BorderSide(color: Color(0xFF00BFA5)),
                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
2
likes
0
points
146
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for Android biometric and device-credential authentication with session management, widget/navigation-level guards, and reactive UI state.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on biometric_guard

Packages that implement biometric_guard