mirror of
https://github.com/Iconica-Development/flutter_login_widget.git
synced 2025-05-19 05:33:45 +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_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');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
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 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,
|
||||
),
|
||||
|
|
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