Skip to content

fix(cat-voices): recovering keychain while finishing registration #2737

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

Merged
merged 3 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -100,7 +100,7 @@ class _RegistrationDialogState extends State<RegistrationDialog> {
case RecoverRegistration():
_cubit.goToStep(const RecoverWithSeedPhraseStep());
case ContinueRegistration():
_cubit.recoverProgress();
unawaited(_cubit.recoverProgress());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,10 @@ final class KeychainCreationCubit extends Cubit<KeychainStateData>
}
}

KeychainProgress createRecoverProgress() {
return KeychainProgress(
seedPhrase: _seedPhrase!,
password: password.value,
);
void clearSeedPhrase() {
_seedPhrase = SeedPhrase();
setSeedPhraseStored(false);
setUserSeedPhraseWords([]);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,11 @@ final class RegistrationCubit extends Cubit<RegistrationState> with BlocErrorEmi
}
}

void recoverProgress() {
Future<void> recoverProgress() async {
_accountId = null;
_keychain = null;
_transaction = null;

final progress = _progressNotifier.value;
final baseProfileProgress = progress.baseProfileProgress;
final keychainProgress = progress.keychainProgress;
Expand All @@ -338,15 +342,27 @@ final class RegistrationCubit extends Cubit<RegistrationState> with BlocErrorEmi
}

if (keychainProgress != null) {
_keychainCreationCubit
..recoverSeedPhrase(keychainProgress.seedPhrase)
..recoverPassword(keychainProgress.password);
try {
_keychain = await _registrationService.getKeychain(keychainProgress.keychainId);

_keychainCreationCubit
..recoverSeedPhrase(keychainProgress.seedPhrase)
..recoverPassword(keychainProgress.password);
} on RegistrationRecoverKeychainNotFoundException catch (_) {
_keychain = null;

_keychainCreationCubit
..clearSeedPhrase()
..recoverPassword('');

emitError(const LocalizedRecoverKeychainNotFoundException());
}
}

final step = AccountCreateProgressStep(
completedSteps: [
if (baseProfileProgress != null) AccountCreateStepType.baseProfile,
if (keychainProgress != null) AccountCreateStepType.keychain,
if (_keychain != null) AccountCreateStepType.keychain,
],
);
_goToStep(step);
Expand Down Expand Up @@ -608,7 +624,26 @@ final class RegistrationCubit extends Cubit<RegistrationState> with BlocErrorEmi
keychainProgress: const Optional.empty(),
);
case AccountCreateStepType.keychain:
final data = _keychainCreationCubit.createRecoverProgress();
final keychain = _keychain;
final seedPhrase = _keychainCreationCubit.seedPhrase;
final password = _keychainCreationCubit.password;

final missingDataErrors = <LocalizedException>[
if (keychain == null) const LocalizedKeychainNotFoundException(),
if (seedPhrase == null) const LocalizedSeedPhraseNotFoundException(),
if (password.isNotValid) const LocalizedUnlockPasswordNotFoundException(),
];

if (missingDataErrors.isNotEmpty) {
emitError(LocalizedSaveRegistrationProgressException(reasons: missingDataErrors));
return;
}

