flutter_auth_screen 0.1.4
flutter_auth_screen: ^0.1.4 copied to clipboard
Customizable PIN authentication plugin with biometric support, lockout protection, and pure UI components.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_auth_screen/flutter_auth_screen.dart';
import 'pin_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Auth Screen Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
brightness: Brightness.light,
),
darkTheme: ThemeData(
primarySwatch: Colors.purple,
useMaterial3: true,
brightness: Brightness.dark,
),
themeMode: ThemeMode.system,
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _isPinSet = false;
String _statusMessage = '';
final _pinService = PinService();
bool _enableBiometric = false;
@override
void initState() {
super.initState();
_checkPinStatus();
}
Future<void> _checkPinStatus() async {
try {
final isPinSet = await _pinService.isPinSet();
if (mounted) {
setState(() {
_isPinSet = isPinSet;
_statusMessage = isPinSet ? 'PIN is set ✓' : 'No PIN set';
});
}
} catch (e) {
if (mounted) {
setState(() {
_isPinSet = false;
_statusMessage = 'Error checking PIN status: $e';
});
}
}
}
void _showSetPinScreen() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SetPinScreen(
onPinSet: (pin) async {
return await _pinService.savePin(pin);
},
onCompleted: (success) {
if (success) {
setState(() {
_statusMessage = 'PIN set successfully!';
});
_checkPinStatus();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('PIN set successfully!'),
backgroundColor: Colors.green,
),
);
} else {
setState(() {
_statusMessage = 'Failed to set PIN';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to set PIN. Please try again.'),
backgroundColor: Colors.red,
),
);
}
},
onCancel: () {
Navigator.of(context).pop();
},
primaryColor: Colors.blue,
createTitle: 'Secure Your App',
createSubtitle: 'Create a 4-digit PIN',
confirmTitle: 'Confirm Your PIN',
confirmSubtitle: 'Enter your PIN again',
mismatchMessage: 'PINs don\'t match! Please try again.',
config: const PinScreenConfig(
pinLength: 4,
enableHaptic: true,
showShakeAnimation: true,
enableVoiceOver: true,
),
),
),
);
}
void _showChangePinScreen() async {
final isPinSet = await _pinService.isPinSet();
if (!isPinSet) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please set a PIN first'),
backgroundColor: Colors.orange,
),
);
return;
}
if (!mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangePinScreen(
onVerifyOldPin: (pin) async {
return await _pinService.verifyPin(pin);
},
onPinChanged: (newPin) async {
return await _pinService.savePin(newPin);
},
onCompleted: (success) {
if (success) {
setState(() {
_statusMessage = 'PIN changed successfully!';
});
_checkPinStatus();
} else {
setState(() {
_statusMessage = 'Failed to change PIN';
});
}
},
validateNewPin: (pin) {
// Optional: Add validation logic here
return null;
},
onCancel: () {
Navigator.of(context).pop();
},
primaryColor: Colors.blue,
oldPinTitle: 'Enter Current PIN',
oldPinSubtitle: 'Verify your current PIN',
newPinTitle: 'Create New PIN',
newPinSubtitle: 'Enter your new 4-digit PIN',
confirmPinTitle: 'Confirm New PIN',
confirmPinSubtitle: 'Re-enter your new PIN',
incorrectOldPinMessage: 'Incorrect PIN.',
mismatchMessage: 'PINs don\'t match! Please try again.',
maxAttempts: 3,
maxAttemptsMessage: 'Too many failed attempts. Please try again later.',
config: const PinScreenConfig(
pinLength: 4,
enableHaptic: true,
showShakeAnimation: true,
enableVoiceOver: true,
),
),
),
);
}
void _showVerifyPinScreen() async {
final isPinSet = await _pinService.isPinSet();
if (!isPinSet) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please set a PIN first'),
backgroundColor: Colors.orange,
),
);
return;
}
// Check if locked out
if (await _pinService.isLockedOut()) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Account is locked. Please try again later.'),
backgroundColor: Colors.red,
duration: Duration(seconds: 3),
),
);
return;
}
if (!mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VerifyPinScreen(
onVerifyPin: (pin) async {
return await _pinService.verifyPin(pin);
},
onVerificationResult: (success, attemptsRemaining) {
if (success) {
setState(() {
_statusMessage = 'PIN verified successfully!';
});
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 12),
Text('Access Granted!', style: TextStyle(fontSize: 16)),
],
),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} else if (attemptsRemaining == 0) {
// Lockout user
_pinService.setLockout();
setState(() {
_statusMessage = 'Account locked for 5 minutes!';
});
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Maximum attempts reached. Account locked for 5 minutes.'),
backgroundColor: Colors.red,
duration: Duration(seconds: 5),
),
);
}
}
},
// Biometric callback (passed directly to screen, not config)
onBiometric: _enableBiometric ? () async {
final result = await _showBiometricDialog();
if (result && context.mounted) {
// If biometric succeeds, close screen
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Authenticated with biometric!'),
backgroundColor: Colors.green,
),
);
}
} : null,
canCancel: true,
maxAttempts: 5,
primaryColor: Colors.blue,
title: 'Verify Identity',
subtitle: 'Enter your PIN to access secured content',
incorrectPinMessage: (remaining) =>
'Wrong PIN! $remaining ${remaining == 1 ? 'attempt' : 'attempts'} left.',
maxAttemptsMessage: 'Account locked. Try again later.',
config: PinScreenConfig(
showBiometric: _enableBiometric,
biometricIcon: Icons.fingerprint,
pinLength: 4,
enableHaptic: true,
showShakeAnimation: true,
enableVoiceOver: true,
pinDotSemanticLabel: 'PIN digit',
deleteButtonSemanticLabel: 'Delete last digit',
biometricButtonSemanticLabel: 'Use fingerprint',
),
),
),
);
}
Future<bool> _showBiometricDialog() async {
// Simulate biometric authentication
// In real app, use local_auth package
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Biometric Authentication'),
content: const Text('In a real app, this would use fingerprint/face ID.\n\nSimulating authentication...'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Authenticate'),
),
],
),
) ?? false;
}
void _removeSecurity() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Remove Security'),
content: const Text('Are you sure you want to remove the PIN?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Remove'),
),
],
),
);
if (confirmed == true) {
final success = await _pinService.removePin();
setState(() {
_statusMessage = success ? 'PIN removed successfully' : 'Failed to remove PIN';
});
_checkPinStatus();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? 'PIN removed successfully' : 'Failed to remove PIN'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Auth Screen Demo'),
centerTitle: true,
elevation: 2,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status Card
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Icon(
_isPinSet ? Icons.lock : Icons.lock_open,
size: 64,
color: _isPinSet ? Colors.green : Colors.grey,
),
const SizedBox(height: 16),
Text(
_statusMessage,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
const SizedBox(height: 24),
// Feature Configuration
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'🎨 Feature Configuration',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Biometric Auth'),
subtitle: const Text('Show fingerprint button (simulated)'),
value: _enableBiometric,
onChanged: (value) {
setState(() {
_enableBiometric = value;
});
},
),
],
),
),
),
const SizedBox(height: 24),
// Action Buttons
if (!_isPinSet)
ElevatedButton.icon(
onPressed: _showSetPinScreen,
icon: const Icon(Icons.pin),
label: const Text('Set PIN'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
if (_isPinSet) ...[
ElevatedButton.icon(
onPressed: _showChangePinScreen,
icon: const Icon(Icons.sync_lock),
label: const Text('Change PIN'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
],
if (!_isPinSet)
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: _isPinSet ? _showVerifyPinScreen : null,
icon: const Icon(Icons.verified_user),
label: const Text('Verify PIN'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _isPinSet ? _removeSecurity : null,
icon: const Icon(Icons.delete_outline),
label: const Text('Remove Security'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 24),
// Feature List
Card(
color: Colors.blue.shade50,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'✨ Features Demonstrated:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 12),
Text('✅ Set & Create new PIN'),
Text('✅ Change existing PIN (3-step flow)'),
Text('✅ Verify PIN with lockout protection'),
Text('✅ Biometric authentication (simulated)'),
Text('✅ Lockout protection (5 attempts, 5 min)'),
Text('✅ Haptic feedback & animations'),
Text('✅ Accessibility support (VoiceOver)'),
Text('✅ Custom messages & colors'),
],
),
),
),
],
),
),
);
}
}