From f19b507a98e12841610c91b760d5f4e2309f103a Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Sun, 22 Sep 2024 19:08:17 +0200 Subject: [PATCH] feat(cat-voices): add password strength indicator widget --- .../voices_password_strength_indicator.dart | 116 ++++++++++++++++++ catalyst_voices/lib/widgets/widgets.dart | 1 + .../catalyst_voices_localizations.dart | 18 +++ .../catalyst_voices_localizations_en.dart | 9 ++ .../catalyst_voices_localizations_es.dart | 9 ++ .../lib/l10n/intl_en.arb | 12 ++ .../lib/src/auth/password_strength.dart | 2 + .../examples/voices_indicators_example.dart | 52 +++++--- 8 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart diff --git a/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart b/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart new file mode 100644 index 0000000000..68333b325d --- /dev/null +++ b/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart @@ -0,0 +1,116 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +/// An indicator for a [PasswordStrength]. +/// +/// Fills in all the available horizontal space, +/// use a [SizedBox] to limit it's width. +final class VoicesPasswordStrengthIndicator extends StatelessWidget { + final PasswordStrength passwordStrength; + + const VoicesPasswordStrengthIndicator({ + super.key, + required this.passwordStrength, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Label(passwordStrength: passwordStrength), + const SizedBox(height: 16), + _Indicator(passwordStrength: passwordStrength), + ], + ); + } +} + +class _Label extends StatelessWidget { + final PasswordStrength passwordStrength; + + const _Label({required this.passwordStrength}); + + @override + Widget build(BuildContext context) { + return Text( + switch (passwordStrength) { + PasswordStrength.weak => context.l10n.weakPasswordStrength, + PasswordStrength.normal => context.l10n.normalPasswordStrength, + PasswordStrength.strong => context.l10n.goodPasswordStrength, + }, + style: Theme.of(context).textTheme.bodySmall, + ); + } +} + +class _Indicator extends StatelessWidget { + static const double _backgroundTrackHeight = 4; + static const double _foregroundTrackHeight = 6; + static const double _tracksGap = 8; + + final PasswordStrength passwordStrength; + + const _Indicator({required this.passwordStrength}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: _foregroundTrackHeight, + child: LayoutBuilder( + builder: (context, constraints) { + final totalWidthOfAllGaps = + (PasswordStrength.values.length - 1) * _tracksGap; + final availableWidth = constraints.maxWidth - totalWidthOfAllGaps; + final trackWidth = availableWidth / PasswordStrength.values.length; + + return Stack( + children: [ + Positioned.fill( + top: 1, + child: Container( + height: _backgroundTrackHeight, + decoration: BoxDecoration( + color: Theme.of(context).colors.onSurfaceSecondary08, + borderRadius: BorderRadius.circular(_backgroundTrackHeight), + ), + ), + ), + for (final strength in PasswordStrength.values) + if (passwordStrength.index >= strength.index) + Positioned( + left: strength.index * (trackWidth + _tracksGap), + width: trackWidth, + child: _Track(passwordStrength: strength), + ), + ], + ); + }, + ), + ); + } +} + +class _Track extends StatelessWidget { + final PasswordStrength passwordStrength; + + const _Track({required this.passwordStrength}); + + @override + Widget build(BuildContext context) { + return Container( + height: _Indicator._foregroundTrackHeight, + decoration: BoxDecoration( + color: switch (passwordStrength) { + PasswordStrength.weak => Theme.of(context).colorScheme.error, + PasswordStrength.normal => Theme.of(context).colors.warning, + PasswordStrength.strong => Theme.of(context).colors.success, + }, + borderRadius: BorderRadius.circular(_Indicator._foregroundTrackHeight), + ), + ); + } +} diff --git a/catalyst_voices/lib/widgets/widgets.dart b/catalyst_voices/lib/widgets/widgets.dart index 0b9e550ea8..ff32170b6e 100644 --- a/catalyst_voices/lib/widgets/widgets.dart +++ b/catalyst_voices/lib/widgets/widgets.dart @@ -34,6 +34,7 @@ export 'indicators/process_progress_indicator.dart'; export 'indicators/voices_circular_progress_indicator.dart'; export 'indicators/voices_linear_progress_indicator.dart'; export 'indicators/voices_no_internet_connection_banner.dart'; +export 'indicators/voices_password_strength_indicator.dart'; export 'indicators/voices_status_indicator.dart'; export 'menu/voices_list_tile.dart'; export 'menu/voices_menu.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index 2ae03bd9b8..1105d7c665 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -645,6 +645,24 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'** String get noConnectionBannerDescription; + + /// Describes a password that is weak + /// + /// In en, this message translates to: + /// **'Weak password strength'** + String get weakPasswordStrength; + + /// Describes a password that has medium strength. + /// + /// In en, this message translates to: + /// **'Normal password strength'** + String get normalPasswordStrength; + + /// Describes a password that is strong. + /// + /// In en, this message translates to: + /// **'Good password strength'** + String get goodPasswordStrength; } class _VoicesLocalizationsDelegate extends LocalizationsDelegate { diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index 967640ee62..b4ae93ae36 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -336,4 +336,13 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get noConnectionBannerDescription => 'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'; + + @override + String get weakPasswordStrength => 'Weak password strength'; + + @override + String get normalPasswordStrength => 'Normal password strength'; + + @override + String get goodPasswordStrength => 'Good password strength'; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index 33e8773d20..734ef69842 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -336,4 +336,13 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get noConnectionBannerDescription => 'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'; + + @override + String get weakPasswordStrength => 'Weak password strength'; + + @override + String get normalPasswordStrength => 'Normal password strength'; + + @override + String get goodPasswordStrength => 'Good password strength'; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb index 215e697aaf..65c3750b5b 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -413,5 +413,17 @@ "noConnectionBannerDescription": "Your internet is playing hide and seek. Check your internet connection, or try again in a moment.", "@noConnectionBannerDescription": { "description": "Text shown in the No Internet Connection Banner widget for the description below the title." + }, + "weakPasswordStrength": "Weak password strength", + "@weakPasswordStrength": { + "description": "Describes a password that is weak" + }, + "normalPasswordStrength": "Normal password strength", + "@normalPasswordStrength": { + "description": "Describes a password that has medium strength." + }, + "goodPasswordStrength": "Good password strength", + "@goodPasswordStrength": { + "description": "Describes a password that is strong." } } \ No newline at end of file diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart index 91054c7085..7dcd283296 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart @@ -1,6 +1,8 @@ import 'package:password_strength/password_strength.dart' as ps; /// Describes strength of a password. +/// +/// The enum values must be sorted from the weakest to the strongest. enum PasswordStrength { /// A weak password. Simple, already exposed, commonly used, etc. weak, diff --git a/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart index 1f8e778b76..60c4b6c294 100644 --- a/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart +++ b/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices/widgets/common/affix_decorator.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/material.dart'; @@ -16,9 +17,9 @@ class VoicesIndicatorsExample extends StatelessWidget { appBar: AppBar(title: const Text('Voices Indicators')), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 42, vertical: 24), - children: const [ - Text('Status Indicator'), - Row( + children: [ + const Text('Status Indicator'), + const Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -30,7 +31,7 @@ class VoicesIndicatorsExample extends StatelessWidget { ), title: Text('Your QR code verified successfully'), body: Text( - 'You can now use your QR-code 
to login into Catalyst.', + 'You can now use your QR-code to login into Catalyst.', ), type: VoicesStatusIndicatorType.success, ), @@ -45,24 +46,24 @@ class VoicesIndicatorsExample extends StatelessWidget { title: Text('Upload failed or QR code not recognized!'), body: Text( 'Are you sure your upload didn’t get interrupted or that ' - 'you provided 
a Catalyst QR code? ' - '

