diff --git a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart index cd9c386c5f..3c5b9a0a97 100644 --- a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart +++ b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart @@ -88,6 +88,8 @@ Future bootstrap() async { GoRouter.optionURLReflectsImperativeAPIs = true; setPathUrlStrategy(); + await Dependencies.instance.init(); + final router = AppRouter.init( guards: const [ MilestoneGuard(), @@ -96,8 +98,6 @@ Future bootstrap() async { Bloc.observer = AppBlocObserver(); - await Dependencies.instance.init(); - return BootstrapArgs(routerConfig: router); } diff --git a/catalyst_voices/apps/voices/lib/pages/account/account_page.dart b/catalyst_voices/apps/voices/lib/pages/account/account_page.dart index c08b4fed1c..c288bbfaaf 100644 --- a/catalyst_voices/apps/voices/lib/pages/account/account_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/account/account_page.dart @@ -4,10 +4,10 @@ import 'package:catalyst_voices/common/ext/account_role_ext.dart'; import 'package:catalyst_voices/pages/account/account_page_header.dart'; import 'package:catalyst_voices/pages/account/delete_keychain_dialog.dart'; import 'package:catalyst_voices/pages/account/keychain_deleted_dialog.dart'; +import 'package:catalyst_voices/routes/routes.dart'; import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; import 'package:catalyst_voices/widgets/list/bullet_list.dart'; -import 'package:catalyst_voices/widgets/modals/voices_dialog.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; @@ -16,9 +16,14 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -final class AccountPage extends StatelessWidget { +final class AccountPage extends StatefulWidget { const AccountPage({super.key}); + @override + State createState() => _AccountPageState(); +} + +class _AccountPageState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -44,7 +49,7 @@ final class AccountPage extends StatelessWidget { AccountRole.drep, ], defaultRole: AccountRole.voter, - onRemoveKeychain: () => unawaited(_removeKeychain(context)), + onRemoveKeychain: () => unawaited(_removeKeychain()), ), ], ), @@ -55,19 +60,22 @@ final class AccountPage extends StatelessWidget { ); } - // Note. probably should redirect somewhere. - Future _removeKeychain(BuildContext context) async { + Future _removeKeychain() async { final confirmed = await DeleteKeychainDialog.show(context); + if (!confirmed) { + return; + } - if (confirmed && context.mounted) { - unawaited(context.read().removeKeychain()); + if (mounted) { + await context.read().removeKeychain(); + } + + if (mounted) { + await KeychainDeletedDialog.show(context); + } - await VoicesDialog.show( - context: context, - builder: (context) { - return const KeychainDeletedDialog(); - }, - ); + if (mounted) { + const DiscoveryRoute().go(context); } } } diff --git a/catalyst_voices/apps/voices/lib/pages/account/delete_keychain_dialog.dart b/catalyst_voices/apps/voices/lib/pages/account/delete_keychain_dialog.dart index d8645dcec1..ea199389f5 100644 --- a/catalyst_voices/apps/voices/lib/pages/account/delete_keychain_dialog.dart +++ b/catalyst_voices/apps/voices/lib/pages/account/delete_keychain_dialog.dart @@ -9,14 +9,12 @@ import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; class DeleteKeychainDialog extends StatefulWidget { - const DeleteKeychainDialog({ - super.key, - }); + const DeleteKeychainDialog._(); static Future show(BuildContext context) async { final result = await VoicesDialog.show( context: context, - builder: (context) => const DeleteKeychainDialog(), + builder: (context) => const DeleteKeychainDialog._(), ); return result ?? false; @@ -107,6 +105,7 @@ class _DeleteKeychainDialogState extends State { width: 300, child: VoicesTextField( controller: _textEditingController, + onFieldSubmitted: _removeKeychain, decoration: VoicesTextFieldDecoration( errorText: _errorText, errorMaxLines: 2, @@ -124,7 +123,7 @@ class _DeleteKeychainDialogState extends State { children: [ VoicesFilledButton( backgroundColor: Theme.of(context).colors.iconsError, - onTap: () async => _onRemoveKeychainTap(), + onTap: _removeKeychain, child: Text(context.l10n.delete), ), const SizedBox(width: 8), @@ -142,9 +141,8 @@ class _DeleteKeychainDialogState extends State { ); } - Future _onRemoveKeychainTap() async { - if (_textEditingController.text == - context.l10n.deleteKeychainDialogRemovingPhrase) { + void _removeKeychain([String? value]) { + if (_isDeleteConfirmed(value ?? _textEditingController.text)) { Navigator.pop(context, true); } else { setState(() { @@ -152,4 +150,8 @@ class _DeleteKeychainDialogState extends State { }); } } + + bool _isDeleteConfirmed(String value) { + return value == context.l10n.deleteKeychainDialogRemovingPhrase; + } } diff --git a/catalyst_voices/apps/voices/lib/pages/account/keychain_deleted_dialog.dart b/catalyst_voices/apps/voices/lib/pages/account/keychain_deleted_dialog.dart index 76671de82c..eaf1145e3d 100644 --- a/catalyst_voices/apps/voices/lib/pages/account/keychain_deleted_dialog.dart +++ b/catalyst_voices/apps/voices/lib/pages/account/keychain_deleted_dialog.dart @@ -1,11 +1,17 @@ -import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart'; -import 'package:catalyst_voices/widgets/modals/voices_desktop_dialog.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; class KeychainDeletedDialog extends StatelessWidget { - const KeychainDeletedDialog({super.key}); + const KeychainDeletedDialog._(); + + static Future show(BuildContext context) { + return VoicesDialog.show( + context: context, + builder: (context) => const KeychainDeletedDialog._(), + ); + } @override Widget build(BuildContext context) { diff --git a/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart b/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart index 770d8ac3ad..7d8e4d6d96 100644 --- a/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart +++ b/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart @@ -18,6 +18,7 @@ final class LoginEmailTextFiled extends StatelessWidget { return VoicesEmailTextField( key: emailInputKey, onChanged: (email) => _onEmailChanged(context, email), + onFieldSubmitted: (email) => _onEmailChanged(context, email), ); }, ); diff --git a/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/account_details_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/account_details_panel.dart index f8b2ed8495..f9f3ac815d 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/account_details_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/account_details_panel.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:catalyst_voices/pages/registration/recover/bloc_recover_builder.dart'; import 'package:catalyst_voices/pages/registration/widgets/wallet_connection_status.dart'; import 'package:catalyst_voices/pages/registration/widgets/wallet_summary.dart'; @@ -33,11 +31,9 @@ class AccountDetailsPanel extends StatelessWidget { style: theme.textTheme.titleMedium?.copyWith(color: textColor), ), const SizedBox(height: 24), - Expanded( + const Expanded( child: SingleChildScrollView( - child: _BlocAccountSummery( - onRetry: () => unawaited(_retryAccountRestore(context)), - ), + child: _BlocAccountSummery(), ), ), const SizedBox(height: 24), @@ -45,19 +41,10 @@ class AccountDetailsPanel extends StatelessWidget { ], ); } - - Future _retryAccountRestore(BuildContext context) async { - final recover = RegistrationCubit.of(context).recover; - await recover.recoverAccount(); - } } class _BlocAccountSummery extends StatelessWidget { - final VoidCallback? onRetry; - - const _BlocAccountSummery({ - this.onRetry, - }); + const _BlocAccountSummery(); @override Widget build(BuildContext context) { @@ -71,10 +58,7 @@ class _BlocAccountSummery extends StatelessWidget { walletSummary: value.walletSummary, ), Failure(:final value) => - _RecoverAccountFailure( - exception: value, - onRetry: onRetry, - ), + _RecoverAccountFailure(exception: value), _ => const Center(child: VoicesCircularProgressIndicator()), }; }, @@ -117,18 +101,19 @@ class _RecoveredAccountSummary extends StatelessWidget { class _RecoverAccountFailure extends StatelessWidget { final LocalizedException exception; - final VoidCallback? onRetry; const _RecoverAccountFailure({ required this.exception, - this.onRetry, }); @override Widget build(BuildContext context) { return VoicesErrorIndicator( message: exception.message(context), - onRetry: onRetry, + onRetry: () async { + final recover = RegistrationCubit.of(context).recover; + await recover.recoverAccount(); + }, ); } } @@ -185,8 +170,12 @@ class _Navigation extends StatelessWidget { ), const SizedBox(height: 10), VoicesTextButton( - onTap: () => RegistrationCubit.of(context).previousStep(), - child: Text(context.l10n.back), + onTap: () async { + final cubit = RegistrationCubit.of(context); + await cubit.recover.reset(); + cubit.previousStep(); + }, + child: Text(context.l10n.recoverDifferentKeychain), ), ], ); diff --git a/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart index 4a63389376..6e81bc7c36 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart @@ -56,6 +56,7 @@ class _UnlockPasswordPanelState extends State { ), const SizedBox(height: 22), _BlocNavigation( + onNextTap: _createKeychain, onBackTap: _clearPasswordAndGoBack, ), ], @@ -74,6 +75,16 @@ class _UnlockPasswordPanelState extends State { RegistrationCubit.of(context).recover.setConfirmPassword(confirmPassword); } + Future _createKeychain() async { + final cubit = RegistrationCubit.of(context); + + final success = await cubit.recover.createKeychain(); + + if (success) { + cubit.nextStep(); + } + } + void _clearPasswordAndGoBack() { final registration = RegistrationCubit.of(context); @@ -124,9 +135,11 @@ class _BlocUnlockPasswordForm extends StatelessWidget { } class _BlocNavigation extends StatelessWidget { + final VoidCallback onNextTap; final VoidCallback onBackTap; const _BlocNavigation({ + required this.onNextTap, required this.onBackTap, }); @@ -138,6 +151,7 @@ class _BlocNavigation extends StatelessWidget { builder: (context, state) { return RegistrationBackNextNavigation( isNextEnabled: state, + onNextTap: onNextTap, onBackTap: onBackTap, ); }, diff --git a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/roles_summary_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/roles_summary_panel.dart index d122ca5dae..c4dfdbebe8 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/roles_summary_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/roles_summary_panel.dart @@ -31,7 +31,7 @@ class RolesSummaryPanel extends StatelessWidget { onTap: () { RegistrationCubit.of(context).nextStep(); }, - child: Text(context.l10n.walletLinkRoleSummaryButton), + child: Text(context.l10n.reviewRegistrationTransaction), ), const SizedBox(height: 10), VoicesTextButton( diff --git a/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart b/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart index 15bd6c4bdf..d704244195 100644 --- a/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart @@ -121,6 +121,7 @@ class _UnlockedHeaderActions extends StatelessWidget { suffixIcon: VoicesAssets.icons.arrowTriangleDown.buildIcon(size: 16), ), + onFieldSubmitted: (value) {}, ), ), const SizedBox(width: 16), @@ -132,6 +133,7 @@ class _UnlockedHeaderActions extends StatelessWidget { hintText: 'Search proposals', prefixIcon: VoicesAssets.icons.search.buildIcon(), ), + onFieldSubmitted: (value) {}, ), ), IconButton( diff --git a/catalyst_voices/apps/voices/lib/routes/app_router.dart b/catalyst_voices/apps/voices/lib/routes/app_router.dart index 54eeefd38e..5fb2fa2623 100644 --- a/catalyst_voices/apps/voices/lib/routes/app_router.dart +++ b/catalyst_voices/apps/voices/lib/routes/app_router.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:catalyst_voices/routes/guards/route_guard.dart'; +import 'package:catalyst_voices/routes/routes.dart'; import 'package:catalyst_voices/routes/routing/routes.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -13,6 +14,7 @@ abstract final class AppRouter { static GoRouter init({ List guards = const [], + Listenable? refreshListenable, }) { return GoRouter( navigatorKey: _rootNavigatorKey, @@ -21,6 +23,7 @@ abstract final class AppRouter { observers: [ SentryNavigatorObserver(), ], + refreshListenable: refreshListenable, routes: Routes.routes, // always true. We're deciding whether to print // them or not in LoggingService @@ -28,16 +31,15 @@ abstract final class AppRouter { ); } - static FutureOr _guard( + static Future _guard( BuildContext context, GoRouterState state, List guards, ) async { for (final guard in guards) { - final redirect = await guard.redirect(context, state); - - if (redirect != null) { - return redirect; + final location = await guard.redirect(context, state); + if (location != null) { + return location; } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_email_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_email_text_field.dart index e39beb61a4..418c7fa70e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_email_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_email_text_field.dart @@ -5,10 +5,12 @@ import 'package:flutter/material.dart'; final class VoicesEmailTextField extends StatelessWidget { /// Emits new value when widget input changes final ValueChanged? onChanged; + final ValueChanged onFieldSubmitted; const VoicesEmailTextField({ super.key, this.onChanged, + required this.onFieldSubmitted, }); @override @@ -18,6 +20,7 @@ final class VoicesEmailTextField extends StatelessWidget { keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, decoration: VoicesTextFieldDecoration( labelText: l10n.emailLabelText, hintText: l10n.emailHintText, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index 0c244f87f3..3122032491 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -85,7 +85,10 @@ class VoicesTextField extends StatefulWidget { this.validator, this.onChanged, this.resizable, - this.onFieldSubmitted, + // Making it required but nullable because default behaviour is + // to make some action when user taps enter. Focus next field or anything + // else. + required this.onFieldSubmitted, this.onSaved, this.inputFormatters, }); diff --git a/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart b/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart index 965cf736a2..7afb9e3c30 100644 --- a/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart +++ b/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart @@ -8,8 +8,10 @@ void main() { group('VoicesTextField Widget Tests', () { testWidgets('renders correctly with default parameters', (tester) async { await tester.pumpWidget( - const _MaterialApp( - child: VoicesTextField(), + _MaterialApp( + child: VoicesTextField( + onFieldSubmitted: (value) {}, + ), ), ); @@ -21,9 +23,10 @@ void main() { const labelText = 'Test Label'; await tester.pumpWidget( - const _MaterialApp( + _MaterialApp( child: VoicesTextField( - decoration: VoicesTextFieldDecoration(labelText: labelText), + decoration: const VoicesTextFieldDecoration(labelText: labelText), + onFieldSubmitted: (value) {}, ), ), ); @@ -39,6 +42,7 @@ void main() { _MaterialApp( child: VoicesTextField( controller: controller, + onFieldSubmitted: (value) {}, ), ), ); @@ -55,12 +59,13 @@ void main() { const errorText = 'Error message'; await tester.pumpWidget( - const _MaterialApp( + _MaterialApp( child: VoicesTextField( - decoration: VoicesTextFieldDecoration( + decoration: const VoicesTextFieldDecoration( hintText: hintText, errorText: errorText, ), + onFieldSubmitted: (value) {}, ), ), ); @@ -80,6 +85,7 @@ void main() { status: VoicesTextFieldStatus.error, errorMessage: errorText, ), + onFieldSubmitted: (value) {}, ), ), ); @@ -101,6 +107,7 @@ void main() { _MaterialApp( child: VoicesTextField( validator: VoicesTextFieldValidationResult.success(), + onFieldSubmitted: (value) {}, ), ), ); @@ -118,11 +125,13 @@ void main() { testWidgets('renders correctly when disabled', (tester) async { await tester.pumpWidget( - const _MaterialApp( + _MaterialApp( child: VoicesTextField( enabled: false, - decoration: - VoicesTextFieldDecoration(labelText: 'Disabled TextField'), + decoration: const VoicesTextFieldDecoration( + labelText: 'Disabled TextField', + ), + onFieldSubmitted: (value) {}, ), ), ); @@ -155,6 +164,7 @@ void main() { _MaterialApp( child: VoicesTextField( validator: validator, + onFieldSubmitted: (value) {}, ), ), ); @@ -186,6 +196,7 @@ void main() { _MaterialApp( child: VoicesTextField( validator: validator, + onFieldSubmitted: (value) {}, ), ), ); @@ -222,6 +233,7 @@ void main() { _MaterialApp( child: VoicesTextField( validator: validator, + onFieldSubmitted: (value) {}, ), ), ); diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart index 5105628edd..df6bbbc3c6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart @@ -17,7 +17,11 @@ abstract interface class RecoverManager implements UnlockPasswordManager { void setSeedPhraseWords(List words); - Future recoverAccount(); + Future recoverAccount(); + + Future createKeychain(); + + Future reset(); } final class RecoverCubit extends Cubit @@ -27,6 +31,7 @@ final class RecoverCubit extends Cubit final RegistrationService _registrationService; SeedPhrase? _seedPhrase; + Account? _recoveredAccount; RecoverCubit({ required UserService userService, @@ -66,24 +71,23 @@ final class RecoverCubit extends Cubit } @override - Future recoverAccount() async { + Future recoverAccount() async { try { emit(state.copyWith(accountDetails: const Optional.empty())); final seedPhrase = _seedPhrase; - final lockFactor = PasswordLockFactor(password.value); - if (seedPhrase == null) { const exception = LocalizedRegistrationSeedPhraseNotFoundException(); emit(state.copyWith(accountDetails: Optional(Failure(exception)))); - return; + return false; } final account = await _registrationService.recoverAccount( seedPhrase: seedPhrase, - lockFactor: lockFactor, ); + _recoveredAccount = account; + await _userService.useAccount(account); final walletInfo = account.walletInfo; @@ -102,23 +106,69 @@ final class RecoverCubit extends Cubit ); emit(state.copyWith(accountDetails: Optional(Success(accountDetails)))); + + return true; } on RegistrationException catch (error, stack) { _logger.severe('recover account', error, stack); + _recoveredAccount = null; + final exception = LocalizedRegistrationException.from(error); emit(state.copyWith(accountDetails: Optional(Failure(exception)))); + + return false; } catch (error, stack) { _logger.severe('recover account', error, stack); + _recoveredAccount = null; + const exception = LocalizedUnknownException(); emit(state.copyWith(accountDetails: Optional(Failure(exception)))); + + return false; + } + } + + @override + Future createKeychain() async { + final account = _recoveredAccount; + final seedPhrase = _seedPhrase; + final password = this.password; + + if (account == null || seedPhrase == null || password.isNotValid) { + emitError(const LocalizedRegistrationUnknownException()); + return false; } + + final lockFactor = PasswordLockFactor(password.value); + + await _registrationService.createKeychainFor( + account: account, + seedPhrase: seedPhrase, + lockFactor: lockFactor, + ); + + await _userService.useAccount(account); + + return true; } @override void onUnlockPasswordStateChanged(UnlockPasswordState data) { emit(state.copyWith(unlockPasswordState: data)); } + + @override + Future reset() async { + final recoveredAccount = _recoveredAccount; + if (recoveredAccount != null) { + await _userService.removeKeychain(recoveredAccount.keychainId); + } + + _recoveredAccount = null; + + setSeedPhraseWords([]); + } } const _testWords = [ diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart index e47537d544..86adb2dbd5 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart @@ -61,7 +61,7 @@ final class SessionCubit extends Cubit } Future removeKeychain() { - return _userService.removeCurrentAccount(); + return _userService.removeCurrentKeychain(); } Future switchToDummyAccount() async { diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart index 0a058f7c00..ae3569baa4 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart @@ -43,7 +43,7 @@ void main() { // Given // When - await userService.removeCurrentAccount(); + await userService.removeCurrentKeychain(); // Then expect(userService.keychain, isNull); @@ -54,7 +54,7 @@ void main() { // Given // When - await userService.removeCurrentAccount(); + await userService.removeCurrentKeychain(); // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); @@ -80,7 +80,7 @@ void main() { // When notifier.value = RegistrationProgress(keychainProgress: keychainProgress); - await userService.removeCurrentAccount(); + await userService.removeCurrentKeychain(); // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index 97407e607c..e25a788ede 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -886,12 +886,6 @@ abstract class VoicesLocalizations { /// **' in Catalyst.'** String get walletLinkRoleSummaryContent3; - /// A button label on the role summary screen in registration for the next step. - /// - /// In en, this message translates to: - /// **'Confirm & Sign with wallet'** - String get walletLinkRoleSummaryButton; - /// Message shown when redirecting to external content that describes which wallets are supported. /// /// In en, this message translates to: @@ -1348,22 +1342,22 @@ abstract class VoicesLocalizations { /// **'Dark'** String get themeDark; - /// A title on keychain deleted dialog + /// No description provided for @keychainDeletedDialogTitle. /// /// In en, this message translates to: /// **'Catalyst keychain removed'** String get keychainDeletedDialogTitle; - /// A subtitle on keychain deleted dialog + /// No description provided for @keychainDeletedDialogSubtitle. /// /// In en, this message translates to: - /// **'Catalyst keychain removed'** + /// **'Your Catalyst Keychain is removed successfully from this device.'** String get keychainDeletedDialogSubtitle; - /// An info on keychain deleted dialog + /// No description provided for @keychainDeletedDialogInfo. /// /// In en, this message translates to: - /// **'Catalyst keychain removed'** + /// **'We reverted this device to Catalyst first use.'** String get keychainDeletedDialogInfo; /// No description provided for @registrationCompletedTitle. @@ -1744,6 +1738,12 @@ abstract class VoicesLocalizations { /// **'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'** String get recoveryUnlockPasswordInstructionsSubtitle; + /// No description provided for @recoverDifferentKeychain. + /// + /// In en, this message translates to: + /// **'Restore a different keychain'** + String get recoverDifferentKeychain; + /// The header label in unlock dialog. /// /// In en, this message translates to: @@ -1905,6 +1905,12 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Connect a different wallet'** String get connectDifferentWallet; + + /// A button label to review the registration transaction in wallet detail panel. + /// + /// In en, this message translates to: + /// **'Review registration transaction'** + String get reviewRegistrationTransaction; } class _VoicesLocalizationsDelegate extends LocalizationsDelegate { diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index b35c281e55..5cd6739f26 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -478,9 +478,6 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get walletLinkRoleSummaryContent3 => ' in Catalyst.'; - @override - String get walletLinkRoleSummaryButton => 'Confirm & Sign with wallet'; - @override String get seeAllSupportedWallets => 'See all supported wallets'; @@ -717,10 +714,10 @@ class VoicesLocalizationsEn extends VoicesLocalizations { String get keychainDeletedDialogTitle => 'Catalyst keychain removed'; @override - String get keychainDeletedDialogSubtitle => 'Catalyst keychain removed'; + String get keychainDeletedDialogSubtitle => 'Your Catalyst Keychain is removed successfully from this device.'; @override - String get keychainDeletedDialogInfo => 'Catalyst keychain removed'; + String get keychainDeletedDialogInfo => 'We reverted this device to Catalyst first use.'; @override String get registrationCompletedTitle => 'Catalyst account setup'; @@ -917,6 +914,9 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get recoveryUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; + @override + String get recoverDifferentKeychain => 'Restore a different keychain'; + @override String get unlockDialogHeader => 'Unlock Catalyst'; @@ -997,4 +997,7 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get connectDifferentWallet => 'Connect a different wallet'; + + @override + String get reviewRegistrationTransaction => 'Review registration transaction'; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index fb7b6d9584..4384ef7847 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -478,9 +478,6 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get walletLinkRoleSummaryContent3 => ' in Catalyst.'; - @override - String get walletLinkRoleSummaryButton => 'Confirm & Sign with wallet'; - @override String get seeAllSupportedWallets => 'See all supported wallets'; @@ -717,10 +714,10 @@ class VoicesLocalizationsEs extends VoicesLocalizations { String get keychainDeletedDialogTitle => 'Catalyst keychain removed'; @override - String get keychainDeletedDialogSubtitle => 'Catalyst keychain removed'; + String get keychainDeletedDialogSubtitle => 'Your Catalyst Keychain is removed successfully from this device.'; @override - String get keychainDeletedDialogInfo => 'Catalyst keychain removed'; + String get keychainDeletedDialogInfo => 'We reverted this device to Catalyst first use.'; @override String get registrationCompletedTitle => 'Catalyst account setup'; @@ -917,6 +914,9 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get recoveryUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; + @override + String get recoverDifferentKeychain => 'Restore a different keychain'; + @override String get unlockDialogHeader => 'Unlock Catalyst'; @@ -997,4 +997,7 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get connectDifferentWallet => 'Connect a different wallet'; + + @override + String get reviewRegistrationTransaction => 'Review registration transaction'; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index 5c8841c526..cee75b5d56 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -595,10 +595,6 @@ "@walletLinkRoleSummaryContent3": { "description": "The last part of the message on the role summary screen in registration." }, - "walletLinkRoleSummaryButton": "Confirm & Sign with wallet", - "@walletLinkRoleSummaryButton": { - "description": "A button label on the role summary screen in registration for the next step." - }, "seeAllSupportedWallets": "See all supported wallets", "@seeAllSupportedWallets": { "description": "Message shown when redirecting to external content that describes which wallets are supported." @@ -812,17 +808,8 @@ "description": "Refers to a dark theme mode." }, "keychainDeletedDialogTitle": "Catalyst keychain removed", - "@keychainDeletedDialogTitle": { - "description": "A title on keychain deleted dialog" - }, - "keychainDeletedDialogSubtitle": "Catalyst keychain removed", - "@keychainDeletedDialogSubtitle": { - "description": "A subtitle on keychain deleted dialog" - }, - "keychainDeletedDialogInfo": "Catalyst keychain removed", - "@keychainDeletedDialogInfo": { - "description": "An info on keychain deleted dialog" - }, + "keychainDeletedDialogSubtitle": "Your Catalyst Keychain is removed successfully from this device.", + "keychainDeletedDialogInfo": "We reverted this device to Catalyst first use.", "registrationCompletedTitle": "Catalyst account setup", "registrationCompletedSubtitle": "Completed!", "registrationCompletedSummaryHeader": "Summary", @@ -913,6 +900,7 @@ "recoveryAccountDetailsAction": "Set unlock password for this device", "recoveryUnlockPasswordInstructionsTitle": "Set your Catalyst unlock password f\u2028or this device", "recoveryUnlockPasswordInstructionsSubtitle": "With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. \u2028\u2028But it can be a bit tedious to enter every single time you want to use the app. \u2028\u2028In this next step, you'll set your Unlock Password for your current device. It's like a shortcut for proving ownership of your Keychain. \u2028\u2028Whenever you recover your account for the first time on a new device, you'll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.", + "recoverDifferentKeychain": "Restore a different keychain", "unlockDialogHeader": "Unlock Catalyst", "@unlockDialogHeader": { "description": "The header label in unlock dialog." @@ -975,5 +963,9 @@ "connectDifferentWallet": "Connect a different wallet", "@connectDifferentWallet": { "description": "A button label to connect a different wallet in wallet detail panel." + }, + "reviewRegistrationTransaction": "Review registration transaction", + "@reviewRegistrationTransaction": { + "description": "A button label to review the registration transaction in wallet detail panel." } } \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart index 83aaae55b8..0e1770c753 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart @@ -48,8 +48,15 @@ abstract interface class RegistrationService { required Set roles, }); + /// Loads account related to this [seedPhrase]. Throws exception if non found. Future recoverAccount({ required SeedPhrase seedPhrase, + }); + + /// Creates [Keychain] for given [account] with [lockFactor]. + Future createKeychainFor({ + required Account account, + required SeedPhrase seedPhrase, required LockFactor lockFactor, }); @@ -138,7 +145,6 @@ final class RegistrationServiceImpl implements RegistrationService { @override Future recoverAccount({ required SeedPhrase seedPhrase, - required LockFactor lockFactor, }) async { await Future.delayed(const Duration(milliseconds: 200)); @@ -148,17 +154,7 @@ final class RegistrationServiceImpl implements RegistrationService { } final roles = {AccountRole.root}; - // TODO(dtscalac): Update key value when derivation is final. - final keyPair = await deriveAccountRoleKeyPair( - seedPhrase: seedPhrase, - roles: roles, - ); - final keychainId = const Uuid().v4(); - final keychain = await _keychainProvider.create(keychainId); - await keychain.setLock(lockFactor); - await keychain.unlock(lockFactor); - await keychain.setMasterKey(keyPair.privateKey); // Note. with rootKey query backend for account details. return Account( @@ -172,6 +168,27 @@ final class RegistrationServiceImpl implements RegistrationService { ); } + @override + Future createKeychainFor({ + required Account account, + required SeedPhrase seedPhrase, + required LockFactor lockFactor, + }) async { + final keychainId = account.keychainId; + + final keyPair = await deriveAccountRoleKeyPair( + seedPhrase: seedPhrase, + roles: account.roles, + ); + + final keychain = await _keychainProvider.create(keychainId); + await keychain.setLock(lockFactor); + await keychain.unlock(lockFactor); + await keychain.setMasterKey(keyPair.privateKey); + + return keychain; + } + @override Future prepareRegistration({ required CardanoWallet wallet, diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart index 53030b69fc..5ad6755c89 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart @@ -32,7 +32,9 @@ abstract interface class UserService { Future useKeychain(String id); - Future removeCurrentAccount(); + Future removeCurrentKeychain(); + + Future removeKeychain(String id); Future dispose(); } @@ -110,16 +112,34 @@ final class UserServiceImpl implements UserService { } @override - Future removeCurrentAccount() async { + Future removeCurrentKeychain() async { final keychain = _keychain; if (keychain == null) { - _logger.warning('Called remove account but no active keychain found'); + _logger.warning('Called remove keychain but no active found'); + return; + } + + await removeKeychain(keychain.id); + } + + @override + Future removeKeychain(String id) async { + if (!await _keychainProvider.exists(id)) { + _logger.warning( + 'Called remove keychain[$id] but no such keychain was found', + ); return; } + final isCurrentKeychain = id == _keychain?.id; + + final keychain = await _keychainProvider.get(id); await keychain.clear(); - await _clearUser(); - await _useKeychain(null); + + if (isCurrentKeychain) { + await _clearUser(); + await _useKeychain(null); + } } @override diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart index eac0f3d7d3..045abedef1 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart @@ -56,7 +56,7 @@ void main() { await service.useKeychain(keychainOne.id); await service.useKeychain(keychainTwo.id); - await service.removeCurrentAccount(); + await service.removeCurrentKeychain(); await service.dispose(); }); @@ -120,7 +120,7 @@ void main() { // Then expect(service.keychain, isNotNull); - await service.removeCurrentAccount(); + await service.removeCurrentKeychain(); expect(service.keychain, isNull); expect(await currentKeychain.isEmpty, isTrue); diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile index bdea955196..bdc0c26543 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile @@ -21,7 +21,7 @@ src: package-test: FROM +src - ENV APP_URL http://app:80 + ENV APP_URL http://test-app:80 RUN mkdir /results VOLUME /results ENTRYPOINT ["/bin/sh", "-c", "/usr/bin/xvfb-run --auto-servernum npm test", ""] @@ -52,7 +52,7 @@ nightly-test: --compose compose.yml \ --load test-app:latest=(+package-app) \ --load test:latest=(+package-test) \ - --service app \ + --service test-app \ --allow-privileged RUN docker run --network=default_default --name=test test:latest && \ diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml index 3a79143e11..eea27db656 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml @@ -1,6 +1,6 @@ version: "3" services: - app: + test-app: image: test-app:latest ports: - 8000:80 \ No newline at end of file diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json index 64d4f46360..cba1e952f6 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json @@ -21,7 +21,7 @@ }, "homepage": "https://github.com/input-output-hk/catalyst-voices#readme", "devDependencies": { - "@playwright/test": "^1.45.3", + "@playwright/test": "^1.48.0", "@types/node": "^20.14.12" }, "dependencies": { @@ -30,7 +30,7 @@ "fs-extra": "^11.2.0", "install": "^0.13.0", "node-fetch": "^2.6.7", - "playwright": "^1.45.3", + "playwright": "^1.48.0", "unzip-crx-3": "^0.2.0" } } diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart index 286c22300d..059f16ab03 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart @@ -42,6 +42,7 @@ class _VoicesTextFieldExampleState extends State { suffixIcon: VoicesAssets.icons.chevronDown.buildIcon(), ), maxLength: 200, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -56,6 +57,7 @@ class _VoicesTextFieldExampleState extends State { ), maxLength: 200, enabled: false, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -70,6 +72,7 @@ class _VoicesTextFieldExampleState extends State { suffixIcon: Icon(Icons.error_outline), ), maxLength: 200, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -83,6 +86,7 @@ class _VoicesTextFieldExampleState extends State { ), maxLength: 200, validator: VoicesTextFieldValidationResult.success(), + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -97,6 +101,7 @@ class _VoicesTextFieldExampleState extends State { maxLength: 200, validator: VoicesTextFieldValidationResult.warning('Warning message'), + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -111,6 +116,7 @@ class _VoicesTextFieldExampleState extends State { maxLength: 200, validator: VoicesTextFieldValidationResult.error('Error message'), + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -125,6 +131,7 @@ class _VoicesTextFieldExampleState extends State { maxLength: 200, validator: VoicesTextFieldValidationResult.success(), enabled: false, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -140,6 +147,7 @@ class _VoicesTextFieldExampleState extends State { validator: VoicesTextFieldValidationResult.warning('Warning message'), enabled: false, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -155,6 +163,7 @@ class _VoicesTextFieldExampleState extends State { validator: VoicesTextFieldValidationResult.error('Error message'), enabled: false, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -188,6 +197,7 @@ class _VoicesTextFieldExampleState extends State { } }, maxLength: 200, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -202,6 +212,7 @@ class _VoicesTextFieldExampleState extends State { maxLength: 200, minLines: 6, maxLines: 10, + onFieldSubmitted: (value) {}, ), ), SizedBox( @@ -212,6 +223,7 @@ class _VoicesTextFieldExampleState extends State { labelText: 'Resizable', ), maxLines: null, + onFieldSubmitted: (value) {}, ), ), ],