diff --git a/lib/features/profile/presentation/widgets/profile_settings_tab.dart b/lib/features/profile/presentation/widgets/profile_settings_tab.dart index 75d8b1c..0aceb65 100644 --- a/lib/features/profile/presentation/widgets/profile_settings_tab.dart +++ b/lib/features/profile/presentation/widgets/profile_settings_tab.dart @@ -431,233 +431,251 @@ class _ProfileSettingsTabState extends ConsumerState { } Future _openChangeEmailDialog() async { - final newEmailCtl = TextEditingController(); - final passwordCtl = TextEditingController(); - String? localError; - bool submitting = false; - bool obscurePw = true; - await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetCtx) { - return StatefulBuilder( - builder: (sheetCtx, setSheetState) { - Future submit() async { - final email = newEmailCtl.text.trim(); - final pw = passwordCtl.text; - if (email.isEmpty || pw.isEmpty) { - setSheetState(() => localError = - 'Please enter a new email and your current password'); - return; - } - setSheetState(() { - submitting = true; - localError = null; - }); - try { - final repo = ref.read(profileRepositoryProvider); - final message = await repo.requestEmailChange(email, pw); - if (sheetCtx.mounted) Navigator.of(sheetCtx).pop(); - if (mounted) _showSnackBar(message); - } catch (e) { - setSheetState(() { - submitting = false; - localError = e.toString().replaceFirst('Exception: ', ''); - }); - } - } - - // Add bottom inset so the sheet rises above the keyboard. - final bottomInset = MediaQuery.of(sheetCtx).viewInsets.bottom; - - return Padding( - padding: EdgeInsets.only(bottom: bottomInset), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical( - top: Radius.circular(24), - ), - ), - padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), - child: SafeArea( - top: false, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Drag handle - Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: const Color(0xFFE5E7EB), - borderRadius: BorderRadius.circular(2), - ), - ), - ), - const Text( - 'Change Email Address', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 18, - fontWeight: FontWeight.w700, - color: AppColors.primaryDark, - ), - ), - const SizedBox(height: 8), - const Text( - 'A verification link will be sent to your new email. Your email changes only after you click the link.', - style: TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 13, - color: Color(0xFF6B7280), - height: 1.4, - ), - ), - const SizedBox(height: 20), - _buildSheetLabel('New Email'), - const SizedBox(height: 6), - TextField( - controller: newEmailCtl, - keyboardType: TextInputType.emailAddress, - autocorrect: false, - enableSuggestions: false, - textCapitalization: TextCapitalization.none, - autofillHints: const [AutofillHints.email], - decoration: _sheetInputDecoration( - 'new@example.com', - ), - ), - const SizedBox(height: 14), - _buildSheetLabel('Current Password'), - const SizedBox(height: 6), - TextField( - controller: passwordCtl, - obscureText: obscurePw, - autocorrect: false, - enableSuggestions: false, - // Empty autofillHints prevents iOS from showing the - // "Strong Password" suggestion bar, which was adding - // extra bottom inset. - autofillHints: const [], - decoration: _sheetInputDecoration( - 'Enter your current password', - suffixIcon: IconButton( - icon: Icon( - obscurePw - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - color: const Color(0xFF6B7280), - size: 20, - ), - onPressed: () => setSheetState( - () => obscurePw = !obscurePw, - ), - ), - ), - ), - if (localError != null) ...[ - const SizedBox(height: 12), - Text( - localError!, - style: const TextStyle( - fontFamily: 'SourceSerif4', - color: Colors.red, - fontSize: 12, - ), - ), - ], - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: submitting - ? null - : () => Navigator.of(sheetCtx).pop(), - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Color(0xFFE5E7EB), - ), - padding: const EdgeInsets.symmetric( - vertical: 14, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - 'Cancel', - style: TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.primaryDark, - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: submitting ? null : submit, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.accentOrange, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - vertical: 14, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - child: Text( - submitting ? 'Sending...' : 'Send Link', - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); + return _ChangeEmailSheet( + onSubmit: (email, password) async { + final repo = ref.read(profileRepositoryProvider); + return repo.requestEmailChange(email, password); + }, + onSuccess: (message) { + if (mounted) _showSnackBar(message); }, ); }, ); + } +} - newEmailCtl.dispose(); - passwordCtl.dispose(); +/// Bottom-sheet widget for changing email. Owns its own controllers so they +/// remain valid throughout the dismiss animation, then disposes cleanly. +class _ChangeEmailSheet extends StatefulWidget { + final Future Function(String email, String password) onSubmit; + final void Function(String message) onSuccess; + + const _ChangeEmailSheet({ + required this.onSubmit, + required this.onSuccess, + }); + + @override + State<_ChangeEmailSheet> createState() => _ChangeEmailSheetState(); +} + +class _ChangeEmailSheetState extends State<_ChangeEmailSheet> { + final _newEmailCtl = TextEditingController(); + final _passwordCtl = TextEditingController(); + String? _localError; + bool _submitting = false; + bool _obscurePw = true; + + @override + void dispose() { + _newEmailCtl.dispose(); + _passwordCtl.dispose(); + super.dispose(); } - Widget _buildSheetLabel(String text) { - return Text( - text, - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 13, - fontWeight: FontWeight.w500, - color: AppColors.primaryDark, + Future _submit() async { + final email = _newEmailCtl.text.trim(); + final pw = _passwordCtl.text; + if (email.isEmpty || pw.isEmpty) { + setState(() => _localError = + 'Please enter a new email and your current password'); + return; + } + setState(() { + _submitting = true; + _localError = null; + }); + try { + final message = await widget.onSubmit(email, pw); + if (!mounted) return; + Navigator.of(context).pop(); + widget.onSuccess(message); + } catch (e) { + if (!mounted) return; + setState(() { + _submitting = false; + _localError = e.toString().replaceFirst('Exception: ', ''); + }); + } + } + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return Padding( + padding: EdgeInsets.only(bottom: bottomInset), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), + child: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Drag handle + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: const Color(0xFFE5E7EB), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const Text( + 'Change Email Address', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + const Text( + 'A verification link will be sent to your new email. Your email changes only after you click the link.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 13, + color: Color(0xFF6B7280), + height: 1.4, + ), + ), + const SizedBox(height: 20), + _label('New Email'), + const SizedBox(height: 6), + TextField( + controller: _newEmailCtl, + keyboardType: TextInputType.emailAddress, + autocorrect: false, + enableSuggestions: false, + textCapitalization: TextCapitalization.none, + autofillHints: const [AutofillHints.email], + decoration: _decoration('new@example.com'), + ), + const SizedBox(height: 14), + _label('Current Password'), + const SizedBox(height: 6), + TextField( + controller: _passwordCtl, + obscureText: _obscurePw, + autocorrect: false, + enableSuggestions: false, + // Empty autofillHints prevents iOS from showing the + // "Strong Password" suggestion bar. + autofillHints: const [], + decoration: _decoration( + 'Enter your current password', + suffixIcon: IconButton( + icon: Icon( + _obscurePw + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: const Color(0xFF6B7280), + size: 20, + ), + onPressed: () => + setState(() => _obscurePw = !_obscurePw), + ), + ), + ), + if (_localError != null) ...[ + const SizedBox(height: 12), + Text( + _localError!, + style: const TextStyle( + fontFamily: 'SourceSerif4', + color: Colors.red, + fontSize: 12, + ), + ), + ], + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _submitting + ? null + : () => Navigator.of(context).pop(), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Color(0xFFE5E7EB)), + padding: + const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: _submitting ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.accentOrange, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: Text( + _submitting ? 'Sending...' : 'Send Link', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), ), ); } - InputDecoration _sheetInputDecoration(String hint, {Widget? suffixIcon}) { + Widget _label(String text) => Text( + text, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ); + + InputDecoration _decoration(String hint, {Widget? suffixIcon}) { return InputDecoration( hintText: hint, hintStyle: const TextStyle(