From 4e7e8661e5948b292e4ad37baa14b77afa508d2f Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Thu, 8 Jan 2026 21:32:12 -0600 Subject: [PATCH] password check before email change, blindMasterMainInput use for all text inputs --- lib/BlindMasterResources/text_inputs.dart | 101 ++++++- .../Startup/forgot_password_screen.dart | 10 +- .../Startup/verify_reset_code_screen.dart | 17 +- .../accountManagement/account_screen.dart | 141 ++-------- .../change_email_screen.dart | 258 ++++++++++++++++++ .../addingDevices/device_setup.dart | 29 +- .../groupControl/create_group_dialog.dart | 18 +- .../individualControl/device_screen.dart | 7 +- 8 files changed, 386 insertions(+), 195 deletions(-) create mode 100644 lib/BlindMasterScreens/accountManagement/change_email_screen.dart diff --git a/lib/BlindMasterResources/text_inputs.dart b/lib/BlindMasterResources/text_inputs.dart index 1ed3320..0c4bab6 100644 --- a/lib/BlindMasterResources/text_inputs.dart +++ b/lib/BlindMasterResources/text_inputs.dart @@ -1,44 +1,102 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; - - -class BlindMasterMainInput extends StatefulWidget { - const BlindMasterMainInput(this.label, {super.key, this.controller, this.validator, this.color, this.password = false}); +/// A comprehensive, flexible text input widget that maintains consistent styling +/// across the entire app while supporting all common text input use cases. +class BlindMasterInput extends StatefulWidget { + const BlindMasterInput( + this.label, { + super.key, + this.controller, + this.validator, + this.color, + this.password = false, + this.enabled = true, + this.keyboardType, + this.prefixIcon, + this.hintText, + this.textCapitalization = TextCapitalization.none, + this.inputFormatters, + this.textAlign = TextAlign.start, + this.maxLength, + this.textInputAction, + this.onFieldSubmitted, + this.onChanged, + this.focusedBorderColor, + this.initialValue, + this.autofocus = false, + }); final String label; final TextEditingController? controller; final Color? color; final bool password; + final bool enabled; + final TextInputType? keyboardType; + final IconData? prefixIcon; + final String? hintText; + final TextCapitalization textCapitalization; + final List? inputFormatters; + final TextAlign textAlign; + final int? maxLength; + final TextInputAction? textInputAction; + final Function(String)? onFieldSubmitted; + final Function(String)? onChanged; + final Color? focusedBorderColor; + final String? initialValue; + final bool autofocus; final String? Function(String?)? validator; @override - State createState() => _BlindMasterMainInputState(); + State createState() => _BlindMasterInputState(); } -class _BlindMasterMainInputState extends State { +class _BlindMasterInputState extends State { bool _obscureText = true; @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.all(10), - child:TextFormField( + padding: const EdgeInsets.all(10), + child: TextFormField( + initialValue: widget.initialValue, + controller: widget.controller, validator: widget.validator, obscureText: widget.password && _obscureText, - enableSuggestions: false, - autocorrect: false, - controller: widget.controller, + enableSuggestions: !widget.password, + autocorrect: !widget.password, + enabled: widget.enabled, + keyboardType: widget.keyboardType, + textCapitalization: widget.textCapitalization, + inputFormatters: widget.inputFormatters, + textAlign: widget.textAlign, + maxLength: widget.maxLength, + textInputAction: widget.textInputAction, + onFieldSubmitted: widget.onFieldSubmitted, + onChanged: widget.onChanged, + autofocus: widget.autofocus, style: TextStyle( - color: widget.color + color: widget.color, ), decoration: InputDecoration( border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), + borderRadius: const BorderRadius.all(Radius.circular(10)), ), + focusedBorder: widget.focusedBorderColor != null + ? OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide( + color: widget.focusedBorderColor!, + width: 2, + ), + ) + : null, labelText: widget.label, + hintText: widget.hintText, labelStyle: TextStyle(color: widget.color), - contentPadding: EdgeInsets.all(10), + contentPadding: const EdgeInsets.all(10), + prefixIcon: widget.prefixIcon != null ? Icon(widget.prefixIcon) : null, suffixIcon: widget.password ? IconButton( icon: Icon( @@ -51,8 +109,21 @@ class _BlindMasterMainInputState extends State { }, ) : null, + counterText: widget.maxLength != null ? '' : null, // Hide character counter ), - ) + ), ); } +} + +// Legacy alias for backward compatibility +class BlindMasterMainInput extends BlindMasterInput { + const BlindMasterMainInput( + super.label, { + super.key, + super.controller, + super.validator, + super.color, + super.password, + }); } \ No newline at end of file diff --git a/lib/BlindMasterScreens/Startup/forgot_password_screen.dart b/lib/BlindMasterScreens/Startup/forgot_password_screen.dart index 1f84e53..5cdb965 100644 --- a/lib/BlindMasterScreens/Startup/forgot_password_screen.dart +++ b/lib/BlindMasterScreens/Startup/forgot_password_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'dart:convert'; import '../../BlindMasterResources/secure_transmissions.dart'; +import '../../BlindMasterResources/text_inputs.dart'; import 'verify_reset_code_screen.dart'; class ForgotPasswordScreen extends StatefulWidget { @@ -139,13 +140,10 @@ class _ForgotPasswordScreenState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 32), - TextFormField( + BlindMasterInput( + 'Email', controller: _emailController, - decoration: const InputDecoration( - labelText: 'Email', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.email), - ), + prefixIcon: Icons.email, keyboardType: TextInputType.emailAddress, validator: _emailValidator, enabled: !_isLoading, diff --git a/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart b/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart index 05f5256..7da1286 100644 --- a/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart +++ b/lib/BlindMasterScreens/Startup/verify_reset_code_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:convert'; import '../../BlindMasterResources/secure_transmissions.dart'; +import '../../BlindMasterResources/text_inputs.dart'; import 'reset_password_form_screen.dart'; class VerifyResetCodeScreen extends StatefulWidget { @@ -220,14 +221,11 @@ class _VerifyResetCodeScreenState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 32), - TextFormField( + BlindMasterInput( + 'Reset Code', controller: _codeController, - decoration: const InputDecoration( - labelText: 'Reset Code', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.security), - hintText: 'ABC123', - ), + prefixIcon: Icons.security, + hintText: 'ABC123', keyboardType: TextInputType.text, textCapitalization: TextCapitalization.characters, inputFormatters: [ @@ -237,11 +235,6 @@ class _VerifyResetCodeScreenState extends State { validator: _codeValidator, enabled: !_isLoading && !_isResending, textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 24, - letterSpacing: 8, - fontWeight: FontWeight.bold, - ), ), const SizedBox(height: 24), ElevatedButton( diff --git a/lib/BlindMasterScreens/accountManagement/account_screen.dart b/lib/BlindMasterScreens/accountManagement/account_screen.dart index ae8905f..a02c7cc 100644 --- a/lib/BlindMasterScreens/accountManagement/account_screen.dart +++ b/lib/BlindMasterScreens/accountManagement/account_screen.dart @@ -2,7 +2,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/accountManagement/change_password_screen.dart'; -import 'package:blind_master/BlindMasterScreens/accountManagement/verify_email_change_screen.dart'; +import 'package:blind_master/BlindMasterScreens/accountManagement/change_email_screen.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -157,130 +157,27 @@ class _AccountScreenState extends State { } Future _handleChangeEmail() async { - final TextEditingController emailController = TextEditingController(); - final formKey = GlobalKey(); - final primaryColor = Theme.of(context).primaryColorLight; + if (email == null || email == 'N/A') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.red[700], + content: Text('Unable to load current email'), + ), + ); + return; + } - final result = await showDialog( - context: context, - builder: (BuildContext dialogContext) { - return AlertDialog( - title: Text('Change Email'), - content: Form( - key: formKey, - child: TextFormField( - controller: emailController, - decoration: InputDecoration( - labelText: 'New Email Address', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.email), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter an email address'; - } - final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); - if (!emailRegex.hasMatch(value)) { - return 'Please enter a valid email'; - } - if (value == email) { - return 'This is your current email'; - } - return null; - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(false), - child: Text('Cancel'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - ), - onPressed: () async { - if (formKey.currentState!.validate()) { - Navigator.of(dialogContext).pop(true); - } - }, - child: Text('Send Verification'), - ), - ], - ); - }, + // Navigate to change email screen + final success = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChangeEmailScreen(currentEmail: email!), + ), ); - // Allow dialog to fully close before disposing controller - await Future.delayed(Duration(milliseconds: 100)); - - if (result == true && mounted) { - final newEmail = emailController.text.trim(); - - try { - // Show loading indicator - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext loadingContext) { - return Center( - child: CircularProgressIndicator( - color: primaryColor, - ), - ); - }, - ); - - final localHour = DateTime.now().hour; - final response = await securePost( - { - 'newEmail': newEmail, - 'localHour': localHour, - }, - 'request-email-change', - ); - - // 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; - - // Navigate to waiting screen - final success = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailChangeWaitingScreen(newEmail: newEmail), - ), - ); - - // If email was changed successfully, refresh account info - if (success == true && mounted) { - await fetchAccountInfo(); - } - } else { - final body = json.decode(response.body); - throw Exception(body['error'] ?? 'Failed to send verification email'); - } - } 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)); - } finally { - emailController.dispose(); - } - } else { - emailController.dispose(); + // If email was changed successfully, refresh account info + if (success == true && mounted) { + await fetchAccountInfo(); } } diff --git a/lib/BlindMasterScreens/accountManagement/change_email_screen.dart b/lib/BlindMasterScreens/accountManagement/change_email_screen.dart new file mode 100644 index 0000000..04d9989 --- /dev/null +++ b/lib/BlindMasterScreens/accountManagement/change_email_screen.dart @@ -0,0 +1,258 @@ +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:blind_master/BlindMasterScreens/accountManagement/verify_email_change_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ChangeEmailScreen extends StatefulWidget { + final String currentEmail; + + const ChangeEmailScreen({super.key, required this.currentEmail}); + + @override + State createState() => _ChangeEmailScreenState(); +} + +class _ChangeEmailScreenState extends State { + final passwordController = TextEditingController(); + final newEmailController = TextEditingController(); + + final _formKey = GlobalKey(); + + @override + void dispose() { + passwordController.dispose(); + newEmailController.dispose(); + super.dispose(); + } + + String? passwordValidator(String? input) { + if (input == null || input.isEmpty) { + return 'Password is required'; + } + return null; + } + + String? emailValidator(String? input) { + if (input == null || input.isEmpty) { + return 'Email address is required'; + } + final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); + if (!emailRegex.hasMatch(input)) { + return 'Please enter a valid email'; + } + if (input == widget.currentEmail) { + return 'This is your current email'; + } + return null; + } + + Future handleChangeEmail() 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 localHour = DateTime.now().hour; + final payload = { + 'password': passwordController.text, + 'newEmail': newEmailController.text, + 'localHour': localHour, + }; + + final response = await securePost(payload, 'request-email-change'); + + // 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; + + // Navigate to waiting screen + final success = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmailChangeWaitingScreen(newEmail: newEmailController.text), + ), + ); + + // If email was changed successfully, return true to refresh account info + if (success == true && mounted) { + Navigator.pop(context, true); + } + } else if (response.statusCode == 401) { + throw Exception('Password is incorrect'); + } else if (response.statusCode == 409) { + throw Exception('Email already in use'); + } else if (response.statusCode == 400) { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Invalid request'); + } else { + throw Exception('Failed to send verification email'); + } + } 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 Email', + 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), + // Current email display + Card( + elevation: 2, + color: Colors.blue[50], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue[700]), + SizedBox(width: 10), + Text( + 'Current Email', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue[700], + ), + ), + ], + ), + SizedBox(height: 8), + Text( + widget.currentEmail, + style: TextStyle( + fontSize: 16, + color: Colors.grey[800], + ), + ), + ], + ), + ), + ), + SizedBox(height: 20), + // Warning card + Card( + elevation: 2, + color: Colors.orange[50], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.warning_amber, color: Colors.orange[700]), + SizedBox(width: 10), + Text( + 'Important', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orange[700], + ), + ), + ], + ), + SizedBox(height: 8), + Text( + 'After changing your email, ${widget.currentEmail} will no longer receive any communications from BlindMaster, including password reset codes.', + style: TextStyle( + fontSize: 14, + color: Colors.grey[800], + ), + ), + ], + ), + ), + ), + SizedBox(height: 30), + Text( + 'Verify your identity and enter new email', + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 30), + BlindMasterMainInput( + 'Current Password', + controller: passwordController, + password: true, + validator: passwordValidator, + ), + SizedBox(height: 20), + Divider(), + SizedBox(height: 20), + BlindMasterMainInput( + 'New Email Address', + controller: newEmailController, + validator: emailValidator, + ), + SizedBox(height: 30), + ElevatedButton( + onPressed: handleChangeEmail, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColorLight, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 15), + ), + child: Text( + 'Send Verification Email', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/BlindMasterScreens/addingDevices/device_setup.dart b/lib/BlindMasterScreens/addingDevices/device_setup.dart index 773b8f7..a2b1895 100644 --- a/lib/BlindMasterScreens/addingDevices/device_setup.dart +++ b/lib/BlindMasterScreens/addingDevices/device_setup.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; +import 'package:blind_master/BlindMasterResources/text_inputs.dart'; import 'package:blind_master/BlindMasterScreens/addingDevices/set_device_name.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -58,7 +59,6 @@ class _DeviceSetupState extends State { final passControl = TextEditingController(); final unameControl = TextEditingController(); - bool _obscureWifiPassword = true; @override void initState() { super.initState(); @@ -203,9 +203,6 @@ class _DeviceSetupState extends State { 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) { @@ -225,9 +222,10 @@ class _DeviceSetupState extends State { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - TextFormField( + BlindMasterInput( + 'Enterprise Login', controller: unameControl, - decoration: const InputDecoration(hintText: "Enter your enterprise login"), + hintText: "Enter your enterprise login", textInputAction: TextInputAction.next, onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), validator: (value) => (value == null || value.isEmpty) ? "Empty username!" : null, @@ -236,22 +234,11 @@ class _DeviceSetupState extends State { ] ), if (!open) - TextFormField( + BlindMasterInput( + 'WiFi Password', controller: passControl, - obscureText: _obscureWifiPassword, - decoration: InputDecoration( - hintText: "Enter password", - suffixIcon: IconButton( - icon: Icon( - _obscureWifiPassword ? Icons.visibility : Icons.visibility_off, - ), - onPressed: () { - setState(() { - _obscureWifiPassword = !_obscureWifiPassword; - }); - }, - ), - ), + password: true, + hintText: "Enter password", validator: (value) => (value == null || value.length < 8) ? "Not long enough!" : null, textInputAction: TextInputAction.send, onFieldSubmitted: (value) { diff --git a/lib/BlindMasterScreens/groupControl/create_group_dialog.dart b/lib/BlindMasterScreens/groupControl/create_group_dialog.dart index 11d5a9b..ba2b6b5 100644 --- a/lib/BlindMasterScreens/groupControl/create_group_dialog.dart +++ b/lib/BlindMasterScreens/groupControl/create_group_dialog.dart @@ -1,6 +1,7 @@ import 'dart:convert'; 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'; @@ -151,21 +152,10 @@ class _CreateGroupDialogState extends State { : Column( mainAxisSize: MainAxisSize.min, children: [ - TextField( + BlindMasterInput( + 'Group Name', controller: _nameController, - decoration: InputDecoration( - labelText: 'Group Name', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColorDark, - width: 2, - ), - ), - ), + focusedBorderColor: Theme.of(context).primaryColorDark, onChanged: (_) { if (errorMessage != null) { setState(() { diff --git a/lib/BlindMasterScreens/individualControl/device_screen.dart b/lib/BlindMasterScreens/individualControl/device_screen.dart index d6669c8..554fb94 100644 --- a/lib/BlindMasterScreens/individualControl/device_screen.dart +++ b/lib/BlindMasterScreens/individualControl/device_screen.dart @@ -211,12 +211,9 @@ class _DeviceScreenState extends State { content: Column( mainAxisSize: MainAxisSize.min, // Keep column compact children: [ - TextFormField( + BlindMasterInput( + 'Peripheral Name', controller: _newPeripheralNameController, - decoration: const InputDecoration( - labelText: 'Peripheral Name', - border: OutlineInputBorder(), - ), ), const SizedBox(height: 20), DropdownButtonFormField(