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.
|
/// Fetch notification preferences.
|
||||||
/// Agent: GET /agents/preferences/notifications
|
/// Agent: GET /agents/preferences/notifications
|
||||||
/// User: GET /users/preferences/notifications
|
/// User: GET /users/preferences/notifications
|
||||||
|
|||||||
@@ -256,11 +256,37 @@ class _ProfileSettingsTabState extends ConsumerState<ProfileSettingsTab> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ProfileFormField(
|
Row(
|
||||||
label: 'Email Address',
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
controller: _emailController,
|
children: [
|
||||||
enabled: false,
|
Expanded(
|
||||||
textCapitalization: TextCapitalization.none),
|
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),
|
const SizedBox(height: 24),
|
||||||
ProfileFormField(
|
ProfileFormField(
|
||||||
label: 'Phone Number',
|
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