Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cat-voices): password strength indicator #861

Merged
merged 3 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading