Merge pull request #28 from Iconica-Development/feat/change_password_current_password

feat: Add password field for authentication
This commit is contained in:
Gorter-dev 2024-02-08 09:28:33 +01:00 committed by GitHub
commit 802265e43c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 194 additions and 140 deletions

View file

@ -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 ## 1.2.1
- Added Iconica CI and Iconica Linter - Added Iconica CI and Iconica Linter

View file

@ -52,62 +52,67 @@ class _ProfileExampleState extends State<ProfileExample> {
var width = MediaQuery.of(context).size.width; var width = MediaQuery.of(context).size.width;
return Scaffold( return Scaffold(
body: Center( body: ProfilePage(
child: ProfilePage( changePasswordConfig:
changePasswordConfig: const ChangePasswordConfig(enablePasswordChange: true),
const ChangePasswordConfig(enablePasswordChange: true), wrapViewOptions: WrapViewOptions(
wrapViewOptions: WrapViewOptions( spacing: 8,
direction: Axis.horizontal, direction: Axis.vertical,
spacing: 16, ),
), bottomActionText: 'Log out',
bottomActionText: 'Log out', itemBuilderOptions: ItemBuilderOptions(
itemBuilderOptions: ItemBuilderOptions( //no label for email
//no label for email validators: {
validators: { 'first_name': (String? value) {
'first_name': (String? value) { if (value == null || value.isEmpty) {
if (value == null || value.isEmpty) { return 'Field empty';
return 'Field empty'; }
} return null;
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;
},
}, },
inputDecorationField: { 'last_name': (String? value) {
'password_1': const InputDecoration( 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( constraints: BoxConstraints(
maxHeight: 60, maxHeight: 60,
maxWidth: 200, maxWidth: 250,
), ),
), hintText: 'Current password'),
'password_2': const InputDecoration( 'password_1': const InputDecoration(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: 60, maxHeight: 60,
maxWidth: 200, maxWidth: 250,
), ),
), hintText: 'New password'),
}, 'password_2': const InputDecoration(
), constraints: BoxConstraints(
user: _user, maxHeight: 60,
service: ExampleProfileService(), maxWidth: 250,
style: ProfileStyle( ),
avatarTextStyle: const TextStyle(fontSize: 20), hintText: 'Repeat new password'),
pagePadding: EdgeInsets.only( },
top: 50, ),
bottom: 50, user: _user,
left: width * 0.1, service: ExampleProfileService(),
right: width * 0.1, style: ProfileStyle(
), avatarTextStyle: const TextStyle(fontSize: 20),
pagePadding: EdgeInsets.only(
top: 50,
bottom: 50,
left: width * 0.1,
right: width * 0.1,
), ),
), ),
), ),

View file

@ -30,7 +30,10 @@ class ExampleProfileService extends ProfileService {
} }
@override @override
FutureOr<void> changePassword(String password) { FutureOr<bool> changePassword(
debugPrint(password); BuildContext context, String currentPassword, String newPassword) {
debugPrint('$currentPassword -> $newPassword');
return true;
} }
} }

View file

@ -122,11 +122,11 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "." path: "."
ref: "2.7.0" ref: "3.1.0"
resolved-ref: "8eb1d80a9f08be0b7fe70078104d1a8851083edd" resolved-ref: "5fca291c5e79c9ad6dad500e4ea5d9b628ee0f5d"
url: "https://github.com/Iconica-Development/flutter_input_library" url: "https://github.com/Iconica-Development/flutter_input_library"
source: git source: git
version: "2.7.0" version: "3.1.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:

View file

@ -47,10 +47,10 @@ class User {
/// ProfileData is used to store custom/addintional data for a 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. /// 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. /// field to house the user data.
abstract class ProfileData { abstract class ProfileData {
const ProfileData(); const ProfileData();

View file

@ -6,15 +6,18 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_profile/src/models/user.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. /// 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. /// the page is tapped.
/// ///
/// EditProfile is called when a user changes and submits a standard textfields. /// EditProfile is called when a user changes and submits a standard textfields.
/// ///
/// UploadImage is called when te user presses the avatar. /// 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 { abstract class ProfileService {
const ProfileService(); const ProfileService();
@ -28,5 +31,9 @@ abstract class ProfileService {
required Function(bool isUploading) onUploadStateChanged, required Function(bool isUploading) onUploadStateChanged,
}); });
FutureOr<void> changePassword(String password); FutureOr<bool> changePassword(
BuildContext context,
String currentPassword,
String newPassword,
);
} }

