diff --git a/frontend/pshared/lib/widgets/password/confirm_field.dart b/frontend/pshared/lib/widgets/password/confirm_field.dart index acd00cc..7276b76 100644 --- a/frontend/pshared/lib/widgets/password/confirm_field.dart +++ b/frontend/pshared/lib/widgets/password/confirm_field.dart @@ -10,6 +10,8 @@ class ConfirmPasswordField extends StatelessWidget { required this.newPasswordController, required this.missingPasswordError, required this.passwordsDoNotMatchError, + required this.obscureText, + required this.onToggleVisibility, }); final TextEditingController controller; @@ -19,6 +21,8 @@ class ConfirmPasswordField extends StatelessWidget { final TextEditingController newPasswordController; final String missingPasswordError; final String passwordsDoNotMatchError; + final bool obscureText; + final VoidCallback onToggleVisibility; @override Widget build(BuildContext context) { @@ -26,11 +30,18 @@ class ConfirmPasswordField extends StatelessWidget { width: fieldWidth, child: TextFormField( controller: controller, - obscureText: true, + obscureText: obscureText, enabled: isEnabled, + autovalidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( labelText: confirmPasswordLabel, border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: onToggleVisibility, + icon: Icon( + obscureText ? Icons.visibility_off : Icons.visibility, + ), + ), ), validator: (value) { if (value == null || value.isEmpty) return missingPasswordError; diff --git a/frontend/pshared/lib/widgets/password/field.dart b/frontend/pshared/lib/widgets/password/field.dart index 5ff5e75..0e16d93 100644 --- a/frontend/pshared/lib/widgets/password/field.dart +++ b/frontend/pshared/lib/widgets/password/field.dart @@ -7,6 +7,8 @@ class PasswordField extends StatelessWidget { required this.labelText, required this.fieldWidth, required this.isEnabled, + required this.obscureText, + required this.onToggleVisibility, required this.validator, }); @@ -14,6 +16,8 @@ class PasswordField extends StatelessWidget { final String labelText; final double fieldWidth; final bool isEnabled; + final bool obscureText; + final VoidCallback onToggleVisibility; final String? Function(String?) validator; @override @@ -22,14 +26,21 @@ class PasswordField extends StatelessWidget { width: fieldWidth, child: TextFormField( controller: controller, - obscureText: true, + obscureText: obscureText, enabled: isEnabled, + autovalidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( labelText: labelText, border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: onToggleVisibility, + icon: Icon( + obscureText ? Icons.visibility_off : Icons.visibility, + ), + ), ), validator: validator, ), ); } -} \ No newline at end of file +} diff --git a/frontend/pshared/lib/widgets/password/fields.dart b/frontend/pshared/lib/widgets/password/fields.dart index 2966389..6e63037 100644 --- a/frontend/pshared/lib/widgets/password/fields.dart +++ b/frontend/pshared/lib/widgets/password/fields.dart @@ -18,6 +18,12 @@ class PasswordFields extends StatelessWidget { required this.fieldWidth, required this.gapSmall, required this.isEnabled, + required this.showOldPassword, + required this.showNewPassword, + required this.showConfirmPassword, + required this.onToggleOldPassword, + required this.onToggleNewPassword, + required this.onToggleConfirmPassword, }); final TextEditingController oldPasswordController; @@ -31,6 +37,12 @@ class PasswordFields extends StatelessWidget { final double fieldWidth; final double gapSmall; final bool isEnabled; + final bool showOldPassword; + final bool showNewPassword; + final bool showConfirmPassword; + final VoidCallback onToggleOldPassword; + final VoidCallback onToggleNewPassword; + final VoidCallback onToggleConfirmPassword; @override Widget build(BuildContext context) { @@ -41,6 +53,8 @@ class PasswordFields extends StatelessWidget { labelText: oldPasswordLabel, fieldWidth: fieldWidth, isEnabled: isEnabled, + obscureText: !showOldPassword, + onToggleVisibility: onToggleOldPassword, validator: (value) => (value == null || value.isEmpty) ? missingPasswordError : null, ), @@ -50,6 +64,8 @@ class PasswordFields extends StatelessWidget { labelText: newPasswordLabel, fieldWidth: fieldWidth, isEnabled: isEnabled, + obscureText: !showNewPassword, + onToggleVisibility: onToggleNewPassword, validator: (value) => (value == null || value.isEmpty) ? missingPasswordError : null, ), @@ -62,8 +78,10 @@ class PasswordFields extends StatelessWidget { newPasswordController: newPasswordController, missingPasswordError: missingPasswordError, passwordsDoNotMatchError: passwordsDoNotMatchError, + obscureText: !showConfirmPassword, + onToggleVisibility: onToggleConfirmPassword, ), ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/models/password_field_type.dart b/frontend/pweb/lib/models/password_field_type.dart new file mode 100644 index 0000000..3114647 --- /dev/null +++ b/frontend/pweb/lib/models/password_field_type.dart @@ -0,0 +1 @@ +enum PasswordFieldType { old, newPassword, confirmPassword } \ No newline at end of file diff --git a/frontend/pweb/lib/models/visibility.dart b/frontend/pweb/lib/models/visibility.dart new file mode 100644 index 0000000..a18711e --- /dev/null +++ b/frontend/pweb/lib/models/visibility.dart @@ -0,0 +1 @@ +enum VisibilityState { hidden, visible } \ No newline at end of file diff --git a/frontend/pweb/lib/pages/settings/profile/account/password/form/form.dart b/frontend/pweb/lib/pages/settings/profile/account/password/form/form.dart index a6ef7eb..ebd4d2c 100644 --- a/frontend/pweb/lib/pages/settings/profile/account/password/form/form.dart +++ b/frontend/pweb/lib/pages/settings/profile/account/password/form/form.dart @@ -4,6 +4,7 @@ import 'package:pshared/provider/account.dart'; import 'package:pshared/widgets/password/fields.dart'; import 'package:pshared/utils/snackbar.dart'; +import 'package:pweb/models/password_field_type.dart'; import 'package:pweb/providers/password_form.dart'; import 'package:pweb/pages/settings/profile/account/password/form/error_text.dart'; import 'package:pweb/pages/settings/profile/account/password/form/submit_button.dart'; @@ -65,6 +66,18 @@ class PasswordForm extends StatelessWidget { fieldWidth: _fieldWidth, gapSmall: _gapSmall, isEnabled: !isFormBusy, + showOldPassword: + formProvider.isPasswordVisible(PasswordFieldType.old), + showNewPassword: + formProvider.isPasswordVisible(PasswordFieldType.newPassword), + showConfirmPassword: formProvider + .isPasswordVisible(PasswordFieldType.confirmPassword), + onToggleOldPassword: () => + formProvider.togglePasswordVisibility(PasswordFieldType.old), + onToggleNewPassword: () => formProvider + .togglePasswordVisibility(PasswordFieldType.newPassword), + onToggleConfirmPassword: () => formProvider + .togglePasswordVisibility(PasswordFieldType.confirmPassword), ), const SizedBox(height: _gapMedium), PasswordSubmitButton( @@ -72,10 +85,11 @@ class PasswordForm extends StatelessWidget { label: savePassword, onSubmit: () async { try { - await formProvider.submit( + final success = await formProvider.submit( accountProvider: accountProvider, errorText: errorText, ); + if (!success) return; if (!context.mounted) return; notifyUser(context, successText); } catch (e) { diff --git a/frontend/pweb/lib/providers/password_form.dart b/frontend/pweb/lib/providers/password_form.dart index 30ab2c0..23770c7 100644 --- a/frontend/pweb/lib/providers/password_form.dart +++ b/frontend/pweb/lib/providers/password_form.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:pshared/provider/account.dart'; import 'package:pweb/models/edit_state.dart'; +import 'package:pshared/api/responses/error/server.dart'; +import 'package:pweb/models/password_field_type.dart'; +import 'package:pweb/models/visibility.dart'; class PasswordFormProvider extends ChangeNotifier { @@ -11,6 +14,11 @@ class PasswordFormProvider extends ChangeNotifier { final newPasswordController = TextEditingController(); final confirmPasswordController = TextEditingController(); + final Map _visibility = { + PasswordFieldType.old: VisibilityState.hidden, + PasswordFieldType.newPassword: VisibilityState.hidden, + PasswordFieldType.confirmPassword: VisibilityState.hidden, + }; EditState _state = EditState.view; String _errorText = ''; bool _disposed = false; @@ -19,6 +27,8 @@ class PasswordFormProvider extends ChangeNotifier { bool get isSaving => _state == EditState.saving; String get errorText => _errorText; EditState get state => _state; + bool isPasswordVisible(PasswordFieldType type) => + _visibility[type] == VisibilityState.visible; void toggleExpanded() { if (_state == EditState.saving) return; @@ -26,12 +36,21 @@ class PasswordFormProvider extends ChangeNotifier { _setError(''); } - Future submit({ + void togglePasswordVisibility(PasswordFieldType type) { + final current = _visibility[type]; + if (current == null) return; + _visibility[type] = current == VisibilityState.hidden + ? VisibilityState.visible + : VisibilityState.hidden; + notifyListeners(); + } + + Future submit({ required AccountProvider accountProvider, required String errorText, }) async { final currentForm = formKey.currentState; - if (currentForm == null || !currentForm.validate()) return; + if (currentForm == null || !currentForm.validate()) return false; _setState(EditState.saving); _setError(''); @@ -45,14 +64,22 @@ class PasswordFormProvider extends ChangeNotifier { oldPasswordController.clear(); newPasswordController.clear(); confirmPasswordController.clear(); + _setState(EditState.view); + return true; } catch (e) { - _setError(errorText); - rethrow; - } finally { + _setError(_errorMessageForException(e, errorText)); _setState(EditState.edit); + rethrow; } } + String _errorMessageForException(Object exception, String fallback) { + if (exception is ErrorResponse && exception.details.isNotEmpty) { + return exception.details; + } + return fallback; + } + void _setState(EditState value) { if (_state == value || _disposed) return; _state = value;