diff --git a/lib/BlindMasterResources/text_inputs.dart b/lib/BlindMasterResources/text_inputs.dart index 4e07c34..1ed3320 100644 --- a/lib/BlindMasterResources/text_inputs.dart +++ b/lib/BlindMasterResources/text_inputs.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; -class BlindMasterMainInput extends StatelessWidget { +class BlindMasterMainInput extends StatefulWidget { const BlindMasterMainInput(this.label, {super.key, this.controller, this.validator, this.color, this.password = false}); final String label; @@ -12,26 +12,45 @@ class BlindMasterMainInput extends StatelessWidget { final String? Function(String?)? validator; + @override + State createState() => _BlindMasterMainInputState(); +} + +class _BlindMasterMainInputState extends State { + bool _obscureText = true; + @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(10), child:TextFormField( - validator: validator, - obscureText: password, + validator: widget.validator, + obscureText: widget.password && _obscureText, enableSuggestions: false, autocorrect: false, - controller: controller, + controller: widget.controller, style: TextStyle( - color: color + color: widget.color ), decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(10)), ), - labelText: label, - labelStyle: TextStyle(color: color), + labelText: widget.label, + labelStyle: TextStyle(color: widget.color), contentPadding: EdgeInsets.all(10), + suffixIcon: widget.password + ? IconButton( + icon: Icon( + _obscureText ? Icons.visibility : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ) + : null, ), ) ); diff --git a/lib/BlindMasterScreens/Startup/create_user_screen.dart b/lib/BlindMasterScreens/Startup/create_user_screen.dart index 8ad8c2a..71ef59c 100644 --- a/lib/BlindMasterScreens/Startup/create_user_screen.dart +++ b/lib/BlindMasterScreens/Startup/create_user_screen.dart @@ -21,6 +21,7 @@ class _CreateUserScreenState extends State { final emailController = TextEditingController(); final nameController = TextEditingController(); final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); final _passFormKey = GlobalKey(); final _emailFormKey = GlobalKey(); @@ -87,13 +88,24 @@ class _CreateUserScreenState extends State { } - String? confirmPasswordValidator(String? input) { - if (input == passwordController.text) { - return null; + String? passwordValidator(String? input) { + if (input == null || input.isEmpty) { + return 'Password is required'; } - else { + if (input.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + } + + String? confirmPasswordValidator(String? input) { + if (input == null || input.isEmpty) { + return 'Please confirm your password'; + } + if (input != passwordController.text) { return "Passwords do not match!"; } + return null; } String? emailValidator(String? input) { @@ -135,18 +147,20 @@ class _CreateUserScreenState extends State { ), Form( key: _passFormKey, - autovalidateMode: AutovalidateMode.onUserInteraction, + autovalidateMode: AutovalidateMode.onUnfocus, child: Column( children: [ BlindMasterMainInput( "Password", password: true, - controller: passwordController + controller: passwordController, + validator: passwordValidator, ), BlindMasterMainInput( "Confirm Password", validator: confirmPasswordValidator, password: true, + controller: confirmPasswordController, ) ], ) diff --git a/lib/BlindMasterScreens/Startup/forgot_password_screen.dart b/lib/BlindMasterScreens/Startup/forgot_password_screen.dart new file mode 100644 index 0000000..74a21e9 --- /dev/null +++ b/lib/BlindMasterScreens/Startup/forgot_password_screen.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'dart:convert'; +import '../../BlindMasterResources/secure_transmissions.dart'; +import 'verify_reset_code_screen.dart'; + +class ForgotPasswordScreen extends StatefulWidget { + const ForgotPasswordScreen({super.key}); + + @override + State createState() => _ForgotPasswordScreenState(); +} + +class _ForgotPasswordScreenState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + String? _emailValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+'); + if (!emailRegex.hasMatch(value)) { + return 'Please enter a valid email'; + } + return null; + } + + Future _handleSendCode() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final response = await regularPost( + { + 'email': _emailController.text.trim(), + }, + '/forgot-password', + ); + + if (!mounted) return; + + if (response.statusCode == 200) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => VerifyResetCodeScreen( + email: _emailController.text.trim(), + ), + ), + ); + } else if (response.statusCode == 429) { + final body = json.decode(response.body) as Map; + final retryAfter = body['retryAfter'] ?? 'some time'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please wait $retryAfter seconds before requesting another code.'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 5), + ), + ); + } else { + final body = json.decode(response.body) as Map; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(body['error'] ?? 'Failed to send reset code'), + backgroundColor: Colors.red, + ), + ); + } + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $error'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Forgot Password'), + backgroundColor: Theme.of(context).primaryColorLight, + ), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.lock_reset, + size: 80, + color: Theme.of(context).primaryColorLight, + ), + const SizedBox(height: 32), + const Text( + 'Reset Your Password', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Text( + 'Enter your email address and we\'ll send you a 6-character code to reset your password.', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + TextFormField( + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email), + ), + keyboardType: TextInputType.emailAddress, + validator: _emailValidator, + enabled: !_isLoading, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _isLoading ? null : _handleSendCode, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColorLight, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Send Reset Code', + style: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/BlindMasterScreens/Startup/login_screen.dart b/lib/BlindMasterScreens/Startup/login_screen.dart index 22b9a43..19fa2e4 100644 --- a/lib/BlindMasterScreens/Startup/login_screen.dart +++ b/lib/BlindMasterScreens/Startup/login_screen.dart @@ -1,5 +1,6 @@ import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterScreens/Startup/create_user_screen.dart'; +import 'package:blind_master/BlindMasterScreens/Startup/forgot_password_screen.dart'; import 'package:blind_master/BlindMasterScreens/home_screen.dart'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:blind_master/BlindMasterResources/fade_transition.dart'; @@ -107,6 +108,13 @@ class _LoginScreenState extends State { ); } + void switchToForgotPassword() { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ForgotPasswordScreen()), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -121,7 +129,7 @@ class _LoginScreenState extends State { child: Column( children: [ BlindMasterMainInput("Email", controller: emailController), - BlindMasterMainInput("Password", controller: passwordController, password: true,), + BlindMasterMainInput("Password", controller: passwordController, password: true), ], ), ), @@ -140,6 +148,13 @@ class _LoginScreenState extends State { "Create Account" ), ), + const SizedBox(height: 8), + TextButton( + onPressed: switchToForgotPassword, + child: Text( + "Forgot Password?" + ), + ), ], ), ), diff --git a/lib/BlindMasterScreens/Startup/reset_password_form_screen.dart b/lib/BlindMasterScreens/Startup/reset_password_form_screen.dart new file mode 100644 index 0000000..7d1eb79 --- /dev/null +++ b/lib/BlindMasterScreens/Startup/reset_password_form_screen.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'dart:convert'; +import '../../BlindMasterResources/secure_transmissions.dart'; +import '../../BlindMasterResources/text_inputs.dart'; +import 'login_screen.dart'; + +class ResetPasswordFormScreen extends StatefulWidget { + final String email; + final String code; + + const ResetPasswordFormScreen({ + super.key, + required this.email, + required this.code, + }); + + @override + State createState() => _ResetPasswordFormScreenState(); +} + +class _ResetPasswordFormScreenState extends State { + final _formKey = GlobalKey(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + String? _passwordValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + } + + String? _confirmPasswordValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Please confirm your password'; + } + if (value != _passwordController.text) { + return 'Passwords do not match'; + } + return null; + } + + Future _handleResetPassword() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final response = await regularPost( + { + 'email': widget.email, + 'code': widget.code, + 'newPassword': _passwordController.text, + }, + '/reset-password', + ); + + if (!mounted) return; + + if (response.statusCode == 200) { + // Navigate back to login screen and remove all previous routes + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => const LoginScreen()), + (route) => false, + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password reset successfully! Please log in with your new password.'), + backgroundColor: Colors.green, + duration: Duration(seconds: 4), + ), + ); + } else if (response.statusCode == 429) { + final body = json.decode(response.body) as Map; + final retryAfter = body['retryAfter'] ?? 'some time'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Too many attempts. Please try again in $retryAfter minutes.'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 5), + ), + ); + } else { + final body = json.decode(response.body) as Map; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(body['error'] ?? 'Failed to reset password'), + backgroundColor: Colors.red, + ), + ); + } + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $error'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Reset Password'), + backgroundColor: Theme.of(context).primaryColorLight, + ), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUnfocus, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.lock_open, + size: 80, + color: Theme.of(context).primaryColorLight, + ), + const SizedBox(height: 32), + const Text( + 'Create New Password', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Text( + 'Enter your new password below.', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + BlindMasterMainInput( + 'New Password', + controller: _passwordController, + password: true, + validator: _passwordValidator, + ), + BlindMasterMainInput( + 'Confirm New Password', + controller: _confirmPasswordController, + password: true, + validator: _confirmPasswordValidator, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _isLoading ? null : _handleResetPassword, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColorLight, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Reset Password', + style: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart b/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart new file mode 100644 index 0000000..9288d2d --- /dev/null +++ b/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:convert'; +import '../../BlindMasterResources/secure_transmissions.dart'; +import 'reset_password_form_screen.dart'; + +class VerifyResetCodeScreen extends StatefulWidget { + final String email; + + const VerifyResetCodeScreen({ + super.key, + required this.email, + }); + + @override + State createState() => _VerifyResetCodeScreenState(); +} + +class _VerifyResetCodeScreenState extends State { + final _formKey = GlobalKey(); + final _codeController = TextEditingController(); + bool _isLoading = false; + bool _isResending = false; + + @override + void dispose() { + _codeController.dispose(); + super.dispose(); + } + + String? _codeValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Please enter the reset code'; + } + if (value.length != 6) { + return 'Code must be 6 characters'; + } + return null; + } + + Future _handleVerifyCode() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final response = await regularPost( + { + 'email': widget.email, + 'code': _codeController.text.trim().toUpperCase(), + }, + '/verify-reset-code', + ); + + if (!mounted) return; + + if (response.statusCode == 200) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ResetPasswordFormScreen( + email: widget.email, + code: _codeController.text.trim().toUpperCase(), + ), + ), + ); + } else if (response.statusCode == 429) { + final body = json.decode(response.body) as Map; + final retryAfter = body['retryAfter'] ?? 'some time'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Too many attempts. Please try again in $retryAfter minutes.'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 5), + ), + ); + } else if (response.statusCode == 401) { + final body = json.decode(response.body) as Map; + final remainingAttempts = body['remainingAttempts'] ?? 0; + String message = body['error'] ?? 'Invalid or expired code'; + if (remainingAttempts > 0) { + message += '\n$remainingAttempts attempts remaining before timeout.'; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.orange[700], + duration: const Duration(seconds: 4), + ), + ); + } else { + final body = json.decode(response.body) as Map; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(body['error'] ?? 'Invalid or expired code'), + backgroundColor: Colors.red, + ), + ); + } + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $error'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _handleResendCode() async { + setState(() { + _isResending = true; + }); + + try { + final response = await regularPost( + { + 'email': widget.email, + }, + '/forgot-password', + ); + + if (!mounted) return; + + if (response.statusCode == 200) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('A new code has been sent to your email'), + backgroundColor: Theme.of(context).primaryColorLight, + ), + ); + _codeController.clear(); + } else if (response.statusCode == 429) { + final body = json.decode(response.body) as Map; + final retryAfter = body['retryAfter'] ?? 'some time'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please wait $retryAfter seconds before requesting another code.'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 5), + ), + ); + } else { + final body = json.decode(response.body) as Map; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(body['error'] ?? 'Failed to resend code'), + backgroundColor: Colors.red, + ), + ); + } + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $error'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() { + _isResending = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Verify Code'), + backgroundColor: Theme.of(context).primaryColorLight, + ), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.mark_email_read, + size: 80, + color: Theme.of(context).primaryColorLight, + ), + const SizedBox(height: 32), + const Text( + 'Check Your Email', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + 'We\'ve sent a 6-character code to ${widget.email}. Enter it below to continue.', + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + TextFormField( + controller: _codeController, + decoration: const InputDecoration( + labelText: 'Reset Code', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.security), + hintText: 'ABC123', + ), + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.characters, + inputFormatters: [ + LengthLimitingTextInputFormatter(6), + FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')), + ], + validator: _codeValidator, + enabled: !_isLoading && !_isResending, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + letterSpacing: 8, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _isLoading || _isResending ? null : _handleVerifyCode, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColorLight, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Verify Code', + style: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 16), + TextButton( + onPressed: _isLoading || _isResending ? null : _handleResendCode, + child: _isResending + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColorLight), + ), + ) + : Text( + 'Didn\'t receive the code? Resend', + style: TextStyle( + color: Theme.of(context).primaryColorLight, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/BlindMasterScreens/account_screen.dart b/lib/BlindMasterScreens/account_screen.dart index 8d8e06a..ca2634f 100644 --- a/lib/BlindMasterScreens/account_screen.dart +++ b/lib/BlindMasterScreens/account_screen.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterScreens/change_password_screen.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -132,18 +133,18 @@ class _AccountScreenState extends State { elevation: 2, child: Column( children: [ - // Placeholder for future options - Padding( - padding: const EdgeInsets.all(20.0), - child: Center( - child: Text( - 'Additional options will appear here', - style: TextStyle( - color: Colors.grey[600], - fontStyle: FontStyle.italic, + ListTile( + leading: Icon(Icons.lock_outline), + title: Text('Change Password'), + trailing: Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChangePasswordScreen(), ), - ), - ), + ); + }, ), ], ), diff --git a/lib/BlindMasterScreens/addingDevices/device_setup.dart b/lib/BlindMasterScreens/addingDevices/device_setup.dart index 5e418d4..773b8f7 100644 --- a/lib/BlindMasterScreens/addingDevices/device_setup.dart +++ b/lib/BlindMasterScreens/addingDevices/device_setup.dart @@ -58,6 +58,7 @@ class _DeviceSetupState extends State { final passControl = TextEditingController(); final unameControl = TextEditingController(); + bool _obscureWifiPassword = true; @override void initState() { super.initState(); @@ -201,72 +202,92 @@ class _DeviceSetupState extends State { Future authenticate(Map network) async { bool ent = isEnterprise(network); bool open = isOpen(network); + + // Reset password visibility state for new dialog + _obscureWifiPassword = true; + Map creds = await showDialog( context: context, builder: (dialogContext) { - return AlertDialog( - title: Text( - network["ssid"], - style: GoogleFonts.aBeeZee(), - ), - content: Form( - autovalidateMode: AutovalidateMode.onUnfocus, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (ent) - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextFormField( - controller: unameControl, - decoration: const InputDecoration(hintText: "Enter your enterprise login"), - textInputAction: TextInputAction.next, // Shows "Next" on keyboard - onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), // Moves to password - validator: (value) => (value == null || value.isEmpty) ? "Empty username!" : null, + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text( + network["ssid"], + style: GoogleFonts.aBeeZee(), + ), + content: Form( + autovalidateMode: AutovalidateMode.onUnfocus, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (ent) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: unameControl, + decoration: const InputDecoration(hintText: "Enter your enterprise login"), + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), + validator: (value) => (value == null || value.isEmpty) ? "Empty username!" : null, + ), + const SizedBox(height: 16), + ] ), - const SizedBox(height: 16), - ] - ), - if (!open) - TextFormField( - controller: passControl, - obscureText: true, - decoration: const InputDecoration(hintText: "Enter password"), - validator: (value) => (value == null || value.length < 8) ? "Not long enough!" : null, - textInputAction: TextInputAction.send, - onFieldSubmitted: (value) { - if (Form.of(context).validate()) { - Navigator.pop(dialogContext, (ent ? - {"uname": unameControl.text, "password": passControl.text} - : (open ? {} : {"password": passControl.text}))); - } - }, - ), - ] - ) - - ), - actions: [ - TextButton( - onPressed: () { - unameControl.clear(); - passControl.clear(); - Navigator.pop(dialogContext); - }, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () { - Navigator.pop(dialogContext, (ent ? - {"uname": unameControl.text, "password": passControl.text} - : (open ? {} : {"password": passControl.text}))); - passControl.clear(); - unameControl.clear(); - }, - child: const Text("Connect"), - ), - ], + if (!open) + TextFormField( + controller: passControl, + obscureText: _obscureWifiPassword, + decoration: InputDecoration( + hintText: "Enter password", + suffixIcon: IconButton( + icon: Icon( + _obscureWifiPassword ? Icons.visibility : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscureWifiPassword = !_obscureWifiPassword; + }); + }, + ), + ), + validator: (value) => (value == null || value.length < 8) ? "Not long enough!" : null, + textInputAction: TextInputAction.send, + onFieldSubmitted: (value) { + if (Form.of(context).validate()) { + Navigator.pop(dialogContext, (ent ? + {"uname": unameControl.text, "password": passControl.text} + : (open ? {} : {"password": passControl.text}))); + } + }, + ), + ] + ) + + ), + actions: [ + TextButton( + onPressed: () { + unameControl.clear(); + passControl.clear(); + Navigator.pop(dialogContext); + }, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.pop(dialogContext, (ent ? + {"uname": unameControl.text, "password": passControl.text} + : (open ? {} : {"password": passControl.text}))); + passControl.clear(); + unameControl.clear(); + }, + child: const Text("Connect"), + ), + ], + ); + } ); } ); diff --git a/lib/BlindMasterScreens/change_password_screen.dart b/lib/BlindMasterScreens/change_password_screen.dart new file mode 100644 index 0000000..4e10fd0 --- /dev/null +++ b/lib/BlindMasterScreens/change_password_screen.dart @@ -0,0 +1,195 @@ +import 'dart:convert'; +import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterResources/text_inputs.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ChangePasswordScreen extends StatefulWidget { + const ChangePasswordScreen({super.key}); + + @override + State createState() => _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends State { + final oldPasswordController = TextEditingController(); + final newPasswordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + + final _formKey = GlobalKey(); + + @override + void dispose() { + oldPasswordController.dispose(); + newPasswordController.dispose(); + confirmPasswordController.dispose(); + super.dispose(); + } + + String? newPasswordValidator(String? input) { + if (input == null || input.isEmpty) { + return 'New password is required'; + } + if (input.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + } + + String? confirmPasswordValidator(String? input) { + if (input == null || input.isEmpty) { + return 'Please confirm your password'; + } + if (input != newPasswordController.text) { + return 'Passwords do not match'; + } + return null; + } + + String? oldPasswordValidator(String? input) { + if (input == null || input.isEmpty) { + return 'Current password is required'; + } + return null; + } + + Future handleChangePassword() async { + if (!_formKey.currentState!.validate()) { + return; + } + + try { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Center( + child: CircularProgressIndicator( + color: Theme.of(context).primaryColorLight, + ), + ); + }, + ); + + final payload = { + 'oldPassword': oldPasswordController.text, + 'newPassword': newPasswordController.text, + }; + + final response = await securePost(payload, 'change_password'); + + // Remove loading indicator + if (mounted) Navigator.of(context).pop(); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + if (!mounted) return; + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.green[700], + content: Text('Password changed successfully'), + duration: Duration(seconds: 2), + ), + ); + + // Navigate back to account screen + Navigator.pop(context); + } else if (response.statusCode == 401) { + throw Exception('Current password is incorrect'); + } else if (response.statusCode == 400) { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Invalid request'); + } else { + throw Exception('Failed to change password'); + } + } catch (e) { + // Remove loading indicator if still showing + if (mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).primaryColorLight, + foregroundColor: Colors.white, + title: Text( + 'Change Password', + style: GoogleFonts.aBeeZee(), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUnfocus, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 20), + Text( + 'Enter your current password and choose a new password', + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 30), + BlindMasterMainInput( + 'Current Password', + controller: oldPasswordController, + password: true, + validator: oldPasswordValidator, + ), + SizedBox(height: 20), + Divider(), + SizedBox(height: 20), + BlindMasterMainInput( + 'New Password', + controller: newPasswordController, + password: true, + validator: newPasswordValidator, + ), + SizedBox(height: 20), + BlindMasterMainInput( + 'Confirm New Password', + controller: confirmPasswordController, + password: true, + validator: confirmPasswordValidator, + ), + SizedBox(height: 30), + ElevatedButton( + onPressed: handleChangePassword, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColorLight, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 15), + ), + child: Text( + 'Change Password', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + ), + ), + ); + } +}