add forgot password form

This commit is contained in:
Joey Boerwinkel 2022-09-29 17:22:26 +02:00
parent 015a6d5f14
commit c84ce5db22
7 changed files with 280 additions and 40 deletions

View file

@ -1,6 +1,27 @@
// ignore_for_file: avoid_print
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_login/flutter_login.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() { void main() {
runApp(const LoginExample()); runApp(const LoginExample());
} }
@ -12,22 +33,49 @@ class LoginExample extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
theme: ThemeData.dark(), theme: ThemeData.dark(),
home: Scaffold( home: LoginScreen(),
body: EmailPasswordLoginForm( );
options: LoginOptions( }
decoration: InputDecoration( }
border: OutlineInputBorder(),
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!'), class ForgotPasswordScreen extends StatelessWidget {
onForgotPassword: () {}, 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');
},
), ),
); );
} }

View file

@ -2,3 +2,5 @@ library flutter_login;
export 'src/config/login_options.dart'; export 'src/config/login_options.dart';
export 'src/widgets/email_password_login.dart'; export 'src/widgets/email_password_login.dart';
export 'src/widgets/forgot_password_form.dart';
export 'src/service/validation.dart';

View file

@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_login/src/service/login_validation_.dart';
import 'package:flutter_login/src/service/validation.dart';
class LoginOptions { class LoginOptions {
const LoginOptions({ const LoginOptions({
@ -15,8 +17,11 @@ class LoginOptions {
this.initialEmail = '', this.initialEmail = '',
this.initialPassword = '', this.initialPassword = '',
this.translations = const LoginTranslations(), this.translations = const LoginTranslations(),
this.validationService,
this.loginButtonBuilder = _createLoginButton, this.loginButtonBuilder = _createLoginButton,
this.forgotPasswordButtonBuilder = _createForgotPasswordButton, this.forgotPasswordButtonBuilder = _createForgotPasswordButton,
this.requestForgotPasswordButtonBuilder =
_createRequestForgotPasswordButton,
this.registrationButtonBuilder = _createRegisterButton, this.registrationButtonBuilder = _createRegisterButton,
this.emailInputContainerBuilder = _createEmailInputContainer, this.emailInputContainerBuilder = _createEmailInputContainer,
this.passwordInputContainerBuilder = _createPasswordInputContainer, this.passwordInputContainerBuilder = _createPasswordInputContainer,
@ -25,6 +30,7 @@ class LoginOptions {
final ButtonBuilder loginButtonBuilder; final ButtonBuilder loginButtonBuilder;
final ButtonBuilder registrationButtonBuilder; final ButtonBuilder registrationButtonBuilder;
final ButtonBuilder forgotPasswordButtonBuilder; final ButtonBuilder forgotPasswordButtonBuilder;
final ButtonBuilder requestForgotPasswordButtonBuilder;
final InputContainerBuilder emailInputContainerBuilder; final InputContainerBuilder emailInputContainerBuilder;
final InputContainerBuilder passwordInputContainerBuilder; final InputContainerBuilder passwordInputContainerBuilder;
@ -39,6 +45,10 @@ class LoginOptions {
final String initialEmail; final String initialEmail;
final String initialPassword; final String initialPassword;
final LoginTranslations translations; final LoginTranslations translations;
final ValidationService? validationService;
ValidationService get validations =>
validationService ?? LoginValidationService(this);
} }
class LoginTranslations { 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( Widget _createRegisterButton(
BuildContext context, BuildContext context,
OptionalAsyncCallback onPressed, OptionalAsyncCallback onPressed,

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
abstract class ValidationService {
const ValidationService._();
String? validateEmail(String? value);
String? validatePassword(String? value);
}

View file

@ -13,7 +13,7 @@ class EmailPasswordLoginForm extends StatefulWidget {
}); });
final LoginOptions options; final LoginOptions options;
final VoidCallback? onForgotPassword; final void Function(String email)? onForgotPassword;
final FutureOr<void> Function(String email, String password)? onRegister; final FutureOr<void> Function(String email, String password)? onRegister;
final FutureOr<void> Function(String email, String password) onLogin; final FutureOr<void> Function(String email, String password) onLogin;
@ -40,32 +40,15 @@ class _EmailPasswordLoginFormState extends State<EmailPasswordLoginForm> {
} }
void _validate() { void _validate() {
late bool isValid = _validateEmail(_currentEmail) == null && late bool isValid =
_validatePassword(_currentPassword) == null; widget.options.validations.validateEmail(_currentEmail) == null &&
widget.options.validations.validatePassword(_currentPassword) ==
null;
if (isValid != _formValid.value) { if (isValid != _formValid.value) {
_formValid.value = isValid; _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<void> _handleLogin() async { Future<void> _handleLogin() async {
if (mounted) { if (mounted) {
var form = _formKey.currentState!; var form = _formKey.currentState!;
@ -128,7 +111,7 @@ class _EmailPasswordLoginFormState extends State<EmailPasswordLoginForm> {
options.emailInputContainerBuilder( options.emailInputContainerBuilder(
TextFormField( TextFormField(
onChanged: _updateCurrentEmail, onChanged: _updateCurrentEmail,
validator: _validateEmail, validator: widget.options.validations.validateEmail,
initialValue: options.initialEmail, initialValue: options.initialEmail,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
@ -143,7 +126,7 @@ class _EmailPasswordLoginFormState extends State<EmailPasswordLoginForm> {
TextFormField( TextFormField(
obscureText: _obscurePassword, obscureText: _obscurePassword,
onChanged: _updateCurrentPassword, onChanged: _updateCurrentPassword,
validator: _validatePassword, validator: widget.options.validations.validatePassword,
initialValue: options.initialPassword, initialValue: options.initialPassword,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
@ -173,7 +156,7 @@ class _EmailPasswordLoginFormState extends State<EmailPasswordLoginForm> {
child: options.forgotPasswordButtonBuilder( child: options.forgotPasswordButtonBuilder(
context, context,
() { () {
widget.onForgotPassword?.call(); widget.onForgotPassword?.call(_currentEmail);
}, },
false, false,
), ),

View file

@ -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<void> Function(String email) onRequestForgotPassword;
@override
State<ForgotPasswordForm> createState() => _ForgotPasswordFormState();
}
class _ForgotPasswordFormState extends State<ForgotPasswordForm> {
final GlobalKey<FormState> _formKey = GlobalKey();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_focusNode.requestFocus();
}
@override
void dispose() {
super.dispose();
_focusNode.dispose();
}
final ValueNotifier<bool> _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);
}
}
}