From 3c61c25f5e0b33d54f278b6ea2a6983a0c92e1f2 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Mon, 20 Apr 2026 14:07:37 +0530 Subject: [PATCH] feat: implement email change request functionality with verification dialog --- .../profile/data/profile_repository.dart | 22 +++ .../widgets/profile_settings_tab.dart | 141 +++++++++++++++++- 2 files changed, 158 insertions(+), 5 deletions(-) diff --git a/lib/features/profile/data/profile_repository.dart b/lib/features/profile/data/profile_repository.dart index 8affce2..c385350 100644 --- a/lib/features/profile/data/profile_repository.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -207,6 +207,28 @@ class ProfileRepository { } } + /// Request email change: POST /auth/change-email + /// Backend sends a verification link to [newEmail]; email is only + /// persisted once the link is clicked. + Future requestEmailChange(String newEmail, String password) async { + try { + final response = await _dio.post('/auth/change-email', data: { + 'newEmail': newEmail, + 'password': password, + }); + return (response.data?['data']?['message'] as String?) ?? + 'Verification link sent'; + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException( + message: e.response?.data?['message'] ?? + e.message ?? + 'Failed to request email change', + statusCode: e.response?.statusCode, + ); + } + } + /// Fetch notification preferences. /// Agent: GET /agents/preferences/notifications /// User: GET /users/preferences/notifications diff --git a/lib/features/profile/presentation/widgets/profile_settings_tab.dart b/lib/features/profile/presentation/widgets/profile_settings_tab.dart index 27abc6e..4c2e993 100644 --- a/lib/features/profile/presentation/widgets/profile_settings_tab.dart +++ b/lib/features/profile/presentation/widgets/profile_settings_tab.dart @@ -256,11 +256,37 @@ class _ProfileSettingsTabState extends ConsumerState { ), ], const SizedBox(height: 24), - ProfileFormField( - label: 'Email Address', - controller: _emailController, - enabled: false, - textCapitalization: TextCapitalization.none), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: ProfileFormField( + label: 'Email Address', + controller: _emailController, + enabled: false, + textCapitalization: TextCapitalization.none), + ), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: TextButton( + onPressed: _openChangeEmailDialog, + style: TextButton.styleFrom( + foregroundColor: AppColors.accentOrange, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + child: const Text( + 'Change', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + ), + ], + ), const SizedBox(height: 24), ProfileFormField( label: 'Phone Number', @@ -403,4 +429,109 @@ class _ProfileSettingsTabState extends ConsumerState { ), ); } + + Future _openChangeEmailDialog() async { + final newEmailCtl = TextEditingController(); + final passwordCtl = TextEditingController(); + String? localError; + bool submitting = false; + + await showDialog( + context: context, + builder: (dialogCtx) { + return StatefulBuilder( + builder: (dialogCtx, setDialogState) { + Future submit() async { + final email = newEmailCtl.text.trim(); + final pw = passwordCtl.text; + if (email.isEmpty || pw.isEmpty) { + setDialogState(() => localError = + 'Please enter a new email and your current password'); + return; + } + setDialogState(() { + submitting = true; + localError = null; + }); + try { + final repo = ref.read(profileRepositoryProvider); + final message = await repo.requestEmailChange(email, pw); + if (dialogCtx.mounted) Navigator.of(dialogCtx).pop(); + if (mounted) _showSnackBar(message); + } catch (e) { + setDialogState(() { + submitting = false; + localError = e.toString().replaceFirst('Exception: ', ''); + }); + } + } + + return AlertDialog( + title: const Text('Change Email Address'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'A verification link will be sent to your new email. Your email changes only after you click the link.', + style: TextStyle(fontSize: 13), + ), + const SizedBox(height: 16), + TextField( + controller: newEmailCtl, + keyboardType: TextInputType.emailAddress, + autocorrect: false, + textCapitalization: TextCapitalization.none, + decoration: const InputDecoration( + labelText: 'New Email', + hintText: 'new@example.com', + ), + ), + const SizedBox(height: 12), + TextField( + controller: passwordCtl, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Current Password', + ), + ), + if (localError != null) ...[ + const SizedBox(height: 12), + Text( + localError!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ], + ], + ), + ), + actions: [ + TextButton( + onPressed: submitting + ? null + : () => Navigator.of(dialogCtx).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: submitting ? null : submit, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.accentOrange, + foregroundColor: Colors.white, + ), + child: Text(submitting ? 'Sending...' : 'Send Link'), + ), + ], + ); + }, + ); + }, + ); + + newEmailCtl.dispose(); + passwordCtl.dispose(); + } }