tasq/lib/screens/auth/signup_screen.dart

276 lines
9.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../providers/auth_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../widgets/responsive_body.dart';
class SignUpScreen extends ConsumerStatefulWidget {
const SignUpScreen({super.key});
@override
ConsumerState<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends ConsumerState<SignUpScreen> {
final _formKey = GlobalKey<FormState>();
final _fullNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final Set<String> _selectedOfficeIds = {};
double _passwordStrength = 0.0;
String _passwordStrengthLabel = 'Very weak';
Color _passwordStrengthColor = Colors.red;
bool _isLoading = false;
@override
void initState() {
super.initState();
_passwordController.addListener(_updatePasswordStrength);
}
@override
void dispose() {
_passwordController.removeListener(_updatePasswordStrength);
_fullNameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _handleSignUp() async {
if (!_formKey.currentState!.validate()) return;
if (_selectedOfficeIds.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Select at least one office.')),
);
return;
}
setState(() => _isLoading = true);
final auth = ref.read(authControllerProvider);
try {
await auth.signUp(
email: _emailController.text.trim(),
password: _passwordController.text,
fullName: _fullNameController.text.trim(),
officeIds: _selectedOfficeIds.toList(),
);
if (mounted) {
context.go('/login');
}
} on Exception catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Sign up failed: $error')));
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
final officesAsync = ref.watch(officesOnceProvider);
return Scaffold(
appBar: AppBar(title: const Text('Create Account')),
body: ResponsiveBody(
maxWidth: 480,
padding: const EdgeInsets.symmetric(vertical: 24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Column(
children: [
Image.asset('assets/tasq_ico.png', height: 72, width: 72),
const SizedBox(height: 12),
Text(
'TasQ',
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
),
const SizedBox(height: 24),
TextFormField(
controller: _fullNameController,
decoration: const InputDecoration(labelText: 'Full name'),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Full name is required.';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required.';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required.';
}
if (value.length < 6) {
return 'Use at least 6 characters.';
}
return null;
},
),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Password strength: $_passwordStrengthLabel',
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(height: 6),
LinearProgressIndicator(
value: _passwordStrength,
minHeight: 8,
borderRadius: BorderRadius.circular(8),
color: _passwordStrengthColor,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
],
),
const SizedBox(height: 12),
TextFormField(
controller: _confirmPasswordController,
decoration: const InputDecoration(
labelText: 'Confirm password',
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) {
if (!_isLoading) {
_handleSignUp();
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Confirm your password.';
}
if (value != _passwordController.text) {
return 'Passwords do not match.';
}
return null;
},
),
const SizedBox(height: 16),
Text('Offices', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
officesAsync.when(
data: (offices) {
if (offices.isEmpty) {
return const Text('No offices available.');
}
return Column(
children: offices
.map(
(office) => CheckboxListTile(
value: _selectedOfficeIds.contains(office.id),
onChanged: _isLoading
? null
: (selected) {
setState(() {
if (selected == true) {
_selectedOfficeIds.add(office.id);
} else {
_selectedOfficeIds.remove(office.id);
}
});
},
title: Text(office.name),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
)
.toList(),
);
},
loading: () => const LinearProgressIndicator(),
error: (error, _) => Text('Failed to load offices: $error'),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _isLoading ? null : _handleSignUp,
child: _isLoading
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create Account'),
),
const SizedBox(height: 12),
TextButton(
onPressed: _isLoading ? null : () => context.go('/login'),
child: const Text('Back to sign in'),
),
],
),
),
),
);
}
void _updatePasswordStrength() {
final text = _passwordController.text;
var score = 0;
if (text.length >= 8) score++;
if (text.length >= 12) score++;
if (RegExp(r'[A-Z]').hasMatch(text)) score++;
if (RegExp(r'[a-z]').hasMatch(text)) score++;
if (RegExp(r'\d').hasMatch(text)) score++;
if (RegExp(r'[!@#$%^&*(),.?":{}|<>\[\]\\/+=;_-]').hasMatch(text)) {
score++;
}
final normalized = (score / 6).clamp(0.0, 1.0);
final (label, color) = switch (normalized) {
<= 0.2 => ('Very weak', Colors.red),
<= 0.4 => ('Weak', Colors.deepOrange),
<= 0.6 => ('Fair', Colors.orange),
<= 0.8 => ('Strong', Colors.green),
_ => ('Excellent', Colors.teal),
};
setState(() {
_passwordStrength = normalized;
_passwordStrengthLabel = label;
_passwordStrengthColor = color;
});
}
}