local_biometrics_manager 0.1.2
local_biometrics_manager: ^0.1.2 copied to clipboard
Biometric authentication for Flutter on Android and iOS: fingerprint, face and iris detection, device-credential fallback, custom prompts and a live status stream.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:local_biometrics_manager/local_biometrics_manager.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Biometrics Demo',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _biometrics = LocalBiometricsManager.instance;
BiometricStatus? _status;
String _lastResult = 'No attempt yet';
StreamSubscription<BiometricStatus>? _statusSub;
@override
void initState() {
super.initState();
_refreshStatus();
// Keep the on-screen status in sync with the device (enrolment, lockout…).
_statusSub = _biometrics.onBiometricStatusChanged.listen((status) {
if (mounted) setState(() => _status = status);
});
}
@override
void dispose() {
_statusSub?.cancel();
super.dispose();
}
Future<void> _refreshStatus() async {
final status = await _biometrics.getBiometricStatus();
if (mounted) setState(() => _status = status);
}
Future<void> _authenticate({bool allowFallback = false}) async {
final result = await _biometrics.authenticate(
BiometricRequest(
title: 'Verify your identity',
subtitle: 'Biometrics demo',
description: 'Use your fingerprint or face to continue.',
allowDeviceFallback: allowFallback,
timeoutSeconds: 30,
uiConfig: const BiometricUIConfig(
negativeButtonText: 'Not now',
primaryColorArgb: 0xFF3F51B5,
),
),
);
if (!mounted) return;
setState(() {
_lastResult = result.isSuccess
? 'Success via ${result.usedType?.name ?? 'biometric'}'
: 'Failed [${result.errorCode}] ${result.errorMessage ?? ''}';
});
}
@override
Widget build(BuildContext context) {
final status = _status;
return Scaffold(
appBar: AppBar(title: const Text('local_biometrics_manager')),
body: RefreshIndicator(
onRefresh: _refreshStatus,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_StatusCard(status: status),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Last result',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(_lastResult),
],
),
),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: (status?.isAvailable ?? false)
? () => _authenticate()
: null,
icon: const Icon(Icons.fingerprint),
label: const Text('Authenticate'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: () => _authenticate(allowFallback: true),
icon: const Icon(Icons.password),
label: const Text('Authenticate (allow PIN fallback)'),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: () => _biometrics.cancelAuthentication(),
icon: const Icon(Icons.close),
label: const Text('Cancel ongoing'),
),
],
),
),
);
}
}
/// Small widget that renders the current [BiometricStatus] in a readable card.
class _StatusCard extends StatelessWidget {
const _StatusCard({required this.status});
final BiometricStatus? status;
@override
Widget build(BuildContext context) {
if (status == null) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Reading biometric status…'),
),
);
}
final s = status!;
final types = s.availableTypes.map((t) => t.name).join(', ');
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Device status',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_row('Available', s.isAvailable ? 'Yes' : 'No'),
_row('Enrolled', s.isEnrolled ? 'Yes' : 'No'),
_row('Types', types.isEmpty ? '—' : types),
if (s.lockoutStatus != null)
_row('Locked out', s.lockoutStatus!.isLockedOut ? 'Yes' : 'No'),
if (s.hardwareInfo?.deviceModel != null)
_row('Device', s.hardwareInfo!.deviceModel!),
],
),
),
);
}
Widget _row(String label, String value) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(width: 110, child: Text(label)),
Expanded(
child: Text(value, style: const TextStyle(color: Colors.black87)),
),
],
),
);
}