Skip to content

Commit

Permalink
feat(cat-voices): unlock password stage (#930)
Browse files Browse the repository at this point in the history
* chore: wip

* chore: wip

* feat: mocked create keychain method

* feat: info panel password picture types

* chore: remove autofill group

* feat: clear password on back

* fix: formatting

* refactor: use password from constructor data

---------

Co-authored-by: Dominik Toton <[email protected]>
  • Loading branch information
damian-molinski and dtscalac authored Oct 3, 2024
1 parent a2e8665 commit 0f060ac
Show file tree
Hide file tree
Showing 23 changed files with 518 additions and 51 deletions.
6 changes: 6 additions & 0 deletions catalyst_voices/lib/common/formatters/input_formatters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:flutter/services.dart';

/// Removes any whitespaces.
final class NoWhitespacesFormatter extends FilteringTextInputFormatter {
NoWhitespacesFormatter() : super.deny(RegExp(r'\s+'));
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

Expand All @@ -15,9 +16,17 @@ final class LoginPasswordTextField extends StatelessWidget {
return BlocBuilder<LoginBloc, LoginState>(
buildWhen: (previous, current) => previous.password != current.password,
builder: (context, state) {
final l10n = context.l10n;

return VoicesPasswordTextField(
key: passwordInputKey,
onChanged: (password) => _onPasswordChanged(context, password),
decoration: VoicesTextFieldDecoration(
errorMaxLines: 2,
labelText: l10n.passwordLabelText,
hintText: l10n.passwordHintText,
errorText: l10n.passwordErrorText,
),
);
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import 'package:catalyst_voices/pages/registration/create_keychain/stage/seed_ph
import 'package:catalyst_voices/pages/registration/create_keychain/stage/seed_phrase_panel.dart';
import 'package:catalyst_voices/pages/registration/create_keychain/stage/splash_panel.dart';
import 'package:catalyst_voices/pages/registration/create_keychain/stage/unlock_password_instructions_panel.dart';
import 'package:catalyst_voices/pages/registration/placeholder_panel.dart';
import 'package:catalyst_voices/pages/registration/create_keychain/stage/unlock_password_panel.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:flutter/material.dart';

class CreateKeychainPanel extends StatelessWidget {
final CreateKeychainStage stage;
final SeedPhraseState seedPhraseState;
final UnlockPasswordState unlockPasswordState;

const CreateKeychainPanel({
super.key,
required this.stage,
required this.seedPhraseState,
required this.unlockPasswordState,
});

@override
Expand All @@ -40,7 +42,9 @@ class CreateKeychainPanel extends StatelessWidget {
),
CreateKeychainStage.unlockPasswordInstructions =>
const UnlockPasswordInstructionsPanel(),
CreateKeychainStage.unlockPasswordCreate => const PlaceholderPanel(),
CreateKeychainStage.unlockPasswordCreate => UnlockPasswordPanel(
data: unlockPasswordState,
),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import 'package:catalyst_voices/pages/registration/registration_stage_navigation.dart';
import 'package:catalyst_voices/widgets/indicators/voices_password_strength_indicator.dart';
import 'package:catalyst_voices/widgets/text_field/voices_password_text_field.dart';
import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';

class UnlockPasswordPanel extends StatefulWidget {
final UnlockPasswordState data;

const UnlockPasswordPanel({
super.key,
required this.data,
});

@override
State<UnlockPasswordPanel> createState() => _UnlockPasswordPanelState();
}

class _UnlockPasswordPanelState extends State<UnlockPasswordPanel> {
late final TextEditingController _passwordController;
late final TextEditingController _confirmPasswordController;

@override
void initState() {
super.initState();

final password = widget.data.password;
final confirmPassword = widget.data.confirmPassword;

_passwordController = TextEditingController(text: password)
..addListener(_onPasswordChanged);
_confirmPasswordController = TextEditingController(text: confirmPassword)
..addListener(_onConfirmPasswordChanged);
}

@override
void dispose() {
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
const SizedBox(height: 12),
_EnterPasswordTextField(
controller: _passwordController,
),
const SizedBox(height: 12),
_ConfirmPasswordTextField(
controller: _confirmPasswordController,
showError: widget.data.showPasswordMisMatch,
minimumLength: widget.data.minPasswordLength,
),
const Spacer(),
const SizedBox(height: 22),
if (widget.data.showPasswordStrength)
VoicesPasswordStrengthIndicator(
passwordStrength: widget.data.passwordStrength,
),
const SizedBox(height: 22),
RegistrationBackNextNavigation(
isNextEnabled: widget.data.isNextEnabled,
onNextTap: _createKeychain,
onBackTap: _clearPasswordAndGoBack,
),
],
);
}

void _onPasswordChanged() {
final password = _passwordController.text;
RegistrationCubit.of(context).setPassword(password);
}

void _onConfirmPasswordChanged() {
final confirmPassword = _confirmPasswordController.text;
RegistrationCubit.of(context).setConfirmPassword(confirmPassword);
}

void _clearPasswordAndGoBack() {
RegistrationCubit.of(context)
..setPassword('')
..setConfirmPassword('')
..previousStep();
}

Future<void> _createKeychain() async {
final registrationCubit = RegistrationCubit.of(context);

await registrationCubit.createKeychain();
registrationCubit.nextStep();
}
}

class _EnterPasswordTextField extends StatelessWidget {
final TextEditingController controller;

const _EnterPasswordTextField({
required this.controller,
});

@override
Widget build(BuildContext context) {
return VoicesPasswordTextField(
controller: controller,
textInputAction: TextInputAction.next,
decoration: VoicesTextFieldDecoration(
labelText: context.l10n.enterPassword,
),
);
}
}

class _ConfirmPasswordTextField extends StatelessWidget {
final TextEditingController controller;
final bool showError;
final int minimumLength;

const _ConfirmPasswordTextField({
required this.controller,
this.showError = false,
required this.minimumLength,
});

@override
Widget build(BuildContext context) {
return VoicesPasswordTextField(
controller: controller,
decoration: VoicesTextFieldDecoration(
labelText: context.l10n.confirmPassword,
helperText: context.l10n.xCharactersMinimum(minimumLength),
errorText: showError ? context.l10n.passwordDoNotMatch : null,
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ class GetStartedPanel extends StatelessWidget {
onTap: () {
switch (type) {
case CreateAccountType.createNew:
RegistrationCubit.of(context).createNewKeychain();
RegistrationCubit.of(context).createNewKeychainStep();
case CreateAccountType.recover:
RegistrationCubit.of(context).recoverKeychain();
RegistrationCubit.of(context).recoverKeychainStep();
}
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:flutter/material.dart';

class PasswordPicture extends StatelessWidget {
const PasswordPicture({super.key});
final TaskPictureType type;

const PasswordPicture({
super.key,
this.type = TaskPictureType.normal,
});

@override
Widget build(BuildContext context) {
return TaskPicture(
child: TaskPictureIconBox(
type: TaskPictureType.error,
type: type,
child: VoicesAssets.icons.lockClosed.buildIcon(size: 48),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ class _RegistrationDialog extends StatelessWidget {
GetStarted() => const GetStartedPanel(),
FinishAccountCreation() => const FinishAccountCreationPanel(),
Recover() => const Placeholder(),
CreateKeychain(:final stage, :final seedPhraseState) =>
CreateKeychain(
:final stage,
:final seedPhrase,
:final unlockPassword,
) =>
CreateKeychainPanel(
stage: stage,
seedPhraseState: seedPhraseState,
seedPhraseState: seedPhrase,
unlockPasswordState: unlockPassword,
),
WalletLink(:final stage, :final stateData) => WalletLinkPanel(
stage: stage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ class RegistrationInfoPanel extends StatelessWidget {
CreateKeychainStage.checkSeedPhraseResult ||
CreateKeychainStage.unlockPasswordInstructions =>
_HeaderStrings(title: context.l10n.catalystKeychain),
CreateKeychainStage.unlockPasswordCreate =>
_HeaderStrings(title: 'TODO'),
CreateKeychainStage.unlockPasswordCreate => _HeaderStrings(
title: context.l10n.catalystKeychain,
subtitle: context.l10n.createKeychainUnlockPasswordIntoSubtitle,
body: context.l10n.createKeychainUnlockPasswordIntoBody,
),
};
}

Expand Down Expand Up @@ -121,6 +124,7 @@ class _RegistrationPicture extends StatelessWidget {
Widget buildKeychainStagePicture(
CreateKeychainStage stage, {
required bool isSeedPhraseCheckConfirmed,
required UnlockPasswordState unlockPassword,
}) {
return switch (stage) {
CreateKeychainStage.splash ||
Expand All @@ -140,7 +144,9 @@ class _RegistrationPicture extends StatelessWidget {
),
CreateKeychainStage.unlockPasswordInstructions ||
CreateKeychainStage.unlockPasswordCreate =>
const PasswordPicture(),
PasswordPicture(
type: unlockPassword.pictureType,
),
};
}

Expand All @@ -160,13 +166,26 @@ class _RegistrationPicture extends StatelessWidget {
GetStarted() => const KeychainPicture(),
FinishAccountCreation() => const KeychainPicture(),
Recover() => const KeychainPicture(),
CreateKeychain(:final stage, :final seedPhraseState) =>
CreateKeychain(:final stage, :final seedPhrase, :final unlockPassword) =>
buildKeychainStagePicture(
stage,
isSeedPhraseCheckConfirmed: seedPhraseState.isCheckConfirmed,
isSeedPhraseCheckConfirmed: seedPhrase.isCheckConfirmed,
unlockPassword: unlockPassword,
),
WalletLink(:final stage) => buildWalletLinkStagePicture(stage),
AccountCompleted() => const KeychainPicture(),
};
}
}

extension _UnlockPasswordStateExt on UnlockPasswordState {
TaskPictureType get pictureType {
if (showPasswordMisMatch) {
return TaskPictureType.error;
}
if (isNextEnabled) {
return TaskPictureType.success;
}
return TaskPictureType.normal;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import 'package:flutter/material.dart';
class RegistrationBackNextNavigation extends StatelessWidget {
final bool isBackEnabled;
final bool isNextEnabled;
final VoidCallback? onNextTap;
final VoidCallback? onBackTap;

const RegistrationBackNextNavigation({
super.key,
this.isBackEnabled = true,
this.isNextEnabled = true,
this.onNextTap,
this.onBackTap,
});

@override
Expand All @@ -21,12 +25,12 @@ class RegistrationBackNextNavigation extends StatelessWidget {
children: [
VoicesBackButton(
onTap: isBackEnabled
? () => RegistrationCubit.of(context).previousStep()
? onBackTap ?? () => RegistrationCubit.of(context).previousStep()
: null,
),
VoicesNextButton(
onTap: isNextEnabled
? () => RegistrationCubit.of(context).nextStep()
? onNextTap ?? () => RegistrationCubit.of(context).nextStep()
: null,
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ class _Label extends StatelessWidget {
PasswordStrength.normal => context.l10n.normalPasswordStrength,
PasswordStrength.strong => context.l10n.goodPasswordStrength,
},
style: Theme.of(context).textTheme.bodySmall,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
),
);
}
}
Expand Down
Loading

0 comments on commit 0f060ac

Please sign in to comment.