From c84ce5db2242a2ee731300bdd43ded8a8a9c929b Mon Sep 17 00:00:00 2001 From: Joey Boerwinkel Date: Thu, 29 Sep 2022 17:22:26 +0200 Subject: [PATCH] add forgot password form --- example/lib/main.dart | 78 +++++++++--- lib/flutter_login.dart | 2 + lib/src/config/login_options.dart | 24 ++++ lib/src/service/login_validation_.dart | 28 ++++ lib/src/service/validation.dart | 7 + lib/src/widgets/email_password_login.dart | 33 ++--- lib/src/widgets/forgot_password_form.dart | 148 ++++++++++++++++++++++ 7 files changed, 280 insertions(+), 40 deletions(-) create mode 100644 lib/src/service/login_validation_.dart create mode 100644 lib/src/service/validation.dart create mode 100644 lib/src/widgets/forgot_password_form.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e910d1..d6ec13d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,27 @@ +// ignore_for_file: avoid_print + import 'package:flutter/material.dart'; import 'package:flutter_login/flutter_login.dart'; +final loginOptions = LoginOptions( + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + emailInputPrefix: const Icon(Icons.email), + passwordInputPrefix: const Icon(Icons.password), + title: const Text('Login'), + image: const FlutterLogo(), + requestForgotPasswordButtonBuilder: (context, onPressed, isDisabled) { + return Opacity( + opacity: isDisabled ? 0.5 : 1.0, + child: ElevatedButton( + onPressed: onPressed, + child: const Text('Send request'), + ), + ); + }, +); + void main() { runApp(const LoginExample()); } @@ -12,22 +33,49 @@ class LoginExample extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( theme: ThemeData.dark(), - home: Scaffold( - body: EmailPasswordLoginForm( - options: LoginOptions( - decoration: InputDecoration( - border: OutlineInputBorder(), + home: LoginScreen(), + ); + } +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: EmailPasswordLoginForm( + options: loginOptions, + onLogin: (email, password) => print('$email:$password'), + onRegister: (email, password) => print('Register!'), + onForgotPassword: (email) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return const ForgotPasswordScreen(); + }, ), - emailInputPrefix: Icon(Icons.email), - passwordInputPrefix: Icon(Icons.password), - title: Text('Login'), - image: FlutterLogo(), - ), - // ignore: avoid_print - onLogin: (email, password) => print('$email:$password'), - onRegister: (email, password) => print('Register!'), - onForgotPassword: () {}, - ), + ); + }, + ), + ); + } +} + +class ForgotPasswordScreen extends StatelessWidget { + const ForgotPasswordScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: ForgotPasswordForm( + options: loginOptions, + title: Text('Forgot password'), + description: Text('Hello world'), + onRequestForgotPassword: (email) { + print('Forgot password email sent to $email'); + }, ), ); } diff --git a/lib/flutter_login.dart b/lib/flutter_login.dart index 6256498..4f2e0fd 100644 --- a/lib/flutter_login.dart +++ b/lib/flutter_login.dart @@ -2,3 +2,5 @@ library flutter_login; export 'src/config/login_options.dart'; export 'src/widgets/email_password_login.dart'; +export 'src/widgets/forgot_password_form.dart'; +export 'src/service/validation.dart'; diff --git a/lib/src/config/login_options.dart b/lib/src/config/login_options.dart index d8542f7..3347388 100644 --- a/lib/src/config/login_options.dart +++ b/lib/src/config/login_options.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_login/src/service/login_validation_.dart'; +import 'package:flutter_login/src/service/validation.dart'; class LoginOptions { const LoginOptions({ @@ -15,8 +17,11 @@ class LoginOptions { this.initialEmail = '', this.initialPassword = '', this.translations = const LoginTranslations(), + this.validationService, this.loginButtonBuilder = _createLoginButton, this.forgotPasswordButtonBuilder = _createForgotPasswordButton, + this.requestForgotPasswordButtonBuilder = + _createRequestForgotPasswordButton, this.registrationButtonBuilder = _createRegisterButton, this.emailInputContainerBuilder = _createEmailInputContainer, this.passwordInputContainerBuilder = _createPasswordInputContainer, @@ -25,6 +30,7 @@ class LoginOptions { final ButtonBuilder loginButtonBuilder; final ButtonBuilder registrationButtonBuilder; final ButtonBuilder forgotPasswordButtonBuilder; + final ButtonBuilder requestForgotPasswordButtonBuilder; final InputContainerBuilder emailInputContainerBuilder; final InputContainerBuilder passwordInputContainerBuilder; @@ -39,6 +45,10 @@ class LoginOptions { final String initialEmail; final String initialPassword; final LoginTranslations translations; + final ValidationService? validationService; + + ValidationService get validations => + validationService ?? LoginValidationService(this); } class LoginTranslations { @@ -85,6 +95,20 @@ Widget _createForgotPasswordButton( ); } +Widget _createRequestForgotPasswordButton( + BuildContext context, + OptionalAsyncCallback onPressed, + bool disabled, +) { + return Opacity( + opacity: disabled ? 0.5 : 1.0, + child: TextButton( + onPressed: onPressed, + child: const Text('Send request'), + ), + ); +} + Widget _createRegisterButton( BuildContext context, OptionalAsyncCallback onPressed, diff --git a/lib/src/service/login_validation_.dart b/lib/src/service/login_validation_.dart new file mode 100644 index 0000000..8062923 --- /dev/null +++ b/lib/src/service/login_validation_.dart @@ -0,0 +1,28 @@ +import 'package:flutter_login/flutter_login.dart'; + +class LoginValidationService implements ValidationService { + const LoginValidationService(this.options); + + final LoginOptions options; + + @override + String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return options.translations.emailEmpty; + } + if (!RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + .hasMatch(value)) { + return options.translations.emailInvalid; + } + return null; + } + + @override + String? validatePassword(String? value) { + if (value == null || value.isEmpty) { + return options.translations.passwordEmpty; + } + return null; + } +} diff --git a/lib/src/service/validation.dart b/lib/src/service/validation.dart new file mode 100644 index 0000000..110ff35 --- /dev/null +++ b/lib/src/service/validation.dart @@ -0,0 +1,7 @@ +abstract class ValidationService { + const ValidationService._(); + + String? validateEmail(String? value); + + String? validatePassword(String? value); +} diff --git a/lib/src/widgets/email_password_login.dart b/lib/src/widgets/email_password_login.dart index bf89a08..db3fc2a 100644 --- a/lib/src/widgets/email_password_login.dart +++ b/lib/src/widgets/email_password_login.dart @@ -13,7 +13,7 @@ class EmailPasswordLoginForm extends StatefulWidget { }); final LoginOptions options; - final VoidCallback? onForgotPassword; + final void Function(String email)? onForgotPassword; final FutureOr Function(String email, String password)? onRegister; final FutureOr Function(String email, String password) onLogin; @@ -40,32 +40,15 @@ class _EmailPasswordLoginFormState extends State { } void _validate() { - late bool isValid = _validateEmail(_currentEmail) == null && - _validatePassword(_currentPassword) == null; + late bool isValid = + widget.options.validations.validateEmail(_currentEmail) == null && + widget.options.validations.validatePassword(_currentPassword) == + null; if (isValid != _formValid.value) { _formValid.value = isValid; } } - String? _validateEmail(String? value) { - if (value == null || value.isEmpty) { - return widget.options.translations.emailEmpty; - } - if (!RegExp( - r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") - .hasMatch(value)) { - return widget.options.translations.emailInvalid; - } - return null; - } - - String? _validatePassword(String? value) { - if (value == null || value.isEmpty) { - return widget.options.translations.passwordEmpty; - } - return null; - } - Future _handleLogin() async { if (mounted) { var form = _formKey.currentState!; @@ -128,7 +111,7 @@ class _EmailPasswordLoginFormState extends State { options.emailInputContainerBuilder( TextFormField( onChanged: _updateCurrentEmail, - validator: _validateEmail, + validator: widget.options.validations.validateEmail, initialValue: options.initialEmail, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, @@ -143,7 +126,7 @@ class _EmailPasswordLoginFormState extends State { TextFormField( obscureText: _obscurePassword, onChanged: _updateCurrentPassword, - validator: _validatePassword, + validator: widget.options.validations.validatePassword, initialValue: options.initialPassword, keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.done, @@ -173,7 +156,7 @@ class _EmailPasswordLoginFormState extends State { child: options.forgotPasswordButtonBuilder( context, () { - widget.onForgotPassword?.call(); + widget.onForgotPassword?.call(_currentEmail); }, false, ), diff --git a/lib/src/widgets/forgot_password_form.dart b/lib/src/widgets/forgot_password_form.dart new file mode 100644 index 0000000..f879cd6 --- /dev/null +++ b/lib/src/widgets/forgot_password_form.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_login/flutter_login.dart'; + +class ForgotPasswordForm extends StatefulWidget { + const ForgotPasswordForm({ + super.key, + required this.options, + required this.title, + required this.description, + required this.onRequestForgotPassword, + this.initialEmail, + }); + + final LoginOptions options; + + final Widget title; + final Widget description; + final String? initialEmail; + + final FutureOr Function(String email) onRequestForgotPassword; + + @override + State createState() => _ForgotPasswordFormState(); +} + +class _ForgotPasswordFormState extends State { + final GlobalKey _formKey = GlobalKey(); + final FocusNode _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _focusNode.requestFocus(); + } + + @override + void dispose() { + super.dispose(); + _focusNode.dispose(); + } + + final ValueNotifier _formValid = ValueNotifier(false); + + String _currentEmail = ''; + + void _updateCurrentEmail(String email) { + _currentEmail = email; + _validate(); + } + + void _validate() { + late bool isValid = + widget.options.validations.validateEmail(_currentEmail) == null; + if (isValid != _formValid.value) { + _formValid.value = isValid; + } + } + + @override + Widget build(BuildContext context) { + var options = widget.options; + var theme = Theme.of(context); + return Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: _wrapWithDefaultStyle( + widget.title, + theme.textTheme.displaySmall, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: _wrapWithDefaultStyle( + widget.description, + theme.textTheme.bodyMedium, + ), + ), + Expanded( + flex: 3, + child: Align( + child: options.emailInputContainerBuilder( + Padding( + padding: const EdgeInsets.all(16), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + ), + child: TextFormField( + focusNode: _focusNode, + onChanged: _updateCurrentEmail, + validator: widget.options.validations.validateEmail, + initialValue: options.initialEmail, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + decoration: options.decoration.copyWith( + prefixIcon: options.emailInputPrefix, + label: options.emailLabel, + ), + ), + ), + ), + ), + ), + ), + Expanded( + child: AnimatedBuilder( + animation: _formValid, + builder: (context, snapshot) { + return Align( + child: widget.options.requestForgotPasswordButtonBuilder( + context, + () { + if (_formValid.value) { + widget.onRequestForgotPassword(_currentEmail); + } + }, + !_formValid.value, + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget? _wrapWithDefaultStyle(Widget? widget, TextStyle? style) { + if (style == null || widget == null) { + return widget; + } else { + return DefaultTextStyle(style: style, child: widget); + } + } +}