biometric_guard 1.0.2
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)),
),
),
],
),
),
),
);
}
}