View file

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_input_library/flutter_input_library.dart'; import 'package:flutter_input_library/flutter_input_library.dart';
import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.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. /// users data item.
/// ///
/// Options sets options for the textfield. /// Options sets options for the textfield.
@ -54,6 +54,7 @@ class ItemBuilder {
Widget buildPassword( Widget buildPassword(
String key, String key,
TextEditingController controller,
Function(String?) onChanged, Function(String?) onChanged,
String? Function(String?) validator, String? Function(String?) validator,
) { ) {
@ -61,6 +62,7 @@ class ItemBuilder {
options.inputDecorationField?[key] ?? options.inputDecoration; options.inputDecorationField?[key] ?? options.inputDecoration;
return FlutterFormInputPassword( return FlutterFormInputPassword(
controller: controller,
style: options.inputTextStyle, style: options.inputTextStyle,
decoration: inputDecoration, decoration: inputDecoration,
onChanged: onChanged, onChanged: onChanged,

View file

@ -4,11 +4,11 @@
import 'package:flutter/material.dart'; 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. /// profile page.
/// ///
/// InputDecoration sets the decoration for all standard textfields. /// InputDecoration sets the decoration for all standard textfields.
/// This is overridden if a field specific decoration is set by /// This is overridden if a field specific decoration is set by
/// inputDecorationField. /// inputDecorationField.
/// ///
/// inputDecorationField sets the inputdecoration by key of the user data field. /// inputDecorationField sets the inputdecoration by key of the user data field.

View file

@ -32,6 +32,11 @@ class _ChangePasswordState extends State<ChangePassword> {
late final Widget? changePasswordChild; late final Widget? changePasswordChild;
late var currentPasswordController = TextEditingController();
late var password1Controller = TextEditingController();
late var password2Controller = TextEditingController();
String? currentPassword;
String? password1; String? password1;
String? password2; String? password2;
@ -51,8 +56,21 @@ class _ChangePasswordState extends State<ChangePassword> {
runSpacing: widget.wrapViewOptions?.runSpacing ?? 0, runSpacing: widget.wrapViewOptions?.runSpacing ?? 0,
clipBehavior: widget.wrapViewOptions?.clipBehavior ?? Clip.none, clipBehavior: widget.wrapViewOptions?.clipBehavior ?? Clip.none,
children: [ children: [
builder.buildPassword(
'current_password',
currentPasswordController,
(value) => currentPassword = value,
(value) {
if (currentPassword?.isEmpty ?? true) {
return config.fieldRequiredErrorText;
}
return null;
},
),
builder.buildPassword( builder.buildPassword(
'password_1', 'password_1',
password1Controller,
(value) => password1 = value, (value) => password1 = value,
(value) { (value) {
if (password1?.isEmpty ?? true) { if (password1?.isEmpty ?? true) {
@ -64,6 +82,7 @@ class _ChangePasswordState extends State<ChangePassword> {
), ),
builder.buildPassword( builder.buildPassword(
'password_2', 'password_2',
password2Controller,
(value) => password2 = value, (value) => password2 = value,
(value) { (value) {
if (password2?.isEmpty ?? true) { if (password2?.isEmpty ?? true) {
@ -90,14 +109,26 @@ class _ChangePasswordState extends State<ChangePassword> {
var theme = Theme.of(context); var theme = Theme.of(context);
Future<void> onTapSave() async { Future<void> onTapSave() async {
if ((_formKey.currentState?.validate() ?? false) && password2 != null) { if ((_formKey.currentState?.validate() ?? false) &&
widget.service.changePassword(password2!); 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( return Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox( SizedBox(
height: widget.style.betweenDefaultItemPadding * 2.5, height: widget.style.betweenDefaultItemPadding * 2.5,
@ -127,7 +158,6 @@ class _ChangePasswordState extends State<ChangePassword> {
onPressed: onTapSave, onPressed: onTapSave,
child: const Text('Save password'), child: const Text('Save password'),
), ),
const Spacer(),
], ],
), ),
); );

View file

@ -4,12 +4,12 @@
import 'package:flutter/material.dart'; 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. /// whole profile page.
/// ///
/// AvatarStyle is used to set some styling for the avatar using [AvatarStyle]. /// 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. /// with its parent.
/// ///
/// BetweenDefaultitemPadding sets te padding between each user data item. /// 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. /// AvatarStyle can be used to set some avatar styling parameters.
final TextStyle avatarTextStyle; 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. /// againt the profile page parent.
final EdgeInsetsGeometry pagePadding; final EdgeInsetsGeometry pagePadding;

View file

@ -74,9 +74,9 @@ class _ProfileWrapperState extends State<ProfileWrapper> {
@override @override
void initState() { void initState() {
super.initState();
_formKey = widget.formKey ?? GlobalKey<FormState>(); _formKey = widget.formKey ?? GlobalKey<FormState>();
super.initState();
if (widget.showDefaultItems) { if (widget.showDefaultItems) {
if (widget.itemBuilder == null) { if (widget.itemBuilder == null) {
var builder = ItemBuilder( var builder = ItemBuilder(
@ -196,50 +196,55 @@ class _ProfileWrapperState extends State<ProfileWrapper> {
} }
@override @override
Widget build(BuildContext context) => Material( Widget build(BuildContext context) => Stack(
color: Colors.transparent, children: [
child: SingleChildScrollView( SizedBox(
child: SizedBox(
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
child: Padding( width: MediaQuery.of(context).size.width,
padding: widget.style.pagePadding, child: SingleChildScrollView(
child: Column( child: Padding(
children: [ padding: widget.style.pagePadding,
if (widget.showAvatar) ...[ child: Column(
InkWell( children: [
onTap: () async => widget.service.uploadImage( if (widget.showAvatar) ...[
context, InkWell(
onUploadStateChanged: (bool isUploading) => setState( onTap: () async => widget.service.uploadImage(
() { context,
_isUploadingImage = isUploading; 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( SizedBox(
avatarBackgroundColor: widget.avatarBackgroundColor, height: widget.style.betweenDefaultItemPadding,
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( if (widget.showItems) ...[
height: widget.style.betweenDefaultItemPadding, Form(
), key: _formKey,
], child: child,
if (widget.showItems) Form(key: _formKey, child: child), ),
if (widget.changePasswordConfig.enablePasswordChange) ...[ ],
Expanded( if (widget.changePasswordConfig.enablePasswordChange) ...[
child: ChangePassword( ChangePassword(
config: widget.changePasswordConfig, config: widget.changePasswordConfig,
service: widget.service, service: widget.service,
wrapViewOptions: widget.wrapViewOptions, wrapViewOptions: widget.wrapViewOptions,
@ -248,42 +253,35 @@ class _ProfileWrapperState extends State<ProfileWrapper> {
itemBuilderOptions: widget.itemBuilderOptions, itemBuilderOptions: widget.itemBuilderOptions,
style: widget.style, 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 /// This calls onSaved on all the fiels which check if they have a new value
void submitAllChangedFields() { void submitAllChangedFields() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState!.save(); _formKey.currentState!.save();
} }
} }

View file

@ -1,6 +1,6 @@
name: flutter_profile name: flutter_profile
description: Flutter profile package description: Flutter profile package
version: 1.2.1 version: 1.3.0
repository: https://github.com/Iconica-Development/flutter_profile repository: https://github.com/Iconica-Development/flutter_profile
publish_to: none publish_to: none
@ -15,7 +15,7 @@ dependencies:
flutter_input_library: flutter_input_library:
git: git:
url: https://github.com/Iconica-Development/flutter_input_library url: https://github.com/Iconica-Development/flutter_input_library
ref: 2.7.0 ref: 3.1.0
flutter: flutter:
sdk: flutter sdk: flutter

View file

@ -28,5 +28,10 @@ class TestProfileService extends ProfileService {
}) {} }) {}
@override @override
FutureOr<void> changePassword(String password) {} FutureOr<bool> changePassword(
BuildContext context,
String currentPassword,
String newPassword,
) =>
true;
} }