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): unlock password stage #930

Merged
merged 13 commits into from
Oct 3, 2024
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,149 @@
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 registrationState = RegistrationCubit.of(context).state;
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved
final createKeychain =
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved
registrationState is CreateKeychain ? registrationState : null;

final unlockPassword = createKeychain?.unlockPassword;

final password = unlockPassword?.password;
final confirmPassword = unlockPassword?.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 PlaceholderPanel(),
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 @@ -120,6 +123,7 @@ class _RegistrationPicture extends StatelessWidget {
Widget buildKeychainStagePicture(
CreateKeychainStage stage, {
required bool isSeedPhraseCheckConfirmed,
required UnlockPasswordState unlockPassword,
}) {
return switch (stage) {
CreateKeychainStage.splash ||
Expand All @@ -139,7 +143,9 @@ class _RegistrationPicture extends StatelessWidget {
),
CreateKeychainStage.unlockPasswordInstructions ||
CreateKeychainStage.unlockPasswordCreate =>
const PasswordPicture(),
PasswordPicture(
type: unlockPassword.pictureType,
),
};
}

Expand All @@ -159,13 +165,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
Loading