inou/app/lib/features/auth/login_page.dart

310 lines
9.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
import 'package:inou_app/design/widgets/widgets.dart';
/// Login page with biometric support
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
// Biometric state
bool _biometricAvailable = false;
bool _biometricEnrolled = false;
@override
void initState() {
super.initState();
_checkBiometricAvailability();
}
Future<void> _checkBiometricAvailability() async {
// TODO: Check actual biometric availability using local_auth
// For now, simulate availability on mobile
setState(() {
_biometricAvailable = true; // Placeholder
_biometricEnrolled = false; // Placeholder - check if user has enrolled
});
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return InouAuthFlowPage(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Welcome back',
style: InouText.sectionTitle.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Sign in to access your health dossier',
style: InouText.body.copyWith(
color: InouTheme.textMuted,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Biometric login button (if available and enrolled)
if (_biometricAvailable && _biometricEnrolled) ...[
_buildBiometricButton(),
const SizedBox(height: 24),
_buildDivider('or sign in with email'),
const SizedBox(height: 24),
],
// Email field
InouTextField(
label: 'Email',
controller: _emailController,
placeholder: 'you@example.com',
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
// Password field
InouTextField(
label: 'Password',
controller: _passwordController,
placeholder: '••••••••',
obscureText: _obscurePassword,
autofillHints: const [AutofillHints.password],
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: InouTheme.textMuted,
size: 20,
),
onPressed: () {
setState(() => _obscurePassword = !_obscurePassword);
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 16),
// Remember me & forgot password row
Row(
children: [
InouCheckbox(
value: _rememberMe,
label: 'Remember me',
onChanged: (value) {
setState(() => _rememberMe = value ?? false);
},
),
const Spacer(),
TextButton(
onPressed: _handleForgotPassword,
child: Text(
'Forgot password?',
style: InouText.bodySmall.copyWith(
color: InouTheme.accent,
),
),
),
],
),
const SizedBox(height: 24),
// Login button
InouButton(
text: _isLoading ? 'Signing in...' : 'Sign in',
onPressed: _isLoading ? null : _handleLogin,
),
const SizedBox(height: 24),
// Sign up link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Don\'t have an account? ',
style: InouText.body.copyWith(
color: InouTheme.textMuted,
),
),
TextButton(
onPressed: () => Navigator.pushReplacementNamed(context, '/signup'),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'Sign up',
style: InouText.body.copyWith(
color: InouTheme.accent,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
);
}
Widget _buildBiometricButton() {
return OutlinedButton.icon(
onPressed: _handleBiometricLogin,
icon: const Icon(Icons.fingerprint, size: 24),
label: const Text('Sign in with biometrics'),
style: OutlinedButton.styleFrom(
foregroundColor: InouTheme.text,
side: BorderSide(color: InouTheme.border),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusMd,
),
),
);
}
Widget _buildDivider(String text) {
return Row(
children: [
Expanded(child: Divider(color: InouTheme.border)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
text,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
),
),
),
Expanded(child: Divider(color: InouTheme.border)),
],
);
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
// TODO: Implement actual login
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
// After successful login, offer biometric enrollment if available
if (_biometricAvailable && !_biometricEnrolled) {
await _offerBiometricEnrollment();
}
// Navigate to dashboard
Navigator.pushNamedAndRemoveUntil(
context,
'/dashboard',
(route) => false,
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Login failed: ${e.toString()}'),
backgroundColor: InouTheme.danger,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _handleBiometricLogin() async {
// TODO: Implement biometric authentication using local_auth
// On success, retrieve stored credentials and login
}
Future<void> _handleForgotPassword() async {
// Navigate to forgot password flow
Navigator.pushNamed(context, '/forgot-password');
}
Future<void> _offerBiometricEnrollment() async {
final shouldEnroll = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusLg,
),
title: const Text('Enable biometric login?'),
content: const Text(
'Sign in faster next time using your fingerprint or face.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(
'Not now',
style: TextStyle(color: InouTheme.textMuted),
),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: InouTheme.accent,
),
child: const Text('Enable'),
),
],
),
);
if (shouldEnroll == true) {
// TODO: Enroll biometric credentials
// Store encrypted credentials securely
}
}
}