final data = KeychainProgress(
keychainId: keychain!.id,
seedPhrase: seedPhrase!,
password: password.value,
);
_progressNotifier.value = _progressNotifier.value.copyWith(
keychainProgress: Optional(data),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:mocktail/mocktail.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:uuid_plus/uuid_plus.dart';

void main() {
late final KeychainProvider keychainProvider;
Expand Down Expand Up @@ -144,6 +145,7 @@ void main() {
'session is in Visitor state with correct flag', () async {
// Given
final keychainProgress = KeychainProgress(
keychainId: const Uuid().v4(),
seedPhrase: SeedPhrase(),
password: 'Test1234',
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,26 @@
"@registrationKeychainNotFound": {
"description": "Error message shown when attempting to prepare registration transaction but keychain was not found or locked. Should not happen."
},
"registrationRecoverKeychainNotFound": "Keychain was not found while recovering.",
"@registrationRecoverKeychainNotFound": {
"description": "Error message shown when recovering account but keychain was not found locally"
},
"registrationSaveProgressException": "Could not save progress",
"@registrationSaveProgressException": {
"description": "Error message shown when data is malformed or missing. Should not happen."
},
"registrationSaveProgressKeychainNotFoundException": "Keychain not found",
"@registrationSaveProgressKeychainNotFoundException": {
"description": "Error message shown when could not save keychain progress"
},
"registrationSaveProgressSeedPhraseNotFoundException": "Keychain not found",
"@registrationSaveProgressSeedPhraseNotFoundException": {
"description": "Error message shown when could not save seed phrase progress"
},
"registrationSaveProgressUnlockPasswordNotFoundException": "Keychain not found",
"@registrationSaveProgressUnlockPasswordNotFoundException": {
"description": "Error message shown when could not save unlock password progress"
},
"registrationUnlockPasswordNotFound": "Password was not found. Make sure valid password was created.",
"@registrationUnlockPasswordNotFound": {
"description": "Error message shown when attempting to register or recover account but password was not found"
Expand Down Expand Up @@ -1476,7 +1496,7 @@
"maximumAsk": "Maximum Ask",
"minimumAsk": "Minimum Ask",
"ideaJourney": "Idea Journey",
"ideaJourneyDescription": "#### Ideas come to life in Catalyst through the key stages below. For the full timeline, deadlines and latest updates, visit the [fund timeline]({link}) Gitbook page.",
"ideaJourneyDescription": "#### Ideas come to life in Catalyst through the key stages below. For the full timeline, deadlines and latest updates, visit the [fund timeline]({link}) Gitbook page.",
"@ideaJourneyDescription": {
"description": "Description for the idea journey section",
"placeholders": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ final class RegistrationTransactionException extends RegistrationException {
String toString() => 'RegistrationTransactionException';
}

/// An exception thrown when recovering registration but keychain was not found locally.
final class RegistrationRecoverKeychainNotFoundException extends RegistrationException {
const RegistrationRecoverKeychainNotFoundException();

@override
String toString() => 'RegistrationRecoverKeychainNotFoundException';
}

/// An exception thrown when attempting to register and the transaction fails.
final class RegistrationUnknownException extends RegistrationException {
const RegistrationUnknownException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ final class BaseProfileProgress extends Equatable {
}

final class KeychainProgress extends Equatable {
final String keychainId;
final SeedPhrase seedPhrase;
final String password;

const KeychainProgress({
required this.keychainId,
required this.seedPhrase,
required this.password,
});

@override
List<Object?> get props => [
keychainId,
seedPhrase,
password,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ abstract interface class RegistrationService {
/// Returns the available cardano wallet extensions.
Future<List<CardanoWallet>> getCardanoWallets();

/// Tries to find keychain with matching [id] and return it.
///
/// Throws error if not found.
Future<Keychain> getKeychain(String id);

/// Loads the wallet balance for given [address].
Future<Coin> getWalletBalance({
required SeedPhrase seedPhrase,
Expand Down Expand Up @@ -145,6 +150,17 @@ final class RegistrationServiceImpl implements RegistrationService {
return _cardano.getWallets();
}

@override
Future<Keychain> getKeychain(String id) async {
final exists = await _keychainProvider.exists(id);

if (!exists) {
throw const RegistrationRecoverKeychainNotFoundException();
}

return _keychainProvider.get(id);
}

@override
Future<Coin> getWalletBalance({
required SeedPhrase seedPhrase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@ import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.da
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';

final class LocalizedKeychainNotFoundException extends LocalizedRegistrationException {
const LocalizedKeychainNotFoundException();

@override
String message(BuildContext context) {
return context.l10n.registrationSaveProgressKeychainNotFoundException;
}
}

final class LocalizedRecoverAccountNotFound extends LocalizedRegistrationException {
const LocalizedRecoverAccountNotFound();

@override
String message(BuildContext context) => context.l10n.registrationAccountNotFound;
}

final class LocalizedRecoverKeychainNotFoundException extends LocalizedRegistrationException {
const LocalizedRecoverKeychainNotFoundException();

@override
String message(BuildContext context) => context.l10n.registrationRecoverKeychainNotFound;
}

/// A [LocalizedException] describing an error during a user registration.
sealed class LocalizedRegistrationException extends LocalizedException {
const LocalizedRegistrationException();
Expand All @@ -29,6 +46,8 @@ sealed class LocalizedRegistrationException extends LocalizedException {
LocalizedRegistrationNetworkIdMismatchException(
targetNetworkId: targetNetworkId,
),
RegistrationRecoverKeychainNotFoundException() =>
const LocalizedRegistrationKeychainNotFoundException(),
};
}
}
Expand Down Expand Up @@ -106,6 +125,44 @@ final class LocalizedRegistrationWalletNotFoundException extends LocalizedRegist
String message(BuildContext context) => context.l10n.registrationWalletNotFound;
}

final class LocalizedSaveRegistrationProgressException extends LocalizedRegistrationException {
final List<LocalizedException> reasons;

const LocalizedSaveRegistrationProgressException({
this.reasons = const [],
});

@override
List<Object?> get props => [reasons];

@override
String message(BuildContext context) {
final buffer = StringBuffer(context.l10n.registrationSaveProgressException);

final reasons = this.reasons.mapIndexed((index, element) {
final suffix = index == 0 ? '.' : ',';
final message = element.message(context);

return '$suffix $message';
});

for (final reason in reasons) {
buffer.write(reason);
}

return buffer.toString();
}
}

final class LocalizedSeedPhraseNotFoundException extends LocalizedRegistrationException {
const LocalizedSeedPhraseNotFoundException();

@override
String message(BuildContext context) {
return context.l10n.registrationSaveProgressSeedPhraseNotFoundException;
}
}

final class LocalizedSeedPhraseWordsDoNotMatchException extends LocalizedRegistrationException {
const LocalizedSeedPhraseWordsDoNotMatchException();

Expand All @@ -115,6 +172,15 @@ final class LocalizedSeedPhraseWordsDoNotMatchException extends LocalizedRegistr
}
}

final class LocalizedUnlockPasswordNotFoundException extends LocalizedRegistrationException {
const LocalizedUnlockPasswordNotFoundException();

@override
String message(BuildContext context) {
return context.l10n.registrationSaveProgressUnlockPasswordNotFoundException;
}
}

final class LocalizedWalletLinkException extends LocalizedRegistrationException {
final WalletApiErrorCode code;

Expand Down
Loading