import "dart:async"; import "package:flutter/material.dart"; import "package:flutter_login/flutter_login.dart"; class EmailPasswordLoginForm extends StatefulWidget { /// Constructs an [EmailPasswordLoginForm] widget. /// /// [title]: The title to display above the form. /// [subtitle]: A subtitle to display below the title. /// [onLogin]: Callback function for user login. /// [onForgotPassword]: Callback function for when the user /// forgets their password. /// [onRegister]: Callback function for user registration. /// [options]: The options for configuring the login form. const EmailPasswordLoginForm({ required this.onLogin, this.title, this.subtitle, this.onForgotPassword, this.onRegister, this.options = const LoginOptions(), super.key, }); final LoginOptions options; final Widget? title; final Widget? subtitle; final void Function(String email, BuildContext ctx)? onForgotPassword; final FutureOr Function( String email, String password, BuildContext context, )? onRegister; final FutureOr Function(String email, String password) onLogin; @override State createState() => _EmailPasswordLoginFormState(); } class _EmailPasswordLoginFormState extends State { final _formKey = GlobalKey(); final ValueNotifier _formValid = ValueNotifier(false); bool _obscurePassword = true; String _currentEmail = ""; String _currentPassword = ""; void _updateCurrentEmail(String email) { _currentEmail = email; _validate(); } void _updateCurrentPassword(String password) { _currentPassword = password; _validate(); } void _validate() { late var isValid = widget.options.validations.validateEmail(_currentEmail) == null && widget.options.validations.validatePassword(_currentPassword) == null; if (isValid != _formValid.value) { _formValid.value = isValid; } } Future _handleLogin() async { if (mounted) { var form = _formKey.currentState!; if (form.validate()) { await widget.onLogin( _currentEmail, _currentPassword, ); } } } @override void initState() { super.initState(); _currentEmail = widget.options.initialEmail; _currentPassword = widget.options.initialPassword; _validate(); } @override Widget build(BuildContext context) { var options = widget.options; var emailTextFormField = TextFormField( autofillHints: const [ AutofillHints.email, AutofillHints.username, ], textAlign: options.emailTextAlign ?? TextAlign.start, onChanged: _updateCurrentEmail, validator: widget.options.validations.validateEmail, initialValue: options.initialEmail, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, style: options.emailTextStyle, decoration: options.emailDecoration, ); var passwordTextFormField = TextFormField( autofillHints: const [ AutofillHints.password, ], textAlign: options.passwordTextAlign ?? TextAlign.start, obscureText: _obscurePassword, onChanged: _updateCurrentPassword, validator: widget.options.validations.validatePassword, initialValue: options.initialPassword, keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.done, style: options.passwordTextStyle, onFieldSubmitted: (_) async => _handleLogin(), decoration: options.passwordDecoration.copyWith( suffixIcon: options.showObscurePassword ? IconButton( padding: options.suffixIconPadding, onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, icon: Icon( _obscurePassword ? Icons.visibility : Icons.visibility_off, size: options.suffixIconSize, ), ) : null, ), ); var forgotPasswordButton = widget.onForgotPassword != null ? Align( alignment: Alignment.topRight, child: options.forgotPasswordButtonBuilder( context, () => widget.onForgotPassword?.call(_currentEmail, context), false, () {}, options, ), ) : const SizedBox(height: 16); var loginButton = AnimatedBuilder( animation: _formValid, builder: (context, _) => options.loginButtonBuilder( context, _handleLogin, !_formValid.value, () { _formKey.currentState?.validate(); }, options, ), ); var registerButton = options.registrationButtonBuilder( context, () async { widget.onRegister?.call( _currentEmail, _currentPassword, context, ); }, false, () {}, options, ); return Scaffold( backgroundColor: options.loginBackgroundColor, body: CustomScrollView( physics: const ScrollPhysics(), slivers: [ SliverFillRemaining( hasScrollBody: false, fillOverscroll: true, child: Column( children: [ Expanded( flex: options.spacers.titleSpacer, child: LoginTitle( options: options, title: widget.title, subtitle: widget.subtitle, ), ), Expanded( flex: options.spacers.formFlexValue, child: ConstrainedBox( constraints: BoxConstraints( maxWidth: options.maxFormWidth ?? 300, ), child: Form( key: _formKey, child: AutofillGroup( child: Column( mainAxisSize: MainAxisSize.min, children: [ options.emailInputContainerBuilder( emailTextFormField, ), options.passwordInputContainerBuilder( passwordTextFormField, ), forgotPasswordButton, if (options.spacers.spacerAfterForm != null) ...[ Spacer(flex: options.spacers.spacerAfterForm!), ], loginButton, if (widget.onRegister != null) ...[ registerButton, ], if (options.spacers.spacerAfterButton != null) ...[ Spacer(flex: options.spacers.spacerAfterButton!), ], ], ), ), ), ), ), ], ), ), ], ), ); } } class LoginTitle extends StatelessWidget { const LoginTitle({ required this.options, this.title, this.subtitle, super.key, }); final LoginOptions options; final Widget? title; final Widget? subtitle; @override Widget build(BuildContext context) { var theme = Theme.of(context); return Column( children: [ if (options.spacers.spacerBeforeTitle != null) ...[ Spacer(flex: options.spacers.spacerBeforeTitle!), ], if (title != null) ...[ Align( alignment: Alignment.topCenter, child: wrapWithDefaultStyle( title, theme.textTheme.headlineSmall, ), ), ], if (options.spacers.spacerAfterTitle != null) ...[ Spacer(flex: options.spacers.spacerAfterTitle!), ], if (subtitle != null) ...[ Align( alignment: Alignment.topCenter, child: wrapWithDefaultStyle( subtitle, theme.textTheme.titleSmall, ), ), ], if (options.spacers.spacerAfterSubtitle != null) ...[ Spacer(flex: options.spacers.spacerAfterSubtitle!), ], if (options.image != null) ...[ Padding( padding: const EdgeInsets.all(16), child: options.image, ), ], if (options.spacers.spacerAfterImage != null) ...[ Spacer(flex: options.spacers.spacerAfterImage!), ], ], ); } } Widget? wrapWithDefaultStyle(Widget? widget, TextStyle? style) { if (style == null || widget == null) { return widget; } else { return DefaultTextStyle(style: style, child: widget); } }