feat: implement email change request functionality with verification dialog

This commit is contained in:
pradeepkumar
2026-04-20 14:07:37 +05:30
parent 6b0fa24b85
commit 3c61c25f5e
2 changed files with 158 additions and 5 deletions

View File

@@ -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<String> 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

View File

@@ -256,11 +256,37 @@ class _ProfileSettingsTabState extends ConsumerState<ProfileSettingsTab> {
),
],
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<ProfileSettingsTab> {
),
);
}
Future<void> _openChangeEmailDialog() async {
final newEmailCtl = TextEditingController();
final passwordCtl = TextEditingController();
String? localError;
bool submitting = false;
await showDialog<void>(
context: context,
builder: (dialogCtx) {
return StatefulBuilder(
builder: (dialogCtx, setDialogState) {
Future<void> 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();
}
}