diff --git a/lib/features/welcome/login/login_view.dart b/lib/features/welcome/login/login_view.dart index a0142eb57..145025230 100644 --- a/lib/features/welcome/login/login_view.dart +++ b/lib/features/welcome/login/login_view.dart @@ -3,21 +3,18 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:fluttertoast/fluttertoast.dart'; +import 'package:notredame/features/welcome/widgets/forgot_password.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/features/app/integration/launch_url_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; -import 'package:notredame/features/welcome/login/login_mask.dart'; import 'package:notredame/features/welcome/login/login_viewmodel.dart'; import 'package:notredame/features/welcome/widgets/password_text_field.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/utils/locator.dart'; import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/welcome/widgets/login_hero.dart'; +import 'package:notredame/features/welcome/widgets/universal_code_text_field.dart'; +import 'package:notredame/features/welcome/widgets/login_button.dart'; +import 'package:notredame/features/welcome/widgets/login_footer.dart'; class LoginView extends StatefulWidget { @override @@ -29,19 +26,9 @@ class _LoginViewState extends State { final FocusScopeNode _focusNode = FocusScopeNode(); - final NavigationService _navigationService = locator(); - - final LaunchUrlService _launchUrlService = locator(); - - final RemoteConfigService _remoteConfigService = - locator(); - /// Unique key of the login form form final GlobalKey formKey = GlobalKey(); - /// Unique key of the tooltip - final GlobalKey tooltipkey = GlobalKey(); - @override Widget build(BuildContext context) => ViewModelBuilder.reactive( @@ -72,77 +59,15 @@ class _LoginViewState extends State { const SizedBox( height: 48, ), - Hero( - tag: 'ets_logo', - child: SvgPicture.asset( - "assets/images/ets_white_logo.svg", - excludeFromSemantics: true, - width: 90, - height: 90, - colorFilter: ColorFilter.mode( - Theme.of(context).brightness == - Brightness.light - ? Colors.white - : AppTheme.etsLightRed, - BlendMode.srcIn), - )), + const LoginHero(), const SizedBox( height: 48, ), - TextFormField( - autofillHints: const [ - AutofillHints.username - ], - cursorColor: Colors.white, - keyboardType: - TextInputType.visiblePassword, - decoration: InputDecoration( - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.white70)), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.white, - width: borderRadiusOnFocus)), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorTextColor, - width: borderRadiusOnFocus)), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorTextColor, - width: borderRadiusOnFocus)), - labelText: AppIntl.of(context)! - .login_prompt_universal_code, - labelStyle: const TextStyle( - color: Colors.white54), - errorStyle: - TextStyle(color: errorTextColor), - suffixIcon: Tooltip( - key: tooltipkey, - triggerMode: - TooltipTriggerMode.manual, - message: AppIntl.of(context)! - .universal_code_example, - preferBelow: true, - child: IconButton( - icon: const Icon(Icons.help, - color: Colors.white), - onPressed: () { - tooltipkey.currentState - ?.ensureTooltipVisible(); - }, - )), - ), - autofocus: true, - style: - const TextStyle(color: Colors.white), - onEditingComplete: _focusNode.nextFocus, + UniversalCodeFormField( validator: model.validateUniversalCode, - initialValue: model.universalCode, - inputFormatters: [ - LoginMask(), - ], + onEditionComplete: + _focusNode.nextFocus, + universalCode: model.universalCode ), const SizedBox( height: 16, @@ -151,101 +76,16 @@ class _LoginViewState extends State { validator: model.validatePassword, onEditionComplete: _focusNode.nextFocus), - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: InkWell( - child: Text( - AppIntl.of(context)! - .forgot_password, - style: const TextStyle( - decoration: - TextDecoration.underline, - color: Colors.white), - ), - onTap: () { - final signetsPasswordResetUrl = - _remoteConfigService - .signetsPasswordResetUrl; - if (signetsPasswordResetUrl != "") { - _launchUrlService.launchInBrowser( - _remoteConfigService - .signetsPasswordResetUrl, - Theme.of(context).brightness); - } else { - Fluttertoast.showToast( - msg: AppIntl.of(context)! - .error); - } - }, - ), - ), - ), + const ForgotPassword(), const SizedBox( height: 24, ), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: !model.canSubmit - ? null - : () async { - final String error = - await model.authenticate(); - - setState(() { - if (error.isNotEmpty) { - Fluttertoast.showToast( - msg: error); - } - formKey.currentState?.reset(); - }); - }, - style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all( - model.canSubmit - ? colorButton - : Colors.white38), - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric( - vertical: 16)), - ), - child: Text( - AppIntl.of(context)! - .login_action_sign_in, - style: TextStyle( - color: model.canSubmit - ? submitTextColor - : Colors.white60, - fontSize: 18), - ), - ), - ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 24), - child: InkWell( - child: Text( - AppIntl.of(context)!.need_help, - style: const TextStyle( - decoration: - TextDecoration.underline, - color: Colors.white), - ), - onTap: () async { - _navigationService.pushNamed( - RouterPaths.faq, - arguments: - Utils.getColorByBrightness( - context, - AppTheme.etsLightRed, - AppTheme.primaryDark)); - }, - ), - ), + LoginButton( + formKey: formKey, + canSubmit: model.canSubmit, + authenticate: model.authenticate, ), + const LoginFooter() ], ), ), @@ -280,13 +120,4 @@ class _LoginViewState extends State { _focusNode.dispose(); super.dispose(); } - - Color get errorTextColor => - Utils.getColorByBrightness(context, Colors.amberAccent, Colors.redAccent); - - Color get colorButton => - Utils.getColorByBrightness(context, Colors.white, AppTheme.etsLightRed); - - Color get submitTextColor => - Utils.getColorByBrightness(context, AppTheme.etsLightRed, Colors.white); } diff --git a/lib/features/welcome/widgets/forgot_password.dart b/lib/features/welcome/widgets/forgot_password.dart new file mode 100644 index 000000000..2b7199c97 --- /dev/null +++ b/lib/features/welcome/widgets/forgot_password.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/integration/launch_url_service.dart'; + +class ForgotPassword extends StatefulWidget{ + + const ForgotPassword( + {super.key}); + + @override + State createState() => _ForgotPasswordState(); +} + +class _ForgotPasswordState extends State{ + + final LaunchUrlService _launchUrlService = locator(); + + final RemoteConfigService _remoteConfigService = + locator(); + + @override + Widget build(BuildContext context) => Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(top: 4), + child: InkWell( + child: Text( + AppIntl.of(context)! + .forgot_password, + style: const TextStyle( + decoration: + TextDecoration.underline, + color: Colors.white), + ), + onTap: () { + final signetsPasswordResetUrl = + _remoteConfigService + .signetsPasswordResetUrl; + if (signetsPasswordResetUrl != "") { + _launchUrlService.launchInBrowser( + _remoteConfigService + .signetsPasswordResetUrl, + Theme.of(context).brightness); + } else { + Fluttertoast.showToast( + msg: AppIntl.of(context)! + .error); + } + }, + ), + ), + ); +} diff --git a/lib/features/welcome/widgets/login_button.dart b/lib/features/welcome/widgets/login_button.dart new file mode 100644 index 000000000..bb8e4bf36 --- /dev/null +++ b/lib/features/welcome/widgets/login_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; + +class LoginButton extends StatefulWidget{ + final GlobalKey formKey; + final bool canSubmit; + final ValueGetter> authenticate; + + const LoginButton({ + super.key, + required this.formKey, + required this.canSubmit, + required this.authenticate}); + + @override + State createState() => _LoginButtonState(); +} + +class _LoginButtonState extends State{ + + @override + Widget build(BuildContext context) => SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: !widget.canSubmit + ? null + : () async { + final String error = await widget.authenticate(); + + setState(() { + if (error.isNotEmpty) { + Fluttertoast.showToast( + msg: error); + } + widget.formKey.currentState?.reset(); + }); + }, + style: ButtonStyle( + backgroundColor: + WidgetStateProperty.all( + widget.canSubmit + ? colorButton + : Colors.white38), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + vertical: 16)), + ), + child: Text( + AppIntl.of(context)! + .login_action_sign_in, + style: TextStyle( + color: widget.canSubmit + ? submitTextColor + : Colors.white60, + fontSize: 18), + ), + ), + ); + + Color get colorButton => + Utils.getColorByBrightness(context, Colors.white, AppTheme.etsLightRed); + + Color get submitTextColor => + Utils.getColorByBrightness(context, AppTheme.etsLightRed, Colors.white); + +} diff --git a/lib/features/welcome/widgets/login_footer.dart b/lib/features/welcome/widgets/login_footer.dart new file mode 100644 index 000000000..e99f85150 --- /dev/null +++ b/lib/features/welcome/widgets/login_footer.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; + +class LoginFooter extends StatefulWidget{ + + const LoginFooter( + {super.key}); + + @override + State createState() => _LoginFooterState(); +} + +class _LoginFooterState extends State{ + final NavigationService _navigationService = locator(); + + @override + Widget build(BuildContext context) => Center( + child: Padding( + padding: const EdgeInsets.only(top: 24), + child: InkWell( + child: Text( + AppIntl.of(context)!.need_help, + style: const TextStyle( + decoration: + TextDecoration.underline, + color: Colors.white), + ), + onTap: () async { + _navigationService.pushNamed( + RouterPaths.faq, + arguments: + Utils.getColorByBrightness( + context, + AppTheme.etsLightRed, + AppTheme.primaryDark)); + }, + ), + ), + ); +} diff --git a/lib/features/welcome/widgets/login_hero.dart b/lib/features/welcome/widgets/login_hero.dart new file mode 100644 index 000000000..5d8645f13 --- /dev/null +++ b/lib/features/welcome/widgets/login_hero.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import 'package:notredame/utils/app_theme.dart'; + +class LoginHero extends StatefulWidget{ + + const LoginHero( + {super.key}); + + @override + State createState() => _LoginHeroState(); +} + +class _LoginHeroState extends State{ + + @override + Widget build(BuildContext context) => Hero( + tag: 'ets_logo', + child: SvgPicture.asset( + "assets/images/ets_white_logo.svg", + excludeFromSemantics: true, + width: 90, + height: 90, + colorFilter: ColorFilter.mode( + Theme.of(context).brightness == + Brightness.light + ? Colors.white + : AppTheme.etsLightRed, + BlendMode.srcIn), + )); +} diff --git a/lib/features/welcome/widgets/password_text_field.dart b/lib/features/welcome/widgets/password_text_field.dart index b8a6be87e..55db37967 100644 --- a/lib/features/welcome/widgets/password_text_field.dart +++ b/lib/features/welcome/widgets/password_text_field.dart @@ -12,7 +12,7 @@ class PasswordFormField extends StatefulWidget { {super.key, required this.validator, required this.onEditionComplete}); @override - _PasswordFormFieldState createState() => _PasswordFormFieldState(); + State createState() => _PasswordFormFieldState(); } class _PasswordFormFieldState extends State { diff --git a/lib/features/welcome/widgets/universal_code_text_field.dart b/lib/features/welcome/widgets/universal_code_text_field.dart new file mode 100644 index 000000000..4786b04d0 --- /dev/null +++ b/lib/features/welcome/widgets/universal_code_text_field.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:notredame/features/welcome/login/login_mask.dart'; + +class UniversalCodeFormField extends StatefulWidget { + final FormFieldValidator validator; + final VoidCallback onEditionComplete; + final String universalCode; + + const UniversalCodeFormField({ + super.key, + required this.validator, + required this.onEditionComplete, + required this.universalCode}); + + @override + State createState() => _UniversalCodeFormFieldState(); +} + +class _UniversalCodeFormFieldState extends State { + /// Define if the password is visible or not + final double borderRadiusOnFocus = 2.0; + + /// Unique key of the tooltip + final GlobalKey tooltipKey = GlobalKey(); + + @override + Widget build(BuildContext context) => TextFormField( + autofillHints: const [ + AutofillHints.username + ], + cursorColor: Colors.white, + keyboardType: + TextInputType.visiblePassword, + decoration: InputDecoration( + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white70)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white, + width: borderRadiusOnFocus)), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorTextColor, + width: borderRadiusOnFocus)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorTextColor, + width: borderRadiusOnFocus)), + labelText: AppIntl.of(context)! + .login_prompt_universal_code, + labelStyle: const TextStyle( + color: Colors.white54), + errorStyle: + TextStyle(color: errorTextColor), + suffixIcon: Tooltip( + key: tooltipKey, + triggerMode: + TooltipTriggerMode.manual, + message: AppIntl.of(context)! + .universal_code_example, + preferBelow: true, + child: IconButton( + icon: const Icon(Icons.help, + color: Colors.white), + onPressed: () { + tooltipKey.currentState + ?.ensureTooltipVisible(); + }, + )), + ), + autofocus: true, + style: + const TextStyle(color: Colors.white), + onEditingComplete: widget.onEditionComplete, + validator: widget.validator, + initialValue: widget.universalCode, + inputFormatters: [ + LoginMask(), + ], + ); + + Color get errorTextColor => Theme.of(context).brightness == Brightness.light + ? Colors.amberAccent + : Colors.redAccent; + +} diff --git a/pubspec.yaml b/pubspec.yaml index 63294827e..1f3d18ea0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.53.2 +version: 4.54.0 environment: sdk: '>=3.3.0 <4.0.0'