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 createState() => _SignUpScreenState(); } class _SignUpScreenState extends State { final GlobalKey _formKey = GlobalKey(); 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 _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 previousRow = List.generate(b.length + 1, (i) => i); List currentRow = List.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 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 ${widget.selectedUserType}', 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 _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: widget.selectedUserType, phoneNumber: _completePhoneNumber, ); if (mounted) { if (result['success']) { if (widget.selectedUserType.toLowerCase() == 'doctor') { await NavigationService.handleDoctorNavigation(context); } else { await NavigationService.handlePatientNavigation(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(); } }