mirror of
https://github.com/Iconica-Development/flutter_login_widget.git
synced 2025-05-19 13:43:44 +02:00
add forgot password form
This commit is contained in:
parent
015a6d5f14
commit
c84ce5db22
7 changed files with 280 additions and 40 deletions
|
@ -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');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
28
lib/src/service/login_validation_.dart
Normal file
28
lib/src/service/login_validation_.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
lib/src/service/validation.dart
Normal file
7
lib/src/service/validation.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
abstract class ValidationService {
|
||||||
|
const ValidationService._();
|
||||||
|
|
||||||
|
String? validateEmail(String? value);
|
||||||
|
|
||||||
|
String? validatePassword(String? value);
|
||||||
|
}
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
148
lib/src/widgets/forgot_password_form.dart
Normal file
148
lib/src/widgets/forgot_password_form.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue