From ce3c599fa50b57de6f093f471e848f20934af779 Mon Sep 17 00:00:00 2001 From: Jacques Date: Wed, 7 Feb 2024 16:31:11 +0100 Subject: [PATCH] feat: Add password field for authentication --- CHANGELOG.md | 4 + example/lib/main.dart | 103 ++++++------- .../lib/utils/example_profile_service.dart | 7 +- example/pubspec.lock | 8 +- lib/src/services/profile_service.dart | 5 +- .../widgets/item_builder/item_builder.dart | 2 + .../profile/change_password_widget.dart | 38 ++++- lib/src/widgets/profile/profile_wrapper.dart | 136 +++++++++--------- pubspec.yaml | 4 +- test/test_classes/test_profile_service.dart | 5 +- 10 files changed, 181 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4364257..34d0fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.3.0 + +- Field has been added so the user can provide it's current password for reauthentication. + ## 1.2.0 - Added the posibilty to enable the user to change it's password. diff --git a/example/lib/main.dart b/example/lib/main.dart index 2ff9bc9..d869729 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -52,62 +52,67 @@ class _ProfileExampleState extends State { var width = MediaQuery.of(context).size.width; return Scaffold( - body: Center( - child: ProfilePage( - changePasswordConfig: - const ChangePasswordConfig(enablePasswordChange: true), - wrapViewOptions: WrapViewOptions( - direction: Axis.horizontal, - spacing: 16, - ), - bottomActionText: 'Log out', - itemBuilderOptions: ItemBuilderOptions( - //no label for email - validators: { - 'first_name': (String? value) { - if (value == null || value.isEmpty) { - return 'Field empty'; - } - return null; - }, - 'last_name': (String? value) { - if (value == null || value.isEmpty) { - return 'Field empty'; - } - return null; - }, - 'email': (String? value) { - if (value == null || value.isEmpty) { - return 'Field empty'; - } - return null; - }, + body: ProfilePage( + changePasswordConfig: + const ChangePasswordConfig(enablePasswordChange: true), + wrapViewOptions: WrapViewOptions( + spacing: 8, + direction: Axis.vertical, + ), + bottomActionText: 'Log out', + itemBuilderOptions: ItemBuilderOptions( + //no label for email + validators: { + 'first_name': (String? value) { + if (value == null || value.isEmpty) { + return 'Field empty'; + } + return null; }, - inputDecorationField: { - 'password_1': const InputDecoration( + 'last_name': (String? value) { + if (value == null || value.isEmpty) { + return 'Field empty'; + } + return null; + }, + 'email': (String? value) { + if (value == null || value.isEmpty) { + return 'Field empty'; + } + return null; + }, + }, + + inputDecorationField: { + 'current_password': const InputDecoration( constraints: BoxConstraints( maxHeight: 60, - maxWidth: 200, + maxWidth: 250, ), - ), - 'password_2': const InputDecoration( + hintText: 'Current password'), + 'password_1': const InputDecoration( constraints: BoxConstraints( maxHeight: 60, - maxWidth: 200, + maxWidth: 250, ), - ), - }, - ), - user: _user, - service: ExampleProfileService(), - style: ProfileStyle( - avatarTextStyle: const TextStyle(fontSize: 20), - pagePadding: EdgeInsets.only( - top: 50, - bottom: 50, - left: width * 0.1, - right: width * 0.1, - ), + hintText: 'New password'), + 'password_2': const InputDecoration( + constraints: BoxConstraints( + maxHeight: 60, + maxWidth: 250, + ), + hintText: 'Repeat new password'), + }, + ), + user: _user, + service: ExampleProfileService(), + style: ProfileStyle( + avatarTextStyle: const TextStyle(fontSize: 20), + pagePadding: EdgeInsets.only( + top: 50, + bottom: 50, + left: width * 0.1, + right: width * 0.1, ), ), ), diff --git a/example/lib/utils/example_profile_service.dart b/example/lib/utils/example_profile_service.dart index d0ea9f7..59941e6 100644 --- a/example/lib/utils/example_profile_service.dart +++ b/example/lib/utils/example_profile_service.dart @@ -30,7 +30,10 @@ class ExampleProfileService extends ProfileService { } @override - FutureOr changePassword(String password) { - debugPrint(password); + FutureOr changePassword( + BuildContext context, String currentPassword, String newPassword) { + debugPrint('$currentPassword -> $newPassword'); + + return true; } } diff --git a/example/pubspec.lock b/example/pubspec.lock index b943d24..d235b58 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -122,11 +122,11 @@ packages: dependency: transitive description: path: "." - ref: "2.7.0" - resolved-ref: "8eb1d80a9f08be0b7fe70078104d1a8851083edd" + ref: "3.1.0" + resolved-ref: "5fca291c5e79c9ad6dad500e4ea5d9b628ee0f5d" url: "https://github.com/Iconica-Development/flutter_input_library" source: git - version: "2.7.0" + version: "3.1.0" flutter_lints: dependency: "direct dev" description: @@ -141,7 +141,7 @@ packages: path: ".." relative: true source: path - version: "1.1.6" + version: "1.2.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/src/services/profile_service.dart b/lib/src/services/profile_service.dart index b3726d4..65ab297 100644 --- a/lib/src/services/profile_service.dart +++ b/lib/src/services/profile_service.dart @@ -13,6 +13,8 @@ import 'package:flutter_profile/src/models/user.dart'; /// EditProfile is called when a user changes and submits a standard textfields. /// /// UploadImage is called when te user presses the avatar. +/// +/// changePassword is called when the user requests to change his password. Return true to clear the inputfields. abstract class ProfileService { const ProfileService(); @@ -25,5 +27,6 @@ abstract class ProfileService { required Function(bool isUploading) onUploadStateChanged, }); - FutureOr changePassword(String password); + FutureOr changePassword( + BuildContext context, String currentPassword, String newPassword); } diff --git a/lib/src/widgets/item_builder/item_builder.dart b/lib/src/widgets/item_builder/item_builder.dart index d9c4f92..11d2d02 100644 --- a/lib/src/widgets/item_builder/item_builder.dart +++ b/lib/src/widgets/item_builder/item_builder.dart @@ -50,6 +50,7 @@ class ItemBuilder { Widget buildPassword( String key, + TextEditingController controller, Function(String?) onChanged, String? Function(String?) validator, ) { @@ -57,6 +58,7 @@ class ItemBuilder { options.inputDecorationField?[key] ?? options.inputDecoration; return FlutterFormInputPassword( + controller: controller, style: options.inputTextStyle, decoration: inputDecoration, onChanged: onChanged, diff --git a/lib/src/widgets/profile/change_password_widget.dart b/lib/src/widgets/profile/change_password_widget.dart index 71459f8..ae97c64 100644 --- a/lib/src/widgets/profile/change_password_widget.dart +++ b/lib/src/widgets/profile/change_password_widget.dart @@ -32,6 +32,11 @@ class _ChangePasswordState extends State { late final Widget? changePasswordChild; + late var currentPasswordController = TextEditingController(); + late var password1Controller = TextEditingController(); + late var password2Controller = TextEditingController(); + + String? currentPassword; String? password1; String? password2; @@ -51,8 +56,21 @@ class _ChangePasswordState extends State { runSpacing: widget.wrapViewOptions?.runSpacing ?? 0, clipBehavior: widget.wrapViewOptions?.clipBehavior ?? Clip.none, children: [ + builder.buildPassword( + 'current_password', + currentPasswordController, + (value) => currentPassword = value, + (value) { + if (currentPassword?.isEmpty ?? true) { + return config.fieldRequiredErrorText; + } + + return null; + }, + ), builder.buildPassword( 'password_1', + password1Controller, (value) => password1 = value, (value) { if (password1?.isEmpty ?? true) { @@ -64,6 +82,7 @@ class _ChangePasswordState extends State { ), builder.buildPassword( 'password_2', + password2Controller, (value) => password2 = value, (value) { if (password2?.isEmpty ?? true) { @@ -89,15 +108,27 @@ class _ChangePasswordState extends State { Widget build(BuildContext context) { var theme = Theme.of(context); - void onTapSave() { - if ((_formKey.currentState?.validate() ?? false) && password2 != null) { - widget.service.changePassword(password2!); + void onTapSave() async { + if ((_formKey.currentState?.validate() ?? false) && + currentPassword != null && + password2 != null) { + if (await widget.service + .changePassword(context, currentPassword!, password2!)) { + currentPasswordController.clear(); + password1Controller.clear(); + password2Controller.clear(); + + currentPassword = null; + password1 = null; + password2 = null; + } } } return Form( key: _formKey, child: Column( + mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: widget.style.betweenDefaultItemPadding * 2.5, @@ -127,7 +158,6 @@ class _ChangePasswordState extends State { onPressed: () => onTapSave(), child: const Text('Save password'), ), - const Spacer(), ], ), ); diff --git a/lib/src/widgets/profile/profile_wrapper.dart b/lib/src/widgets/profile/profile_wrapper.dart index 07c9f63..e6dce58 100644 --- a/lib/src/widgets/profile/profile_wrapper.dart +++ b/lib/src/widgets/profile/profile_wrapper.dart @@ -73,9 +73,9 @@ class _ProfileWrapperState extends State { @override void initState() { + super.initState(); _formKey = widget.formKey ?? GlobalKey(); - super.initState(); if (widget.showDefaultItems) { if (widget.itemBuilder == null) { ItemBuilder builder = ItemBuilder( @@ -196,50 +196,55 @@ class _ProfileWrapperState extends State { @override Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: SingleChildScrollView( - child: SizedBox( + return Stack( + children: [ + SizedBox( height: MediaQuery.of(context).size.height, - child: Padding( - padding: widget.style.pagePadding, - child: Column( - children: [ - if (widget.showAvatar) ...[ - InkWell( - onTap: () => widget.service.uploadImage( - context, - onUploadStateChanged: (isUploading) => setState( - () { - _isUploadingImage = isUploading; - }, + width: MediaQuery.of(context).size.width, + child: SingleChildScrollView( + child: Padding( + padding: widget.style.pagePadding, + child: Column( + children: [ + if (widget.showAvatar) ...[ + InkWell( + onTap: () => widget.service.uploadImage( + context, + onUploadStateChanged: (isUploading) => setState( + () { + _isUploadingImage = isUploading; + }, + ), + ), + child: AvatarWrapper( + avatarBackgroundColor: widget.avatarBackgroundColor, + user: widget.user, + textStyle: widget.style.avatarTextStyle, + customAvatar: _isUploadingImage + ? Container( + width: 100, + height: 100, + decoration: const BoxDecoration( + color: Colors.black, + shape: BoxShape.circle, + ), + child: const CircularProgressIndicator(), + ) + : widget.customAvatar, ), ), - child: AvatarWrapper( - avatarBackgroundColor: widget.avatarBackgroundColor, - user: widget.user, - textStyle: widget.style.avatarTextStyle, - customAvatar: _isUploadingImage - ? Container( - width: 100, - height: 100, - decoration: const BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - ), - child: const CircularProgressIndicator(), - ) - : widget.customAvatar, + SizedBox( + height: widget.style.betweenDefaultItemPadding, ), - ), - SizedBox( - height: widget.style.betweenDefaultItemPadding, - ), - ], - if (widget.showItems) Form(key: _formKey, child: child), - if (widget.changePasswordConfig.enablePasswordChange) ...[ - Expanded( - child: ChangePassword( + ], + if (widget.showItems) ...[ + Form( + key: _formKey, + child: child, + ), + ], + if (widget.changePasswordConfig.enablePasswordChange) ...[ + ChangePassword( config: widget.changePasswordConfig, service: widget.service, wrapViewOptions: widget.wrapViewOptions, @@ -248,43 +253,38 @@ class _ProfileWrapperState extends State { itemBuilderOptions: widget.itemBuilderOptions, style: widget.style, ), - ), - ], - if (widget.bottomActionText != null) ...[ - SizedBox( - height: widget.style.betweenDefaultItemPadding, - ), - if (!widget.changePasswordConfig.enablePasswordChange) ...[ - const Spacer(), ], - InkWell( - onTap: () { - widget.service.pageBottomAction(); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - widget.bottomActionText!, - style: widget.style.bottomActionTextStyle, - ), - ), - ), ], - if (widget.bottomActionText == null && - !widget.changePasswordConfig.enablePasswordChange) ...[ - const Spacer(), - ] - ], + ), ), ), ), - ), + if (widget.bottomActionText != null && + MediaQuery.of(Scaffold.of(context).context).viewInsets.bottom == + 0.0) ...[ + Align( + alignment: Alignment.bottomCenter, + child: InkWell( + onTap: () { + widget.service.pageBottomAction(); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + widget.bottomActionText!, + style: widget.style.bottomActionTextStyle, + ), + ), + ), + ), + ], + ], ); } /// This calls onSaved on all the fiels which check if they have a new value void submitAllChangedFields() { - if (_formKey.currentState!.validate()) { + if (_formKey.currentState?.validate() ?? false) { _formKey.currentState!.save(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 0fb9f71..feb2e3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_profile description: Flutter profile package -version: 1.2.0 +version: 1.3.0 repository: https://github.com/Iconica-Development/flutter_profile publish_to: none @@ -15,7 +15,7 @@ dependencies: flutter_input_library: git: url: https://github.com/Iconica-Development/flutter_input_library - ref: 2.7.0 + ref: 3.1.0 flutter: sdk: flutter diff --git a/test/test_classes/test_profile_service.dart b/test/test_classes/test_profile_service.dart index 101e502..e226657 100644 --- a/test/test_classes/test_profile_service.dart +++ b/test/test_classes/test_profile_service.dart @@ -27,5 +27,8 @@ class TestProfileService extends ProfileService { }) {} @override - FutureOr changePassword(String password) {} + FutureOr changePassword( + BuildContext context, String currentPassword, String newPassword) { + return true; + } }