feat: implement email change request functionality with verification dialog
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user