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_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(
home: LoginScreen(),
);
}
}
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: EmailPasswordLoginForm(
options: LoginOptions(
decoration: InputDecoration(
border: OutlineInputBorder(),
),
emailInputPrefix: Icon(Icons.email),
passwordInputPrefix: Icon(Icons.password),
title: Text('Login'),
image: FlutterLogo(),
),
// ignore: avoid_print
options: loginOptions,
onLogin: (email, password) => print('$email:$password'),
onRegister: (email, password) => print('Register!'),
onForgotPassword: () {},
onForgotPassword: (email) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return const ForgotPasswordScreen();
},
),
);
},
),
);
}
}
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');
},
),
);
}

View file

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

View file

@ -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,

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 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) onLogin;
@ -40,32 +40,15 @@ class _EmailPasswordLoginFormState extends State<EmailPasswordLoginForm> {
}
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<void> _handleLogin() async {
if (mounted) {
var form = _formKey.currentState!;
@ -128,7 +111,7 @@ class _EmailPasswordLoginFormState extends State<EmailPasswordLoginForm> {
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<EmailPasswordLoginForm> {
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<EmailPasswordLoginForm> {
child: options.forgotPasswordButtonBuilder(
context,
() {
widget.onForgotPassword?.call();
widget.onForgotPassword?.call(_currentEmail);
},
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);
}
}
}