diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4b3a7..5397177 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.1 - Added Iconica CI and Iconica Linter 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 5fb46a4..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: diff --git a/lib/src/models/user.dart b/lib/src/models/user.dart index f54f1a1..be9eba0 100644 --- a/lib/src/models/user.dart +++ b/lib/src/models/user.dart @@ -47,10 +47,10 @@ class User { /// ProfileData is used to store custom/addintional data for a user. /// -/// The MapWidget method is used to bind a [Widget] to one of the keys. +/// The MapWidget method is used to bind a [Widget] to one of the keys. /// This will override the standard textfield. /// -/// The Builditems method is used to make the list of +/// The Builditems method is used to make the list of /// field to house the user data. abstract class ProfileData { const ProfileData(); diff --git a/lib/src/services/profile_service.dart b/lib/src/services/profile_service.dart index 56394d4..4eaa7e5 100644 --- a/lib/src/services/profile_service.dart +++ b/lib/src/services/profile_service.dart @@ -6,15 +6,18 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_profile/src/models/user.dart'; -/// ProfileService can be extended and set for the profilePage. +/// ProfileService can be extended and set for the profilePage. /// The following method can be overriden. /// -/// BottompageAction is called when the [InkWell] at the bottom of +/// BottompageAction is called when the [InkWell] at the bottom of /// the page is tapped. /// /// 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(); @@ -28,5 +31,9 @@ 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 825980f..464cacb 100644 --- a/lib/src/widgets/item_builder/item_builder.dart +++ b/lib/src/widgets/item_builder/item_builder.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_input_library/flutter_input_library.dart'; import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart'; -/// ItemBuilder is used to set the standard textfield for each undefined +/// ItemBuilder is used to set the standard textfield for each undefined /// users data item. /// /// Options sets options for the textfield. @@ -54,6 +54,7 @@ class ItemBuilder { Widget buildPassword( String key, + TextEditingController controller, Function(String?) onChanged, String? Function(String?) validator, ) { @@ -61,6 +62,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/item_builder/item_builder_options.dart b/lib/src/widgets/item_builder/item_builder_options.dart index 8c0d7c7..86618bf 100644 --- a/lib/src/widgets/item_builder/item_builder_options.dart +++ b/lib/src/widgets/item_builder/item_builder_options.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; -/// ItemBuilderOptions is a class to store all settings for a field in the +/// ItemBuilderOptions is a class to store all settings for a field in the /// profile page. /// -/// InputDecoration sets the decoration for all standard textfields. -/// This is overridden if a field specific decoration is set by +/// InputDecoration sets the decoration for all standard textfields. +/// This is overridden if a field specific decoration is set by /// inputDecorationField. /// /// inputDecorationField sets the inputdecoration by key of the user data field. diff --git a/lib/src/widgets/profile/change_password_widget.dart b/lib/src/widgets/profile/change_password_widget.dart index 9a11ac7..0eb53f7 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) { @@ -90,14 +109,26 @@ class _ChangePasswordState extends State { var theme = Theme.of(context); Future onTapSave() async { - if ((_formKey.currentState?.validate() ?? false) && password2 != null) { - widget.service.changePassword(password2!); + 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_style.dart b/lib/src/widgets/profile/profile_style.dart index 5ddbdfe..28c035f 100644 --- a/lib/src/widgets/profile/profile_style.dart +++ b/lib/src/widgets/profile/profile_style.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; -/// ProfielStyle is used to set a couple of style paramaters for the +/// ProfielStyle is used to set a couple of style paramaters for the /// whole profile page. /// /// AvatarStyle is used to set some styling for the avatar using [AvatarStyle]. /// -/// PagePaddign is used to set the padding around the whole profile page +/// PagePaddign is used to set the padding around the whole profile page /// with its parent. /// /// BetweenDefaultitemPadding sets te padding between each user data item. @@ -24,7 +24,7 @@ class ProfileStyle { /// AvatarStyle can be used to set some avatar styling parameters. final TextStyle avatarTextStyle; - /// PagePadding can be set to determine the padding of the whole page + /// PagePadding can be set to determine the padding of the whole page /// againt the profile page parent. final EdgeInsetsGeometry pagePadding; diff --git a/lib/src/widgets/profile/profile_wrapper.dart b/lib/src/widgets/profile/profile_wrapper.dart index 037cff6..121c4eb 100644 --- a/lib/src/widgets/profile/profile_wrapper.dart +++ b/lib/src/widgets/profile/profile_wrapper.dart @@ -74,9 +74,9 @@ class _ProfileWrapperState extends State { @override void initState() { + super.initState(); _formKey = widget.formKey ?? GlobalKey(); - super.initState(); if (widget.showDefaultItems) { if (widget.itemBuilder == null) { var builder = ItemBuilder( @@ -196,50 +196,55 @@ class _ProfileWrapperState extends State { } @override - Widget build(BuildContext context) => Material( - color: Colors.transparent, - child: SingleChildScrollView( - child: SizedBox( + Widget build(BuildContext context) => Stack( + children: [ + SizedBox( height: MediaQuery.of(context).size.height, - child: Padding( - padding: widget.style.pagePadding, - child: Column( - children: [ - if (widget.showAvatar) ...[ - InkWell( - onTap: () async => widget.service.uploadImage( - context, - onUploadStateChanged: (bool 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: () async => 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,42 +253,35 @@ 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: () async { - 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: () async => await 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 ed15053..faf4221 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_profile description: Flutter profile package -version: 1.2.1 +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 dac3477..9fb98ca 100644 --- a/test/test_classes/test_profile_service.dart +++ b/test/test_classes/test_profile_service.dart @@ -28,5 +28,10 @@ class TestProfileService extends ProfileService { }) {} @override - FutureOr changePassword(String password) {} + FutureOr changePassword( + BuildContext context, + String currentPassword, + String newPassword, + ) => + true; }