Please try again.', + 'you provided a Catalyst QR code? ' + 'Please try again.', ), type: VoicesStatusIndicatorType.error, ), ), ], ), - Text('Process Stepper Indicator'), - _Steps(), - Text('Linear - Indeterminate'), - VoicesLinearProgressIndicator(), - VoicesLinearProgressIndicator(showTrack: false), - Text('Linear - Fixed'), - VoicesLinearProgressIndicator(value: 0.25), - VoicesLinearProgressIndicator(value: 0.25, showTrack: false), - Text('Circular - Indeterminate'), - Row( + const Text('Process Stepper Indicator'), + const _Steps(), + const Text('Linear - Indeterminate'), + const VoicesLinearProgressIndicator(), + const VoicesLinearProgressIndicator(showTrack: false), + const Text('Linear - Fixed'), + const VoicesLinearProgressIndicator(value: 0.25), + const VoicesLinearProgressIndicator(value: 0.25, showTrack: false), + const Text('Circular - Indeterminate'), + const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ VoicesCircularProgressIndicator(), @@ -70,8 +71,8 @@ class VoicesIndicatorsExample extends StatelessWidget { VoicesCircularProgressIndicator(showTrack: false), ], ), - Text('Circular - Fixed'), - Row( + const Text('Circular - Fixed'), + const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ VoicesCircularProgressIndicator(value: 0.75), @@ -79,8 +80,19 @@ class VoicesIndicatorsExample extends StatelessWidget { VoicesCircularProgressIndicator(value: 0.75, showTrack: false), ], ), - Text('No Internet Connection Banner'), - NoInternetConnectionBanner(), + const Text('No Internet Connection Banner'), + const NoInternetConnectionBanner(), + const Text('Password strength indicator'), + for (final passwordStrength in PasswordStrength.values) + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + child: VoicesPasswordStrengthIndicator( + passwordStrength: passwordStrength, + ), + ), + ), ].separatedByIndexed( (index, value) { return switch (value.runtimeType) {