diff --git a/lib/src/config/login_options.dart b/lib/src/config/login_options.dart index 60ecd72..75b13dc 100644 --- a/lib/src/config/login_options.dart +++ b/lib/src/config/login_options.dart @@ -10,18 +10,57 @@ import 'package:flutter_login/src/service/validation.dart'; class LoginOptions { const LoginOptions({ this.image, - this.title, + this.title = const Text( + 'Log in', + style: TextStyle( + color: Color(0xff71C6D1), + fontWeight: FontWeight.w800, + fontSize: 24, + ), + ), this.subtitle, this.maxFormWidth, this.emailTextStyle, this.passwordTextStyle, this.emailTextAlign, this.passwordTextAlign, - this.emailDecoration = const InputDecoration(), - this.passwordDecoration = const InputDecoration(), + this.emailDecoration = const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 8), + labelText: 'Email address', + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xff71C6D1), + ), + ), + labelStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 16, + ), + ), + this.passwordDecoration = const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 8), + labelText: 'Password', + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xff71C6D1), + ), + ), + labelStyle: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 16, + ), + ), this.initialEmail = '', this.initialPassword = '', - this.spacers = const LoginSpacerOptions(), + this.spacers = const LoginSpacerOptions( + spacerBeforeTitle: 8, + spacerAfterTitle: 2, + formFlexValue: 2, + ), this.translations = const LoginTranslations(), this.validationService, this.loginButtonBuilder = _createLoginButton, @@ -32,137 +71,18 @@ class LoginOptions { this.emailInputContainerBuilder = _createEmailInputContainer, this.passwordInputContainerBuilder = _createPasswordInputContainer, this.showObscurePassword = true, - this.forgotPasswordSpacerOptions = const ForgotPasswordSpacerOptions(), + this.forgotPasswordSpacerOptions = const ForgotPasswordSpacerOptions( + spacerAfterButton: 3, + spacerBeforeTitle: 1, + ), + this.loginBackgroundColor = const Color(0xffFAF9F6), + this.forgotPasswordBackgroundColor = const Color(0xffFAF9F6), + this.forgotPasswordScreenPadding = const Padding( + padding: EdgeInsets.symmetric(horizontal: 60), + ), + this.forgotPasswordCustomAppBar, }); - factory LoginOptions.defaults() => LoginOptions( - spacers: const LoginSpacerOptions( - spacerBeforeTitle: 8, - spacerAfterTitle: 2, - formFlexValue: 2, - ), - title: const Text( - 'Log in', - style: TextStyle( - color: Color(0xff71C6D1), - fontWeight: FontWeight.w800, - fontSize: 24, - ), - ), - emailDecoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 8), - labelText: 'Email address', - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xff71C6D1), - ), - ), - labelStyle: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 16, - ), - ), - passwordDecoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 8), - labelText: 'Password', - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xff71C6D1), - ), - ), - labelStyle: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 16, - ), - ), - loginButtonBuilder: - (context, onPressed, isDisabled, onDisabledPress, options) => - InkWell( - onTap: () async => onPressed.call(), - child: Container( - height: 44, - width: 254, - decoration: const BoxDecoration( - color: Color(0xff71C6D1), - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - child: const Center( - child: Text( - 'Log in', - style: TextStyle( - fontWeight: FontWeight.w800, - fontSize: 20, - color: Colors.white, - ), - ), - ), - ), - ), - registrationButtonBuilder: - (context, onPressed, isDisabled, onDisabledPress, options) => - TextButton( - onPressed: () async { - await onPressed.call(); - }, - child: const Text( - 'Create account', - style: TextStyle( - decoration: TextDecoration.underline, - decorationColor: Color(0xff71C6D1), - color: Color(0xff71C6D1), - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ), - forgotPasswordButtonBuilder: - (context, onPressed, isDisabled, onDisabledPress, options) => - TextButton( - onPressed: () async { - await onPressed.call(); - }, - child: const Text( - 'Forgot password?', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xff8D8D8D), - ), - ), - ), - forgotPasswordSpacerOptions: const ForgotPasswordSpacerOptions( - spacerAfterButton: 3, - spacerBeforeTitle: 1, - ), - requestForgotPasswordButtonBuilder: - (context, onPressed, isDisabled, onDisabledPress, options) => - InkWell( - onTap: onPressed, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xff71C6D1), - ), - height: 44, - width: 254, - child: const Center( - child: Text( - 'Send link', - style: TextStyle( - fontWeight: FontWeight.w800, - fontSize: 20, - color: Colors.white, - ), - ), - ), - ), - ), - ); - /// Builds the login button. final ButtonBuilder loginButtonBuilder; @@ -236,6 +156,18 @@ class LoginOptions { /// Get validations. ValidationService get validations => validationService ?? LoginValidationService(this); + + /// The background color for the login screen. + final Color loginBackgroundColor; + + /// The background color for the forgot password screen. + final Color forgotPasswordBackgroundColor; + + /// The padding for the forgot password screen. + final Padding forgotPasswordScreenPadding; + + /// forgot password custom AppBar + final AppBar? forgotPasswordCustomAppBar; } class LoginTranslations { @@ -245,7 +177,7 @@ class LoginTranslations { this.emailInvalid = 'Enter a valid email address', this.loginButton = 'Login', this.forgotPasswordButton = 'Forgot password?', - this.requestForgotPasswordButton = 'Send request', + this.requestForgotPasswordButton = 'Send link', this.registrationButton = 'Create Account', }); @@ -274,9 +206,27 @@ Widget _createLoginButton( ) => Opacity( opacity: disabled ? 0.5 : 1.0, - child: ElevatedButton( - onPressed: !disabled ? onPressed : onDisabledPress, - child: Text(options.translations.loginButton), + child: InkWell( + onTap: () async => + !disabled ? await onPressed() : await onDisabledPress(), + child: Container( + height: 44, + width: 254, + decoration: const BoxDecoration( + color: Color(0xff71C6D1), + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + child: Center( + child: Text( + options.translations.loginButton, + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 20, + color: Colors.white, + ), + ), + ), + ), ), ); @@ -289,9 +239,18 @@ Widget _createForgotPasswordButton( ) => Opacity( opacity: disabled ? 0.5 : 1.0, - child: ElevatedButton( + child: TextButton( onPressed: !disabled ? onPressed : onDisabledPress, - child: Text(options.translations.forgotPasswordButton), + child: Text( + options.translations.forgotPasswordButton, + style: const TextStyle( + decoration: TextDecoration.underline, + decorationColor: Color(0xff8D8D8D), + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xff8D8D8D), + ), + ), ), ); @@ -304,9 +263,26 @@ Widget _createRequestForgotPasswordButton( ) => Opacity( opacity: disabled ? 0.5 : 1.0, - child: ElevatedButton( - onPressed: !disabled ? onPressed : onDisabledPress, - child: Text(options.translations.requestForgotPasswordButton), + child: InkWell( + onTap: !disabled ? onPressed : onDisabledPress, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: const Color(0xff71C6D1), + ), + height: 44, + width: 254, + child: Center( + child: Text( + options.translations.requestForgotPasswordButton, + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 20, + color: Colors.white, + ), + ), + ), + ), ), ); @@ -319,9 +295,18 @@ Widget _createRegisterButton( ) => Opacity( opacity: disabled ? 0.5 : 1.0, - child: ElevatedButton( + child: TextButton( onPressed: !disabled ? onPressed : onDisabledPress, - child: Text(options.translations.registrationButton), + child: Text( + options.translations.registrationButton, + style: const TextStyle( + decoration: TextDecoration.underline, + decorationColor: Color(0xff71C6D1), + color: Color(0xff71C6D1), + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), ), ); diff --git a/lib/src/widgets/email_password_login.dart b/lib/src/widgets/email_password_login.dart index c66f560..e88c908 100644 --- a/lib/src/widgets/email_password_login.dart +++ b/lib/src/widgets/email_password_login.dart @@ -84,171 +84,176 @@ class _EmailPasswordLoginFormState extends State { Widget build(BuildContext context) { var theme = Theme.of(context); var options = widget.options; - return CustomScrollView( - physics: const ScrollPhysics(), - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - fillOverscroll: true, - child: Column( - children: [ - Expanded( - flex: options.spacers.titleSpacer, - child: Column( - children: [ - if (options.spacers.spacerBeforeTitle != null) ...[ - Spacer(flex: options.spacers.spacerBeforeTitle!), - ], - if (options.title != null) ...[ - Align( - alignment: Alignment.topCenter, - child: wrapWithDefaultStyle( - options.title, - theme.textTheme.headlineSmall, - ), - ), - ], - if (options.spacers.spacerAfterTitle != null) ...[ - Spacer(flex: options.spacers.spacerAfterTitle!), - ], - if (options.subtitle != null) ...[ - Align( - alignment: Alignment.topCenter, - child: wrapWithDefaultStyle( - options.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!), - ], - ], - ), - ), - Expanded( - flex: options.spacers.formFlexValue, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: options.maxFormWidth ?? 300, - ), - 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, + 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: Column( + children: [ + if (options.spacers.spacerBeforeTitle != null) ...[ + Spacer(flex: options.spacers.spacerBeforeTitle!), + ], + if (options.title != null) ...[ + Align( + alignment: Alignment.topCenter, + child: wrapWithDefaultStyle( + options.title, + theme.textTheme.headlineSmall, ), ), - 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( - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - icon: Icon( - _obscurePassword - ? Icons.visibility - : Icons.visibility_off, - ), - ) - : null, + ], + if (options.spacers.spacerAfterTitle != null) ...[ + Spacer(flex: options.spacers.spacerAfterTitle!), + ], + if (options.subtitle != null) ...[ + Align( + alignment: Alignment.topCenter, + child: wrapWithDefaultStyle( + options.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!), + ], + ], + ), + ), + Expanded( + flex: options.spacers.formFlexValue, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: options.maxFormWidth ?? 300, + ), + 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, ), ), - ), - if (widget.onForgotPassword != null) ...[ - Align( - alignment: Alignment.topRight, - child: options.forgotPasswordButtonBuilder( + 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( + onPressed: () { + setState(() { + _obscurePassword = + !_obscurePassword; + }); + }, + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, + ), + ) + : 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 0bc594c..b7fea32 100644 --- a/lib/src/widgets/forgot_password_form.dart +++ b/lib/src/widgets/forgot_password_form.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_login/flutter_login.dart'; +// ignore: must_be_immutable class ForgotPasswordForm extends StatefulWidget { /// Constructs a [ForgotPasswordForm] widget. /// @@ -11,18 +12,44 @@ class ForgotPasswordForm extends StatefulWidget { /// [onRequestForgotPassword]: Callback function for requesting /// password reset. /// [title]: Widget to display title. - const ForgotPasswordForm({ + ForgotPasswordForm({ required this.options, - required this.description, required this.onRequestForgotPassword, + this.description, super.key, this.title, - }); + }) { + title == null + ? title = const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + 'Forgot Password', + style: TextStyle( + color: Color(0xff71C6D1), + fontWeight: FontWeight.w800, + fontSize: 24, + ), + ), + ) + : title = title; + + description == null + ? description = const Padding( + padding: EdgeInsets.only(bottom: 16), + child: Text( + 'No worries. Enter your email address below so we can' + ' send you a link to reset your password.', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.w400, fontSize: 16), + ), + ) + : description = description; + } final LoginOptions options; - final Widget? title; - final Widget description; + Widget? title; + Widget? description; final FutureOr Function(String email) onRequestForgotPassword; @@ -69,109 +96,126 @@ class _ForgotPasswordFormState extends State { var theme = Theme.of(context); var options = widget.options; - return CustomScrollView( - physics: const ScrollPhysics(), - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - fillOverscroll: true, - child: Column( - children: [ - if (options.forgotPasswordSpacerOptions.spacerBeforeTitle != - null) ...[ - Spacer( - flex: options.forgotPasswordSpacerOptions.spacerBeforeTitle!, - ), - ], - Align( - alignment: Alignment.topCenter, - child: wrapWithDefaultStyle( - widget.title, - theme.textTheme.displaySmall, - ), - ), - if (options.forgotPasswordSpacerOptions.spacerAfterTitle != - null) ...[ - Spacer( - flex: options.forgotPasswordSpacerOptions.spacerAfterTitle!, - ), - ], - Align( - alignment: Alignment.topCenter, - child: wrapWithDefaultStyle( - widget.description, - theme.textTheme.bodyMedium, - ), - ), - if (options.forgotPasswordSpacerOptions.spacerAfterDescription != - null) ...[ - Spacer( - flex: options - .forgotPasswordSpacerOptions.spacerAfterDescription!, - ), - ], - Expanded( - flex: options.forgotPasswordSpacerOptions.formFlexValue, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: options.maxFormWidth ?? 300, + return Scaffold( + backgroundColor: options.forgotPasswordBackgroundColor, + appBar: widget.options.forgotPasswordCustomAppBar ?? + AppBar( + backgroundColor: const Color(0xffFAF9F6), + ), + body: Padding( + padding: options.forgotPasswordScreenPadding.padding, + child: CustomScrollView( + physics: const ScrollPhysics(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Column( + children: [ + if (options.forgotPasswordSpacerOptions.spacerBeforeTitle != + null) ...[ + Spacer( + flex: options + .forgotPasswordSpacerOptions.spacerBeforeTitle!, + ), + ], + Align( + alignment: Alignment.topCenter, + child: wrapWithDefaultStyle( + widget.title, + theme.textTheme.displaySmall, + ), ), - 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, + if (options.forgotPasswordSpacerOptions.spacerAfterTitle != + null) ...[ + Spacer( + flex: + options.forgotPasswordSpacerOptions.spacerAfterTitle!, + ), + ], + Align( + alignment: Alignment.topCenter, + child: wrapWithDefaultStyle( + widget.description, + theme.textTheme.bodyMedium, + ), + ), + if (options + .forgotPasswordSpacerOptions.spacerAfterDescription != + null) ...[ + Spacer( + flex: options + .forgotPasswordSpacerOptions.spacerAfterDescription!, + ), + ], + Expanded( + flex: options.forgotPasswordSpacerOptions.formFlexValue, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: options.maxFormWidth ?? 300, + ), + 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, + ), + ), ), ), ), ), - ), - ), - if (options.forgotPasswordSpacerOptions.spacerBeforeButton != - null) ...[ - Spacer( - flex: options.forgotPasswordSpacerOptions.spacerBeforeButton!, - ), - ], - AnimatedBuilder( - animation: _formValid, - builder: (context, snapshot) => Align( - child: widget.options.requestForgotPasswordButtonBuilder( - context, - () async { - _formKey.currentState?.validate(); - if (_formValid.value) { - widget.onRequestForgotPassword(_currentEmail); - } - }, - !_formValid.value, - () { - _formKey.currentState?.validate(); - }, - options, + if (options.forgotPasswordSpacerOptions.spacerBeforeButton != + null) ...[ + Spacer( + flex: options + .forgotPasswordSpacerOptions.spacerBeforeButton!, + ), + ], + AnimatedBuilder( + animation: _formValid, + builder: (context, snapshot) => Align( + child: widget.options.requestForgotPasswordButtonBuilder( + context, + () async { + _formKey.currentState?.validate(); + if (_formValid.value) { + widget.onRequestForgotPassword(_currentEmail); + } + }, + !_formValid.value, + () { + _formKey.currentState?.validate(); + }, + options, + ), + ), ), - ), + if (options.forgotPasswordSpacerOptions.spacerAfterButton != + null) ...[ + Spacer( + flex: options + .forgotPasswordSpacerOptions.spacerAfterButton!, + ), + ], + ], ), - if (options.forgotPasswordSpacerOptions.spacerAfterButton != - null) ...[ - Spacer( - flex: options.forgotPasswordSpacerOptions.spacerAfterButton!, - ), - ], - ], - ), + ), + ], ), - ], + ), ); } }