medora-provider/lib/screens/authentication/sign_up_screen.dart

349 lines
11 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:medora/data/services/data_service.dart';
import 'package:medora/data/services/navigation_service.dart';
import 'package:medora/widgets/primary_button.dart';
class SignUpScreen extends StatefulWidget {
// final String selectedUserType;
const SignUpScreen({
super.key,
// required this.selectedUserType,
});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
String _completePhoneNumber = '';
bool _isLoading = false;
bool _obscurePassword = true;
// Add focus nodes to handle keyboard navigation
final _emailFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
// Email validation patterns
final RegExp _emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
caseSensitive: false,
);
// Common email domain validation
final List<String> _commonEmailDomains = [
'gmail.com',
'yahoo.com',
'hotmail.com',
'outlook.com'
];
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email address is required';
}
value = value.trim();
// Basic format validation
if (!_emailRegex.hasMatch(value)) {
return 'Please enter a valid email address';
}
// Check email length
if (value.length > 254) {
return 'Email address is too long';
}
// Split email into local and domain parts
final parts = value.split('@');
if (parts.length != 2) {
return 'Invalid email format';
}
final localPart = parts[0];
final domainPart = parts[1].toLowerCase();
// Validate local part
if (localPart.isEmpty || localPart.length > 64) {
return 'Invalid email username';
}
// Check for common typos in popular domains
for (final domain in _commonEmailDomains) {
if (domainPart.length > 3 &&
domainPart != domain &&
_calculateLevenshteinDistance(domainPart, domain) <= 2) {
return 'Did you mean @$domain?';
}
}
// Additional domain validation
if (!domainPart.contains('.')) {
return 'Invalid email domain';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Password must contain at least one uppercase letter';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Password must contain at least one number';
}
if (!value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
return 'Password must contain at least one special character';
}
return null;
}
// Calculate Levenshtein distance for typo detection
int _calculateLevenshteinDistance(String a, String b) {
if (a.isEmpty) return b.length;
if (b.isEmpty) return a.length;
List<int> previousRow = List<int>.generate(b.length + 1, (i) => i);
List<int> currentRow = List<int>.filled(b.length + 1, 0);
for (int i = 0; i < a.length; i++) {
currentRow[0] = i + 1;
for (int j = 0; j < b.length; j++) {
int insertCost = previousRow[j + 1] + 1;
int deleteCost = currentRow[j] + 1;
int replaceCost = previousRow[j] + (a[i] != b[j] ? 1 : 0);
currentRow[j + 1] = [insertCost, deleteCost, replaceCost].reduce(min);
}
List<int> temp = previousRow;
previousRow = currentRow;
currentRow = temp;
}
return previousRow[b.length];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sign Up'),
elevation: 0,
),
body: Container(
decoration: const BoxDecoration(),
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Register as doctor',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
Column(
children: [
TextFormField(
controller: _emailController,
focusNode: _emailFocusNode,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.email_outlined,
color: Colors.blue),
errorMaxLines: 2,
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_passwordFocusNode.requestFocus();
},
validator: _validateEmail,
onChanged: (value) {
// Trigger validation on change if the field was previously invalid
if (_formKey.currentState?.validate() ?? false) {
setState(() {});
}
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
focusNode: _passwordFocusNode,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue),
),
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.blue),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.blue,
),
onPressed: () => setState(
() => _obscurePassword = !_obscurePassword),
),
errorMaxLines: 2,
),
obscureText: _obscurePassword,
validator: _validatePassword,
onChanged: (value) {
// Trigger validation on change if the field was previously invalid
if (_formKey.currentState?.validate() ?? false) {
setState(() {});
}
},
),
const SizedBox(height: 16),
IntlPhoneField(
decoration: InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue),
),
),
initialCountryCode: 'IN',
onChanged: (phone) {
_completePhoneNumber = phone.completeNumber;
},
validator: (phone) {
if (phone?.completeNumber.isEmpty ?? true) {
return 'Phone number is required';
}
return null;
},
dropdownTextStyle: const TextStyle(color: Colors.blue),
style: const TextStyle(color: Colors.blue),
),
],
),
const SizedBox(height: 24),
PrimaryButton(
onPressed: _isLoading ? null : _handleSignUp,
text: _isLoading ? 'Creating Account...' : 'Create Account',
icon: Icons.person_add,
),
],
),
),
),
),
);
}
Future<void> _handleSignUp() async {
if (_formKey.currentState == null || !_formKey.currentState!.validate()) {
_showErrorSnackBar('Please fix the errors in the form');
return;
}
setState(() {
_isLoading = true;
});
try {
final result = await DataService.createUserProfile(
email: _emailController.text.trim(),
password: _passwordController.text,
userType: 'doctor',
phoneNumber: _completePhoneNumber,
);
if (mounted) {
if (result['success']) {
await NavigationService.handleDoctorNavigation(context);
} else {
_showErrorSnackBar(result['message']);
}
}
} catch (e) {
if (mounted) {
_showErrorSnackBar('An unexpected error occurred. Please try again.');
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
}