m3mo d8164be49a Add user authentication to Flutter frontend
- Create auth feature with Clean Architecture (domain/data/presentation)
- Add login and register pages with form validation
- Implement secure token storage with flutter_secure_storage
- Create AuthenticatedClient for automatic Bearer token headers
- Add AuthViewModel for global auth state management
- Update router with auth guards (redirect to login if not authenticated)
- Add logout option to settings page
- Update TaskRemoteDataSource to use authenticated client
- Add auth-related localization strings (EN/DE)
2026-02-02 22:58:07 +01:00

245 lines
9.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../../../../l10n/app_localizations.dart';
import '../viewmodels/auth_viewmodel.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _register() async {
if (!_formKey.currentState!.validate()) return;
final viewModel = context.read<AuthViewModel>();
final l10n = AppLocalizations.of(context)!;
final success = await viewModel.register(
email: _emailController.text.trim(),
password: _passwordController.text,
name: _nameController.text.trim(),
);
if (mounted && success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.registerSuccess)),
);
context.go('/login');
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/login'),
),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.register,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
TextFormField(
controller: _nameController,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: l10n.name,
prefixIcon: const Icon(Icons.person_outlined),
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.nameRequired;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: l10n.email,
prefixIcon: const Icon(Icons.email_outlined),
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.emailRequired;
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return l10n.emailInvalid;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: l10n.password,
prefixIcon: const Icon(Icons.lock_outlined),
border: const OutlineInputBorder(),
suffixIcon: SizedBox(
width: 48,
height: 48,
child: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() => _obscurePassword = !_obscurePassword);
},
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.passwordRequired;
}
if (value.length < 8) {
return l10n.passwordTooShort;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _register(),
decoration: InputDecoration(
labelText: l10n.confirmPassword,
prefixIcon: const Icon(Icons.lock_outlined),
border: const OutlineInputBorder(),
suffixIcon: SizedBox(
width: 48,
height: 48,
child: IconButton(
icon: Icon(
_obscureConfirmPassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() =>
_obscureConfirmPassword = !_obscureConfirmPassword);
},
),
),
),
validator: (value) {
if (value != _passwordController.text) {
return l10n.passwordsDoNotMatch;
}
return null;
},
),
const SizedBox(height: 8),
Consumer<AuthViewModel>(
builder: (context, viewModel, _) {
if (viewModel.error != null) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
viewModel.error!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 16),
Consumer<AuthViewModel>(
builder: (context, viewModel, child) {
return SizedBox(
height: 56,
child: FilledButton(
onPressed: viewModel.isLoading ? null : _register,
child: viewModel.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(l10n.register),
),
);
},
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(l10n.alreadyHaveAccount),
TextButton(
onPressed: () => context.go('/login'),
child: Text(l10n.login),
),
],
),
],
),
),
),
),
),
),
);
}
}