Skip to content

Commit

Permalink
feat(cat-voices): password strength indicator (#861)
Browse files Browse the repository at this point in the history
* feat(cat-voices): password strength calculator

* feat(cat-voices): add password strength indicator widget

* chore(cat-voices): reformat
  • Loading branch information
dtscalac committed Sep 23, 2024
1 parent b96002e commit 50348cd
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -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),
),
);
}
}
1 change: 1 addition & 0 deletions catalyst_voices/lib/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VoicesLocalizations> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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,

/// A medium password, not complex.
normal,

/// A complex password with characters from different groups.
strong;

/// The minimum length of accepted password.
static const int minimumPasswordLength = 8;

factory PasswordStrength.calculate(String text) {
if (text.length < minimumPasswordLength) return PasswordStrength.weak;

final strength = ps.estimatePasswordStrength(text);
if (strength <= 0.33) return PasswordStrength.weak;
if (strength <= 0.66) return PasswordStrength.normal;
return PasswordStrength.strong;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
library catalyst_voices_models;

export 'authentication_status.dart';
export 'auth/authentication_status.dart';
export 'auth/password_strength.dart';
export 'errors/errors.dart';
export 'proposal/funded_proposal.dart';
export 'proposal/pending_proposal.dart';
Expand Down
4 changes: 3 additions & 1 deletion catalyst_voices/packages/catalyst_voices_models/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ dependencies:
equatable: ^2.0.5
flutter_quill: ^10.5.13
meta: ^1.10.0
password_strength: ^0.2.0

dev_dependencies:
catalyst_analysis: ^2.0.0
test: ^1.24.9
flutter_test:
sdk: flutter
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group(PasswordStrength, () {
test('weak password - too short', () {
expect(
PasswordStrength.calculate('123456'),
equals(PasswordStrength.weak),
);

expect(
PasswordStrength.calculate('Ab1!@_'),
equals(PasswordStrength.weak),
);
});

test('weak password - too popular', () {
expect(
PasswordStrength.calculate('password'),
equals(PasswordStrength.weak),
);
});

test('weak password - too simple', () {
expect(
/* cSpell:disable */
PasswordStrength.calculate('simplepw'),
/* cSpell:enable */
equals(PasswordStrength.weak),
);
});

test('normal password', () {
expect(
PasswordStrength.calculate('Passwd12'),
equals(PasswordStrength.normal),
);
});

test('strong password', () {
expect(
PasswordStrength.calculate('Passwd!@'),
equals(PasswordStrength.strong),
);
});

test('strong password', () {
expect(
PasswordStrength.calculate('4Gf;Rd04WP,RxgBl)n5&RlG'),
equals(PasswordStrength.strong),
);
});
});
}
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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: [
Expand All @@ -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,
),
Expand All @@ -45,42 +46,53 @@ 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(),
SizedBox(width: 16),
VoicesCircularProgressIndicator(showTrack: false),
],
),
Text('Circular - Fixed'),
Row(
const Text('Circular - Fixed'),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
VoicesCircularProgressIndicator(value: 0.75),
SizedBox(width: 16),
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) {
Expand Down
Loading

0 comments on commit 50348cd

Please sign in to comment.