From bb65426298db27820dd14c34b4a3403a8c61ad4b Mon Sep 17 00:00:00 2001 From: Tobias Leijs Date: Tue, 25 Jun 2024 15:09:04 +0200 Subject: [PATCH 1/3] feat: add AutoFillGroup to support native password managers --- lib/src/widgets/email_password_login.dart | 194 ++++++++++++---------- lib/src/widgets/forgot_password_form.dart | 33 ++-- 2 files changed, 120 insertions(+), 107 deletions(-) diff --git a/lib/src/widgets/email_password_login.dart b/lib/src/widgets/email_password_login.dart index 8fca6e5..eb0e43e 100644 --- a/lib/src/widgets/email_password_login.dart +++ b/lib/src/widgets/email_password_login.dart @@ -153,108 +153,118 @@ class _EmailPasswordLoginFormState extends State { ), child: Form( key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - options.emailInputContainerBuilder( - TextFormField( - 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, - ), - ), - options.passwordInputContainerBuilder( - TextFormField( - 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, + child: AutofillGroup( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + options.emailInputContainerBuilder( + 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, ), ), - ), - if (widget.onForgotPassword != null) ...[ - Align( - alignment: Alignment.topRight, - child: options.forgotPasswordButtonBuilder( + options.passwordInputContainerBuilder( + 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, + ), + ), + ), + if (widget.onForgotPassword != null) ...[ + Align( + alignment: Alignment.topRight, + child: options.forgotPasswordButtonBuilder( + context, + () { + widget.onForgotPassword + ?.call(_currentEmail, context); + }, + false, + () {}, + options, + ), + ), + ] else ...[ + const SizedBox(height: 16), + ], + if (options.spacers.spacerAfterForm != null) ...[ + Spacer(flex: options.spacers.spacerAfterForm!), + ], + AnimatedBuilder( + animation: _formValid, + builder: (context, _) => + options.loginButtonBuilder( context, + _handleLogin, + !_formValid.value, () { - widget.onForgotPassword - ?.call(_currentEmail, context); + _formKey.currentState?.validate(); + }, + options, + ), + ), + if (widget.onRegister != null) ...[ + options.registrationButtonBuilder( + context, + () async { + widget.onRegister?.call( + _currentEmail, + _currentPassword, + context, + ); }, false, () {}, options, ), - ), - ] else ...[ - const SizedBox(height: 16), + ], + if (options.spacers.spacerAfterButton != null) ...[ + Spacer(flex: options.spacers.spacerAfterButton!), + ], ], - if (options.spacers.spacerAfterForm != null) ...[ - Spacer(flex: options.spacers.spacerAfterForm!), - ], - AnimatedBuilder( - animation: _formValid, - builder: (context, _) => options.loginButtonBuilder( - context, - _handleLogin, - !_formValid.value, - () { - _formKey.currentState?.validate(); - }, - options, - ), - ), - if (widget.onRegister != null) ...[ - options.registrationButtonBuilder( - context, - () async { - widget.onRegister?.call( - _currentEmail, - _currentPassword, - context, - ); - }, - false, - () {}, - options, - ), - ], - if (options.spacers.spacerAfterButton != null) ...[ - Spacer(flex: options.spacers.spacerAfterButton!), - ], - ], + ), ), ), ), diff --git a/lib/src/widgets/forgot_password_form.dart b/lib/src/widgets/forgot_password_form.dart index b848594..3928fd5 100644 --- a/lib/src/widgets/forgot_password_form.dart +++ b/lib/src/widgets/forgot_password_form.dart @@ -129,21 +129,24 @@ class _ForgotPasswordFormState extends State { ), child: Form( key: _formKey, - child: Align( - alignment: Alignment.center, - child: options.emailInputContainerBuilder( - TextFormField( - textAlign: - options.emailTextAlign ?? TextAlign.start, - focusNode: _focusNode, - onChanged: _updateCurrentEmail, - validator: - widget.options.validations.validateEmail, - initialValue: options.initialEmail, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.next, - style: options.emailTextStyle, - decoration: options.emailDecoration, + child: AutofillGroup( + child: Align( + alignment: Alignment.center, + child: options.emailInputContainerBuilder( + TextFormField( + autofillHints: const [AutofillHints.email], + textAlign: + options.emailTextAlign ?? TextAlign.start, + focusNode: _focusNode, + onChanged: _updateCurrentEmail, + validator: + widget.options.validations.validateEmail, + initialValue: options.initialEmail, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + style: options.emailTextStyle, + decoration: options.emailDecoration, + ), ), ), ), From b57b238794a6fe30dc392404997f823d80453b2a Mon Sep 17 00:00:00 2001 From: Tobias Leijs Date: Tue, 25 Jun 2024 15:34:04 +0200 Subject: [PATCH 2/3] refactor: extract variables and widgets to shorten the widget tree --- lib/src/widgets/email_password_login.dart | 288 ++++++++++++---------- 1 file changed, 154 insertions(+), 134 deletions(-) diff --git a/lib/src/widgets/email_password_login.dart b/lib/src/widgets/email_password_login.dart index eb0e43e..07ee4dd 100644 --- a/lib/src/widgets/email_password_login.dart +++ b/lib/src/widgets/email_password_login.dart @@ -90,8 +90,94 @@ class _EmailPasswordLoginFormState extends State { @override Widget build(BuildContext context) { - var theme = Theme.of(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( @@ -104,45 +190,10 @@ class _EmailPasswordLoginFormState extends State { children: [ Expanded( flex: options.spacers.titleSpacer, - child: Column( - children: [ - if (options.spacers.spacerBeforeTitle != null) ...[ - Spacer(flex: options.spacers.spacerBeforeTitle!), - ], - if (widget.title != null) ...[ - Align( - alignment: Alignment.topCenter, - child: wrapWithDefaultStyle( - widget.title, - theme.textTheme.headlineSmall, - ), - ), - ], - if (options.spacers.spacerAfterTitle != null) ...[ - Spacer(flex: options.spacers.spacerAfterTitle!), - ], - if (widget.subtitle != null) ...[ - Align( - alignment: Alignment.topCenter, - child: wrapWithDefaultStyle( - widget.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!), - ], - ], + child: LoginTitle( + options: options, + title: widget.title, + subtitle: widget.subtitle, ), ), Expanded( @@ -158,107 +209,18 @@ class _EmailPasswordLoginFormState extends State { mainAxisSize: MainAxisSize.min, children: [ options.emailInputContainerBuilder( - 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, - ), + emailTextFormField, ), options.passwordInputContainerBuilder( - 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, - ), - ), + passwordTextFormField, ), - if (widget.onForgotPassword != null) ...[ - Align( - alignment: Alignment.topRight, - child: options.forgotPasswordButtonBuilder( - context, - () { - widget.onForgotPassword - ?.call(_currentEmail, context); - }, - false, - () {}, - options, - ), - ), - ] else ...[ - const SizedBox(height: 16), - ], + forgotPasswordButton, if (options.spacers.spacerAfterForm != null) ...[ Spacer(flex: options.spacers.spacerAfterForm!), ], - AnimatedBuilder( - animation: _formValid, - builder: (context, _) => - options.loginButtonBuilder( - context, - _handleLogin, - !_formValid.value, - () { - _formKey.currentState?.validate(); - }, - options, - ), - ), + loginButton, if (widget.onRegister != null) ...[ - options.registrationButtonBuilder( - context, - () async { - widget.onRegister?.call( - _currentEmail, - _currentPassword, - context, - ); - }, - false, - () {}, - options, - ), + registerButton, ], if (options.spacers.spacerAfterButton != null) ...[ Spacer(flex: options.spacers.spacerAfterButton!), @@ -278,6 +240,64 @@ class _EmailPasswordLoginFormState extends State { } } +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; From 0ebb84b7b69456e970f2e198962fd9db8d51fbcf Mon Sep 17 00:00:00 2001 From: Tobias Leijs Date: Tue, 25 Jun 2024 15:35:15 +0200 Subject: [PATCH 3/3] chore: bump the version number to 7.1.0 --- CHANGELOG.md | 5 +++++ example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 539bbfe..e5113cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 7.1.0 + +* Added autofillgroup to support native password managers + + ## 7.0.0 * Removed `title` and `subtitle` parameters from `LoginOptions` in favour of passing them directly to the `EmailPasswordLoginForm` widget directly diff --git a/example/pubspec.lock b/example/pubspec.lock index 77b8d3e..5382d60 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -68,7 +68,7 @@ packages: path: ".." relative: true source: path - version: "6.1.0" + version: "7.1.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 112a182..3bf7b7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_login description: Flutter Login Component -version: 7.0.0 +version: 7.1.0 environment: sdk: ">=2.18.1 <3.0.0"