diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index 0055f8912de..da29f091551 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -41,7 +41,6 @@ final class Dependencies extends DependencyProvider { () { return SessionCubit( get(), - get(), get(), get(), get(), @@ -95,7 +94,13 @@ final class Dependencies extends DependencyProvider { ) ..registerLazySingleton(ProposalRepository.new) ..registerLazySingleton(CampaignRepository.new) - ..registerLazySingleton(ConfigRepository.new); + ..registerLazySingleton(ConfigRepository.new) + ..registerLazySingleton(() { + return UserRepository( + get(), + get(), + ); + }); } void _registerServices() { @@ -111,7 +116,6 @@ final class Dependencies extends DependencyProvider { }); registerLazySingleton(Downloader.new); registerLazySingleton(() => CatalystCardano.instance); - registerLazySingleton(SecureUserStorage.new); registerLazySingleton( RegistrationProgressNotifier.new, ); @@ -126,14 +130,11 @@ final class Dependencies extends DependencyProvider { registerLazySingleton( () { return UserService( - keychainProvider: get(), - userStorage: get(), - dummyUserFactory: get(), + userRepository: get(), ); }, dispose: (service) => unawaited(service.dispose()), ); - registerLazySingleton(DummyUserFactory.new); registerLazySingleton(AccessControl.new); registerLazySingleton(() { return CampaignService( @@ -155,5 +156,6 @@ final class Dependencies extends DependencyProvider { void _registerStorages() { registerLazySingleton(FlutterSecureStorage.new); registerLazySingleton(SharedPreferencesAsync.new); + registerLazySingleton(SecureUserStorage.new); } } diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart b/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart index cba7cec290a..292653edb1d 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart @@ -1,6 +1,6 @@ import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,7 +42,7 @@ class _ToggleStateTextState extends State { await sessionBloc .switchToDummyAccount() - .then((_) => sessionBloc.unlock(DummyUserFactory.dummyUnlockFactor)); + .then((_) => sessionBloc.unlock(Account.dummyUnlockFactor)); }; } diff --git a/catalyst_voices/melos.yaml b/catalyst_voices/melos.yaml index cb32bb8b8b2..367385f9d2c 100644 --- a/catalyst_voices/melos.yaml +++ b/catalyst_voices/melos.yaml @@ -159,6 +159,16 @@ scripts: The catalyst_voices_repositories is skipped because to run a build_runner there you must generate first swagger docs (see related Earthfile). + build_runner_repository: + run: | + melos exec -c 1 \ + --depends-on="build_runner" \ + --scope="catalyst_voices_repositories" -- \ + dart run build_runner build --delete-conflicting-outputs \ + --build-filter="lib/src/dto/*" + description: | + Run `build_runner` in catalyst_voices_repositories package only in selected folders + metrics: run: | melos exec -c 1 -- \ 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 df6bbbc3c6f..293531a2004 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 @@ -141,13 +141,15 @@ final class RecoverCubit extends Cubit } final lockFactor = PasswordLockFactor(password.value); - - await _registrationService.createKeychainFor( - account: account, + final masterKey = await _registrationService.deriveMasterKey( seedPhrase: seedPhrase, - lockFactor: lockFactor, ); + final keychain = account.keychain; + await keychain.setLock(lockFactor); + await keychain.unlock(lockFactor); + await keychain.setMasterKey(masterKey); + await _userService.useAccount(account); return true; @@ -162,7 +164,7 @@ final class RecoverCubit extends Cubit Future reset() async { final recoveredAccount = _recoveredAccount; if (recoveredAccount != null) { - await _userService.removeKeychain(recoveredAccount.keychainId); + await _userService.removeAccount(recoveredAccount); } _recoveredAccount = null; 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 e833d3b8b52..7c355ae7251 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 @@ -12,7 +12,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; final class SessionCubit extends Cubit with BlocErrorEmitterMixin { final UserService _userService; - final DummyUserFactory _dummyUserFactory; final RegistrationService _registrationService; final RegistrationProgressNotifier _registrationProgressNotifier; final AccessControl _accessControl; @@ -20,32 +19,23 @@ final class SessionCubit extends Cubit final _logger = Logger('SessionCubit'); - bool _hasKeychain = false; - bool _isUnlocked = false; Account? _account; AdminToolsState _adminToolsState; - StreamSubscription? _keychainSub; StreamSubscription? _keychainUnlockedSub; StreamSubscription? _accountSub; StreamSubscription? _adminToolsSub; SessionCubit( this._userService, - this._dummyUserFactory, this._registrationService, this._registrationProgressNotifier, this._accessControl, this._adminTools, ) : _adminToolsState = _adminTools.state, super(const VisitorSessionState(isRegistrationInProgress: false)) { - _keychainSub = _userService.watchKeychain - .map((keychain) => keychain != null) - .distinct() - .listen(_onHasKeychainChanged); - - _keychainUnlockedSub = _userService.watchKeychain - .transform(KeychainToUnlockTransformer()) + _keychainUnlockedSub = _userService.watchAccount + .transform(AccountToKeychainUnlockTransformer()) .distinct() .listen(_onActiveKeychainUnlockChanged); @@ -56,43 +46,39 @@ final class SessionCubit extends Cubit _adminToolsSub = _adminTools.stream.listen(_onAdminToolsChanged); } - Future unlock(LockFactor lockFactor) { - return _userService.keychain!.unlock(lockFactor); + Future unlock(LockFactor lockFactor) async { + final keychain = _userService.account?.keychain; + if (keychain == null) { + return false; + } + + return keychain.unlock(lockFactor); } Future lock() async { - await _userService.keychain!.lock(); + await _userService.account?.keychain.lock(); } - Future removeKeychain() { - return _userService.removeCurrentKeychain(); + Future removeKeychain() async { + final account = _userService.account; + if (account != null) { + await _userService.removeAccount(account); + } } Future switchToDummyAccount() async { - final keychains = await _userService.keychains; - final dummyKeychain = keychains.firstWhereOrNull( - (keychain) => keychain.id == DummyUserFactory.dummyKeychainId, - ); - - if (dummyKeychain != null) { - await _userService.useKeychain(dummyKeychain.id); + final account = _userService.account; + if (account?.isDummy ?? false) { return; } - final account = await _registrationService.registerTestAccount( - keychainId: DummyUserFactory.dummyKeychainId, - seedPhrase: DummyUserFactory.dummySeedPhrase, - lockFactor: DummyUserFactory.dummyUnlockFactor, - ); + final dummyAccount = await _getDummyAccount(); - await _userService.useAccount(account); + await _userService.useAccount(dummyAccount); } @override Future close() async { - await _keychainSub?.cancel(); - _keychainSub = null; - await _keychainUnlockedSub?.cancel(); _keychainUnlockedSub = null; @@ -108,24 +94,17 @@ final class SessionCubit extends Cubit return super.close(); } - void _onHasKeychainChanged(bool hasKeychain) { - _logger.fine('Has keychain changed [$hasKeychain]'); + void _onActiveAccountChanged(Account? account) { + _logger.fine('Active account changed [$account]'); + + _account = account; - _hasKeychain = hasKeychain; _updateState(); } void _onActiveKeychainUnlockChanged(bool isUnlocked) { _logger.fine('Keychain unlock changed [$isUnlocked]'); - _isUnlocked = isUnlocked; - _updateState(); - } - - void _onActiveAccountChanged(Account? account) { - _logger.fine('Active account changed [$account]'); - - _account = account; _updateState(); } @@ -142,18 +121,23 @@ final class SessionCubit extends Cubit void _updateState() { if (_adminToolsState.enabled) { - emit(_createMockedSessionState()); + unawaited( + _createMockedSessionState().then((value) { + if (!isClosed) { + emit(value); + } + }), + ); } else { emit(_createSessionState()); } } SessionState _createSessionState() { - final hasKeychain = _hasKeychain; - final isUnlocked = _isUnlocked; final account = _account; + final isUnlocked = _account?.keychain.lastIsUnlocked ?? false; - if (!hasKeychain) { + if (account == null) { final isEmpty = _registrationProgressNotifier.value.isEmpty; return VisitorSessionState(isRegistrationInProgress: !isEmpty); } @@ -174,11 +158,14 @@ final class SessionCubit extends Cubit ); } - SessionState _createMockedSessionState() { + Future _createMockedSessionState() async { switch (_adminToolsState.sessionStatus) { case SessionStatus.actor: + // TODO(damian-molinski): Limiting exposed Account so its not future. + final dummyAccount = await _getDummyAccount(); + return ActiveAccountSessionState( - account: _dummyUserFactory.buildDummyAccount(), + account: dummyAccount, spaces: Space.values, overallSpaces: Space.values, spacesShortcuts: AccessControl.allSpacesShortcutsActivators, @@ -189,4 +176,16 @@ final class SessionCubit extends Cubit return const VisitorSessionState(isRegistrationInProgress: false); } } + + Future _getDummyAccount() async { + final dummyAccount = + _userService.accounts.firstWhereOrNull((e) => e.isDummy); + + return dummyAccount ?? + await _registrationService.registerTestAccount( + keychainId: Account.dummyKeychainId, + seedPhrase: Account.dummySeedPhrase, + lockFactor: Account.dummyUnlockFactor, + ); + } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart index b7010c04a69..08d97a3262a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart @@ -54,6 +54,7 @@ final class GuestSessionState extends SessionState { /// The user has registered and unlocked the keychain. final class ActiveAccountSessionState extends SessionState { + // TODO(damian-molinski): Try limiting exposed Account to something smaller. final Account? account; @override final List spaces; 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 19f166a2184..c4acaa934ba 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 @@ -1,6 +1,8 @@ import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,9 +13,8 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p void main() { late final KeychainProvider keychainProvider; - late final UserStorage userStorage; + late final UserRepository userRepository; - late final DummyUserFactory dummyUserFactory; late final UserService userService; late final RegistrationService registrationService; late final RegistrationProgressNotifier notifier; @@ -33,15 +34,15 @@ void main() { sharedPreferences: SharedPreferencesAsync(), cacheConfig: const CacheConfig(), ); - userStorage = SecureUserStorage(); + userRepository = UserRepository( + SecureUserStorage(), + keychainProvider, + ); - dummyUserFactory = DummyUserFactory(); userService = UserService( - keychainProvider: keychainProvider, - userStorage: userStorage, - dummyUserFactory: dummyUserFactory, + userRepository: userRepository, ); - registrationService = _MockRegistrationService(); + registrationService = _MockRegistrationService(keychainProvider); notifier = RegistrationProgressNotifier(); accessControl = const AccessControl(); }); @@ -52,7 +53,6 @@ void main() { sessionCubit = SessionCubit( userService, - dummyUserFactory, registrationService, notifier, accessControl, @@ -63,6 +63,11 @@ void main() { tearDown(() async { await sessionCubit.close(); + final user = await userService.getUser(); + for (final account in user.accounts) { + await userService.removeAccount(account); + } + await const FlutterSecureStorage().deleteAll(); await SharedPreferencesAsync().clear(); @@ -70,14 +75,17 @@ void main() { }); group(SessionCubit, () { - test('when no keychain is found session is in Visitor state', () async { + test('when no account is found session is in Visitor state', () async { // Given // When - await userService.removeCurrentKeychain(); + final account = userService.account; + if (account != null) { + await userService.removeAccount(account); + } // Then - expect(userService.keychain, isNull); + expect(userService.account, isNull); expect(sessionCubit.state, isA()); }); @@ -85,13 +93,16 @@ void main() { // Given // When - await userService.removeCurrentKeychain(); + final account = userService.account; + if (account != null) { + await userService.removeAccount(account); + } // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNull); + expect(userService.account, isNull); expect(sessionCubit.state, isA()); expect( sessionCubit.state, @@ -111,13 +122,16 @@ void main() { // When notifier.value = RegistrationProgress(keychainProgress: keychainProgress); - await userService.removeCurrentKeychain(); + final account = userService.account; + if (account != null) { + await userService.removeAccount(account); + } // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNull); + expect(userService.account, isNull); expect(sessionCubit.state, isA()); expect( sessionCubit.state, @@ -135,13 +149,16 @@ void main() { await keychain.setLock(lockFactor); await keychain.lock(); - await userService.useKeychain(keychainId); + final account = Account.dummy(keychain: keychain); + + await userService.useAccount(account); // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNotNull); + expect(userService.account, isNotNull); + expect(userService.account?.id, account.id); expect(sessionCubit.state, isNot(isA())); expect(sessionCubit.state, isA()); }); @@ -155,14 +172,16 @@ void main() { final keychain = await keychainProvider.create(keychainId); await keychain.setLock(lockFactor); - await userService.useKeychain(keychainId); - await userService.keychain?.unlock(lockFactor); + final account = Account.dummy(keychain: keychain); + + await userService.useAccount(account); + await account.keychain.unlock(lockFactor); // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNotNull); + expect(userService.account, isNotNull); expect(sessionCubit.state, isNot(isA())); expect(sessionCubit.state, isNot(isA())); expect(sessionCubit.state, isA()); @@ -185,4 +204,22 @@ void main() { }); } -class _MockRegistrationService extends Mock implements RegistrationService {} +class _MockRegistrationService extends Mock implements RegistrationService { + final KeychainProvider keychainProvider; + + _MockRegistrationService(this.keychainProvider); + + @override + Future registerTestAccount({ + required String keychainId, + required SeedPhrase seedPhrase, + required LockFactor lockFactor, + }) async { + final keychain = await keychainProvider.create(keychainId); + + await keychain.setLock(lockFactor); + await keychain.unlock(lockFactor); + + return Account.dummy(keychain: keychain); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart index 64df6598df5..fe3b073d6e0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart @@ -1,13 +1,6 @@ -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; -part 'app_config.g.dart'; - -@JsonSerializable() final class AppConfig extends Equatable { - // Not ready. See comment below. - @JsonKey(includeFromJson: false, includeToJson: false) final SentryConfig sentry; final CacheConfig cache; @@ -16,17 +9,10 @@ final class AppConfig extends Equatable { this.cache = const CacheConfig(), }); - factory AppConfig.fromJson(Map json) { - return _$AppConfigFromJson(json); - } - - Map toJson() => _$AppConfigToJson(this); - @override List get props => [sentry, cache]; } -@JsonSerializable() final class SentryConfig extends Equatable { final String dns; final String environment; @@ -39,12 +25,6 @@ final class SentryConfig extends Equatable { this.release = '1.0.0', }); - factory SentryConfig.fromJson(Map json) { - return _$SentryConfigFromJson(json); - } - - Map toJson() => _$SentryConfigToJson(this); - @override List get props => [ dns, @@ -53,7 +33,6 @@ final class SentryConfig extends Equatable { ]; } -@JsonSerializable() final class CacheConfig extends Equatable { final ExpiryDuration expiryDuration; @@ -61,31 +40,17 @@ final class CacheConfig extends Equatable { this.expiryDuration = const ExpiryDuration(), }); - factory CacheConfig.fromJson(Map json) { - return _$CacheConfigFromJson(json); - } - - Map toJson() => _$CacheConfigToJson(this); - @override List get props => [expiryDuration]; } -@JsonSerializable() final class ExpiryDuration extends Equatable { - @DurationConverter() final Duration keychainUnlock; const ExpiryDuration({ this.keychainUnlock = const Duration(hours: 1), }); - factory ExpiryDuration.fromJson(Map json) { - return _$ExpiryDurationFromJson(json); - } - - Map toJson() => _$ExpiryDurationToJson(this); - @override List get props => [keychainUnlock]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart index 6fcdaf5545d..7e623e21517 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart @@ -1,28 +1,94 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:equatable/equatable.dart'; /// Defines singular account used by [User] (physical person). /// One [User] may have multiple [Account]'s. final class Account extends Equatable { - final String keychainId; + final Keychain keychain; final Set roles; final WalletInfo walletInfo; + /// Whether this account is being used. + final bool isActive; + + /// When account registration transaction is posted on chain account is + /// "provisional". This means backend does not yet know about it because + /// transaction was not yet read. + final bool isProvisional; + + static const dummyKeychainId = 'TestUserKeychainID'; + static const dummyUnlockFactor = PasswordLockFactor('Test1234'); + static final dummySeedPhrase = SeedPhrase.fromMnemonic( + 'few loyal swift champion rug peace dinosaur ' + 'erase bacon tone install universe', + ); + const Account({ - required this.keychainId, + required this.keychain, required this.roles, required this.walletInfo, + this.isActive = false, + this.isProvisional = true, }); + factory Account.dummy({ + required Keychain keychain, + bool isActive = false, + }) { + return Account( + keychain: keychain, + roles: const { + AccountRole.voter, + AccountRole.proposer, + }, + walletInfo: WalletInfo( + metadata: const WalletMetadata(name: 'Dummy Wallet', icon: null), + balance: Coin.fromAda(10), + /* cSpell:disable */ + address: ShelleyAddress.fromBech32( + 'addr_test1vzpwq95z3xyum8vqndgdd' + '9mdnmafh3djcxnc6jemlgdmswcve6tkw', + ), + /* cSpell:enable */ + ), + isActive: isActive, + isProvisional: true, + ); + } + + String get id => keychain.id; + // Note. this is not defined yet what we will show here. String get acronym => 'A'; bool get isAdmin => true; + bool get isDummy => keychain.id == dummyKeychainId; + + Account copyWith({ + Keychain? keychain, + Set? roles, + WalletInfo? walletInfo, + bool? isActive, + bool? isProvisional, + }) { + return Account( + keychain: keychain ?? this.keychain, + roles: roles ?? this.roles, + walletInfo: walletInfo ?? this.walletInfo, + isActive: isActive ?? this.isActive, + isProvisional: isProvisional ?? this.isProvisional, + ); + } + @override List get props => [ - keychainId, + keychain.id, roles, walletInfo, + isActive, + isProvisional, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart index f7a8c1c6cfd..1a815a469e2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart @@ -1,4 +1,5 @@ -import 'package:catalyst_voices_models/src/user/account.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; /// Defines user or the app. @@ -9,12 +10,46 @@ final class User extends Equatable { required this.accounts, }); - /// Just syntax sugar for [activeAccount]. - Account get account => activeAccount; + Account? get activeAccount { + return accounts.singleWhereOrNull((account) => account.isActive); + } - // Note. At the moment we support only single user profile but later - // this may change and this implementation with it. - Account get activeAccount => accounts.single; + User useAccount({required String id}) { + if (this.accounts.none((e) => e.id == id)) { + throw ArgumentError('Account[$id] is not on the list'); + } + + final accounts = [...this.accounts] + .map((e) => e.copyWith(isActive: e.id == id)) + .toList(); + + return copyWith(accounts: accounts); + } + + bool hasAccount({required String id}) { + return accounts.any((element) => element.id == id); + } + + User addAccount(Account account) { + final accounts = [...this.accounts, account]; + + return copyWith(accounts: accounts); + } + + User removeAccount({required String id}) { + final accounts = + [...this.accounts].where((element) => element.id != id).toList(); + + return copyWith(accounts: accounts); + } + + User copyWith({ + List? accounts, + }) { + return User( + accounts: accounts ?? this.accounts, + ); + } @override List get props => [ diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml index 6cd4eb2eac8..2103e4b4976 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml @@ -16,12 +16,9 @@ dependencies: collection: ^1.18.0 convert: ^3.1.1 equatable: ^2.0.7 - json_annotation: ^4.9.0 meta: ^1.10.0 password_strength: ^0.2.0 dev_dependencies: - build_runner: ^2.4.12 catalyst_analysis: ^2.0.0 - json_serializable: ^6.9.0 test: ^1.24.9 diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml b/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml index 21f460f83d8..ceb38a39b0a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml @@ -20,4 +20,7 @@ targets: - SimpleProposal$ProposalCategory - SimpleProposal$Proposer - CommunityChoiceProposal$ProposalCategory - - CommunityChoiceProposal$Proposer \ No newline at end of file + - CommunityChoiceProposal$Proposer + json_serializable: + options: + explicit_to_json: true diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart index 55452597fc8..c3a83f1d2c1 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart @@ -2,3 +2,5 @@ export 'campaign/campaign_repository.dart'; export 'config/config_repository.dart' show ConfigRepository; export 'proposal/proposal_repository.dart'; export 'transaction/transaction_config_repository.dart'; +export 'user/user_repository.dart' show UserRepository; +export 'user/user_storage.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart new file mode 100644 index 00000000000..66ae3bdfb7d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart @@ -0,0 +1,121 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/utils/json_converters.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'app_config_dto.g.dart'; + +@JsonSerializable() +final class AppConfigDto { + @JsonKey(includeToJson: false, includeFromJson: false) + final SentryConfigDto? sentry; + final CacheConfigDto cache; + + const AppConfigDto({ + this.sentry, + this.cache = const CacheConfigDto(), + }); + + AppConfigDto.fromModel(AppConfig data) + : this( + sentry: SentryConfigDto.fromModel(data.sentry), + cache: CacheConfigDto.fromModel(data.cache), + ); + + factory AppConfigDto.fromJson(Map json) { + return _$AppConfigDtoFromJson(json); + } + + Map toJson() => _$AppConfigDtoToJson(this); + + AppConfig toModel() { + return AppConfig( + sentry: sentry?.toModel() ?? const SentryConfig(), + cache: cache.toModel(), + ); + } +} + +@JsonSerializable() +final class SentryConfigDto { + final String dns; + final String environment; + final String release; + + const SentryConfigDto({ + required this.dns, + required this.environment, + required this.release, + }); + + SentryConfigDto.fromModel(SentryConfig data) + : this( + dns: data.dns, + environment: data.environment, + release: data.release, + ); + + factory SentryConfigDto.fromJson(Map json) { + return _$SentryConfigDtoFromJson(json); + } + + Map toJson() => _$SentryConfigDtoToJson(this); + + SentryConfig toModel() { + return SentryConfig( + dns: dns, + environment: environment, + release: release, + ); + } +} + +@JsonSerializable() +final class CacheConfigDto { + final ExpiryDurationDto expiryDuration; + + const CacheConfigDto({ + this.expiryDuration = const ExpiryDurationDto(), + }); + + CacheConfigDto.fromModel(CacheConfig data) + : this( + expiryDuration: ExpiryDurationDto.fromModel(data.expiryDuration), + ); + + factory CacheConfigDto.fromJson(Map json) { + return _$CacheConfigDtoFromJson(json); + } + + Map toJson() => _$CacheConfigDtoToJson(this); + + CacheConfig toModel() { + return CacheConfig( + expiryDuration: expiryDuration.toModel(), + ); + } +} + +@JsonSerializable() +final class ExpiryDurationDto { + @DurationConverter() + final Duration keychainUnlock; + + const ExpiryDurationDto({ + this.keychainUnlock = const Duration(hours: 1), + }); + + ExpiryDurationDto.fromModel(ExpiryDuration data) + : this(keychainUnlock: data.keychainUnlock); + + factory ExpiryDurationDto.fromJson(Map json) { + return _$ExpiryDurationDtoFromJson(json); + } + + Map toJson() => _$ExpiryDurationDtoToJson(this); + + ExpiryDuration toModel() { + return ExpiryDuration( + keychainUnlock: keychainUnlock, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart new file mode 100644 index 00000000000..14236050005 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart @@ -0,0 +1,159 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/utils/json_converters.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_dto.g.dart'; + +@JsonSerializable() +final class UserDto { + final List accounts; + final String? activeKeychainId; + + UserDto({ + this.accounts = const [], + this.activeKeychainId, + }); + + UserDto.fromModel(User data) + : this( + accounts: data.accounts.map(AccountDto.fromModel).toList(), + activeKeychainId: data.activeAccount?.keychain.id, + ); + + factory UserDto.fromJson(Map json) { + return _$UserDtoFromJson(json); + } + + Map toJson() => _$UserDtoToJson(this); + + Future toModel({ + required KeychainProvider keychainProvider, + }) async { + final accounts = []; + + for (final accountDto in this.accounts) { + final account = await accountDto.toModel( + activeKeychainId: activeKeychainId, + keychainProvider: keychainProvider, + ); + + accounts.add(account); + } + + return User( + accounts: accounts, + ); + } +} + +@JsonSerializable() +final class AccountDto { + final String keychainId; + final Set roles; + final AccountWalletInfoDto walletInfo; + final bool isProvisional; + + AccountDto({ + required this.keychainId, + required this.roles, + required this.walletInfo, + this.isProvisional = true, + }); + + AccountDto.fromModel(Account data) + : this( + keychainId: data.keychain.id, + roles: data.roles, + walletInfo: AccountWalletInfoDto.fromModel(data.walletInfo), + isProvisional: data.isProvisional, + ); + + factory AccountDto.fromJson(Map json) { + return _$AccountDtoFromJson(json); + } + + Map toJson() => _$AccountDtoToJson(this); + + Future toModel({ + required String? activeKeychainId, + required KeychainProvider keychainProvider, + }) async { + final keychain = await keychainProvider.get(keychainId); + + return Account( + keychain: keychain, + roles: roles, + walletInfo: walletInfo.toModel(), + isActive: keychainId == activeKeychainId, + isProvisional: isProvisional, + ); + } +} + +@JsonSerializable() +final class AccountWalletInfoDto { + final AccountWalletMetadataDto metadata; + @CoinConverter() + final Coin balance; + @ShelleyAddressConverter() + final ShelleyAddress address; + + AccountWalletInfoDto({ + required this.metadata, + required this.balance, + required this.address, + }); + + AccountWalletInfoDto.fromModel(WalletInfo data) + : this( + metadata: AccountWalletMetadataDto.fromModel(data.metadata), + balance: data.balance, + address: data.address, + ); + + factory AccountWalletInfoDto.fromJson(Map json) { + return _$AccountWalletInfoDtoFromJson(json); + } + + Map toJson() => _$AccountWalletInfoDtoToJson(this); + + WalletInfo toModel() { + return WalletInfo( + metadata: metadata.toModel(), + balance: balance, + address: address, + ); + } +} + +@JsonSerializable() +final class AccountWalletMetadataDto { + final String name; + final String? icon; + + AccountWalletMetadataDto({ + required this.name, + this.icon, + }); + + AccountWalletMetadataDto.fromModel(WalletMetadata data) + : this( + name: data.name, + icon: data.icon, + ); + + factory AccountWalletMetadataDto.fromJson(Map json) { + return _$AccountWalletMetadataDtoFromJson(json); + } + + Map toJson() => _$AccountWalletMetadataDtoToJson(this); + + WalletMetadata toModel() { + return WalletMetadata( + name: name, + icon: icon, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart new file mode 100644 index 00000000000..49ecb3dd5d7 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart @@ -0,0 +1,46 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/user_dto.dart'; +import 'package:catalyst_voices_repositories/src/user/user_storage.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +abstract interface class UserRepository { + factory UserRepository( + UserStorage storage, + KeychainProvider keychainProvider, + ) { + return UserRepositoryImpl( + storage, + keychainProvider, + ); + } + + Future getUser(); + + Future saveUser(User user); +} + +final class UserRepositoryImpl implements UserRepository { + final UserStorage _storage; + final KeychainProvider _keychainProvider; + + UserRepositoryImpl( + this._storage, + this._keychainProvider, + ); + + @override + Future getUser() async { + final dto = await _storage.readUser(); + + final user = await dto?.toModel(keychainProvider: _keychainProvider); + + return user ?? const User(accounts: []); + } + + @override + Future saveUser(User user) { + final dto = UserDto.fromModel(user); + + return _storage.writeUser(dto); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart new file mode 100644 index 00000000000..bf403a0c2fe --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_repositories/src/dto/user_dto.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +const _userKey = 'User'; + +abstract interface class UserStorage { + Future readUser(); + + Future writeUser(UserDto user); + + Future deleteUser(); +} + +final class SecureUserStorage extends SecureStorage implements UserStorage { + SecureUserStorage({ + super.secureStorage, + }); + + @override + Future readUser() async { + final encoded = await readString(key: _userKey); + if (encoded == null) { + return null; + } + + final decoded = json.decode(encoded) as Map; + + return UserDto.fromJson(decoded); + } + + @override + Future writeUser(UserDto user) async { + final encoded = json.encode(user.toJson()); + await writeString(encoded, key: _userKey); + } + + @override + Future deleteUser() async { + await delete(key: _userKey); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart new file mode 100644 index 00000000000..069d2ff7313 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart @@ -0,0 +1,33 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:json_annotation/json_annotation.dart'; + +final class DurationConverter implements JsonConverter { + const DurationConverter(); + + @override + Duration fromJson(int json) => Duration(seconds: json); + + @override + int toJson(Duration object) => object.inSeconds; +} + +final class CoinConverter implements JsonConverter { + const CoinConverter(); + + @override + Coin fromJson(int json) => Coin(json); + + @override + int toJson(Coin object) => object.value; +} + +final class ShelleyAddressConverter + implements JsonConverter { + const ShelleyAddressConverter(); + + @override + ShelleyAddress fromJson(String json) => ShelleyAddress.fromBech32(json); + + @override + String toJson(ShelleyAddress object) => object.toBech32(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart index 67ef01d8ce9..b4344585cd0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart @@ -1,15 +1,8 @@ export 'campaign/campaign_service.dart' show CampaignService; export 'config/config_service.dart' show ConfigService; export 'downloader/downloader.dart'; -export 'keychain/keychain.dart'; -export 'keychain/keychain_provider.dart'; -export 'keychain/keychain_transformers.dart'; -export 'keychain/vault_keychain.dart'; -export 'keychain/vault_keychain_provider.dart'; export 'proposal/proposal_service.dart' show ProposalService; export 'registration/registration_progress_notifier.dart'; export 'registration/registration_service.dart' show RegistrationService; export 'registration/registration_transaction_builder.dart'; -export 'user/dummy_user_service.dart' show DummyUserFactory; export 'user/user_service.dart' show UserService; -export 'user/user_storage.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart deleted file mode 100644 index 37e2101565d..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; - -final class KeychainToUnlockTransformer - extends StreamTransformerBase { - KeychainToUnlockTransformer(); - - @override - Stream bind(Stream stream) { - return Stream.eventTransformed( - stream, - _KeychainUnlockStreamSink.new, - ); - } -} - -final class _KeychainUnlockStreamSink implements EventSink { - final EventSink _outputSink; - StreamSubscription? _streamSub; - - _KeychainUnlockStreamSink(this._outputSink); - - @override - void add(Keychain? event) { - final stream = event?.watchIsUnlocked ?? Stream.value(false); - - unawaited(_streamSub?.cancel()); - _streamSub = stream.listen(_outputSink.add); - } - - @override - void addError(Object error, [StackTrace? stackTrace]) { - _outputSink.addError(error, stackTrace); - } - - @override - void close() { - unawaited(_streamSub?.cancel()); - _streamSub = null; - _outputSink.close(); - } -} 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 dc554670a86..d67454ad140 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 @@ -53,13 +53,6 @@ abstract interface class RegistrationService { required SeedPhrase seedPhrase, }); - /// Creates [Keychain] for given [account] with [lockFactor]. - Future createKeychainFor({ - required Account account, - required SeedPhrase seedPhrase, - required LockFactor lockFactor, - }); - /// Builds an unsigned registration transaction from given parameters. /// /// Throws a subclass of [RegistrationException] in case of a failure. @@ -151,11 +144,13 @@ final class RegistrationServiceImpl implements RegistrationService { // TODO(dtscalac): derive a key from the seed phrase and fetch // from the backend info about the registration (roles, wallet, etc). final roles = {AccountRole.root}; + final keychainId = const Uuid().v4(); + final keychain = await _keychainProvider.create(keychainId); // Note. with rootKey query backend for account details. return Account( - keychainId: keychainId, + keychain: keychain, roles: roles, walletInfo: WalletInfo( metadata: const WalletMetadata(name: 'Dummy Wallet'), @@ -165,23 +160,6 @@ final class RegistrationServiceImpl implements RegistrationService { ); } - @override - Future createKeychainFor({ - required Account account, - required SeedPhrase seedPhrase, - required LockFactor lockFactor, - }) async { - final keychainId = account.keychainId; - final masterKey = await deriveMasterKey(seedPhrase: seedPhrase); - - final keychain = await _keychainProvider.create(keychainId); - await keychain.setLock(lockFactor); - await keychain.unlock(lockFactor); - await keychain.setMasterKey(masterKey); - - return keychain; - } - @override Future prepareRegistration({ required CardanoWallet wallet, @@ -253,7 +231,7 @@ final class RegistrationServiceImpl implements RegistrationService { final address = await enabledWallet.getChangeAddress(); return Account( - keychainId: keychainId, + keychain: keychain, roles: roles, walletInfo: WalletInfo( metadata: WalletMetadata.fromCardanoWallet(wallet), @@ -283,7 +261,7 @@ final class RegistrationServiceImpl implements RegistrationService { await keychain.setMasterKey(masterKey); return Account( - keychainId: keychainId, + keychain: keychain, roles: roles, walletInfo: WalletInfo( metadata: const WalletMetadata(name: 'Dummy Wallet'), diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/dummy_user_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/dummy_user_service.dart deleted file mode 100644 index d34328dabe9..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/dummy_user_service.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; - -/// Creates dummy users and accounts. -abstract interface class DummyUserFactory { - static const dummyKeychainId = 'TestUserKeychainID'; - static const dummyUnlockFactor = PasswordLockFactor('Test1234'); - static final dummySeedPhrase = SeedPhrase.fromMnemonic( - 'few loyal swift champion rug peace dinosaur ' - 'erase bacon tone install universe', - ); - - factory DummyUserFactory() { - return const DummyUserFactoryImpl(); - } - - User buildDummyUser({String keychainId = dummyKeychainId}); - - Account buildDummyAccount({String keychainId = dummyKeychainId}); -} - -final class DummyUserFactoryImpl implements DummyUserFactory { - const DummyUserFactoryImpl(); - - @override - User buildDummyUser({String keychainId = DummyUserFactory.dummyKeychainId}) { - return User( - accounts: [ - buildDummyAccount(keychainId: keychainId), - ], - ); - } - - @override - Account buildDummyAccount({ - String keychainId = DummyUserFactory.dummyKeychainId, - }) { - return Account( - keychainId: keychainId, - roles: const { - AccountRole.voter, - AccountRole.proposer, - }, - walletInfo: WalletInfo( - metadata: const WalletMetadata( - name: 'Dummy Wallet', - icon: null, - ), - balance: Coin.fromAda(10), - /* cSpell:disable */ - address: ShelleyAddress.fromBech32( - 'addr_test1vzpwq95z3xyum8vqndgdd' - '9mdnmafh3djcxnc6jemlgdmswcve6tkw', - ), - /* cSpell:enable */ - ), - ); - } -} 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 4e9c2e1e5bd..5dd57b078e1 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 @@ -1,86 +1,59 @@ import 'dart:async'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; abstract interface class UserService implements ActiveAware { factory UserService({ - required KeychainProvider keychainProvider, - required UserStorage userStorage, - required DummyUserFactory dummyUserFactory, + required UserRepository userRepository, }) { return UserServiceImpl( - keychainProvider, - userStorage, - dummyUserFactory, + userRepository, ); } Account? get account; - Stream get watchAccount; - - Keychain? get keychain; + List get accounts; - Future> get keychains; + Stream get watchAccount; - Stream get watchKeychain; + Future getUser(); Future useLastAccount(); Future useAccount(Account account); - Future useKeychain(String id); - - Future removeCurrentKeychain(); - - Future removeKeychain(String id); + Future removeAccount(Account account); Future dispose(); } final class UserServiceImpl implements UserService { - final KeychainProvider _keychainProvider; - final UserStorage _userStorage; - final DummyUserFactory _dummyUserFactory; + final UserRepository _userRepository; final _logger = Logger('UserService'); - User? _user; - final _userSC = StreamController.broadcast(); - - Keychain? _keychain; - final _keychainSC = StreamController.broadcast(); - StreamSubscription? _keychainUnlockSub; + User _user = const User(accounts: []); + final _userSC = StreamController.broadcast(); bool _isActive = true; UserServiceImpl( - this._keychainProvider, - this._userStorage, - this._dummyUserFactory, + this._userRepository, ); @override - Account? get account => _user?.activeAccount; + Account? get account => _user.activeAccount; @override - Stream get watchAccount async* { - yield account; - yield* _userSC.stream.map((user) => user?.activeAccount).distinct(); - } - - @override - Keychain? get keychain => _keychain; + List get accounts => List.unmodifiable(_user.accounts); @override - Future> get keychains => _keychainProvider.getAll(); - - @override - Stream get watchKeychain async* { - yield _keychain; - yield* _keychainSC.stream; + Stream get watchAccount async* { + yield account; + yield* _userSC.stream.map((user) => user.activeAccount).distinct(); } @override @@ -90,153 +63,69 @@ final class UserServiceImpl implements UserService { set isActive(bool value) { if (_isActive != value) { _isActive = value; - _keychain?.isActive = value; + _user.activeAccount?.keychain.isActive = value; } } @override - Future useLastAccount() async { - final keychainId = await _userStorage.getLastKeychainId(); - if (keychainId == null) { - await _clearUser(); - await _useKeychain(null); - return; - } - - final keychain = await _findKeychain(keychainId); - if (keychain == null) { - _logger.severe('Active keychain[$keychainId] was not found!'); - } - - await _clearUser(); - await _useKeychain(keychain); - } + Future getUser() => _userRepository.getUser(); @override - Future useAccount(Account account) async { - await useKeychain(account.keychainId); - _updateUser(User(accounts: [account])); - } + Future useLastAccount() async { + final user = await _userRepository.getUser(); - @override - Future useKeychain(String id) async { - final keychain = await _findKeychain(id); - if (keychain == null) { - _logger.severe('Account keychain[$id] was not found!'); - } - await _useKeychain(keychain); + await _updateUser(user); } @override - Future removeCurrentKeychain() async { - final keychain = _keychain; - if (keychain == null) { - _logger.warning('Called remove keychain but no active found'); - return; - } - - await removeKeychain(keychain.id); - } + Future useAccount(Account account) async { + var user = await getUser(); - @override - Future removeKeychain(String id) async { - if (!await _keychainProvider.exists(id)) { - _logger.warning( - 'Called remove keychain[$id] but no such keychain was found', - ); - return; + if (!user.hasAccount(id: account.id)) { + user = user.addAccount(account); } - final isCurrentKeychain = id == _keychain?.id; + user = user.useAccount(id: account.id); - final keychain = await _keychainProvider.get(id); - await keychain.clear(); - - if (isCurrentKeychain) { - await _clearUser(); - await _useKeychain(null); - } + await _updateUser(user); } @override - Future dispose() async { - await _keychainUnlockSub?.cancel(); - _keychainUnlockSub = null; - - _keychain = null; - await _keychainSC.close(); - } - - Future _useKeychain(Keychain? keychain) async { - await _userStorage.setUsedKeychainId(keychain?.id); + Future removeAccount(Account account) async { + var user = await getUser(); - await _keychainUnlockSub?.cancel(); - _keychainUnlockSub = null; - - _updateActiveKeychain(keychain); - - if (keychain != null) { - _keychainUnlockSub = - keychain.watchIsUnlocked.listen(_onKeychainUnlockChanged); + if (user.hasAccount(id: account.id)) { + user = user.removeAccount(id: account.id); } - } - - Future _onKeychainUnlockChanged(bool isUnlocked) async { - final keychain = _keychain; - - _logger.finest('$keychain unlock changed[$isUnlocked]'); - - assert( - keychain != null, - 'Keychain unlock stage changed but keychain is null', - ); - if (!isUnlocked) { - await _clearUser(); - return; + if (user.activeAccount == null) { + final firstAccount = user.accounts.firstOrNull; + if (firstAccount != null) { + user = user.useAccount(id: firstAccount.id); + } } - await _fetchUserDetails(keychain!); - } - - // TODO(damian-molinski): fetch user details from backend with root key. - Future _fetchUserDetails(Keychain keychain) async { - await Future.delayed(const Duration(milliseconds: 100)); - - final user = _user?.account.keychainId == keychain.id - ? _user - : _dummyUserFactory.buildDummyUser(keychainId: keychain.id); - - _updateUser(user); - } - - Future _clearUser() async { - _updateUser(null); - } + await _updateUser(user); - Future _findKeychain(String id) async { - final exists = await _keychainProvider.exists(id); - - return exists ? await _keychainProvider.get(id) : null; + await account.keychain.erase(); } - void _updateActiveKeychain(Keychain? keychain) { - if (_keychain?.id != keychain?.id) { - _logger.finest('Keychain changed to $keychain'); + Future _updateUser(User user) async { + if (_user != user) { + _logger.info('Changing user to [$user]'); - _keychain?.isActive = false; - _keychain = keychain; - _keychain?.isActive = _isActive; + if (_user.activeAccount?.keychain.id != user.activeAccount?.keychain.id) { + _user.activeAccount?.keychain.isActive = false; + user.activeAccount?.keychain.isActive = _isActive; + } - _keychainSC.add(keychain); - } - } + await _userRepository.saveUser(user); - void _updateUser(User? user) { - if (_user != user) { - _logger.finest('User changed to $user'); _user = user; _userSC.add(user); } } + + @override + Future dispose() async {} } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart deleted file mode 100644 index b99209fd550..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; - -const _activeKeychainIdKey = 'activeKeychainId'; - -abstract interface class UserStorage { - Future getLastKeychainId(); - - Future setUsedKeychainId(String? id); -} - -final class SecureUserStorage extends SecureStorage implements UserStorage { - SecureUserStorage({ - super.secureStorage, - }); - - @override - Future getLastKeychainId() { - return readString(key: _activeKeychainIdKey); - } - - @override - Future setUsedKeychainId(String? id) { - return writeString(id, key: _activeKeychainIdKey); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml index d013fc8d12f..8744e2f0762 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: path: ../catalyst_voices_repositories catalyst_voices_shared: path: ../catalyst_voices_shared - convert: ^3.1.1 + collection: ^1.18.0 flutter: sdk: flutter flutter_driver: diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/dummy_user_factory_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/dummy_user_factory_test.dart deleted file mode 100644 index 1a669fc8eff..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/dummy_user_factory_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:catalyst_voices_services/src/user/dummy_user_service.dart'; -import 'package:test/test.dart'; - -void main() { - group(DummyUserFactory, () { - late DummyUserFactory factory; - - setUp(() { - factory = DummyUserFactory(); - }); - - test('dummy user returns account with dummy keychain', () { - expect( - factory.buildDummyUser().accounts.single.keychainId, - equals(DummyUserFactory.dummyKeychainId), - ); - }); - - test('dummy account returns account with dummy keychain', () { - expect( - factory.buildDummyAccount().keychainId, - equals(DummyUserFactory.dummyKeychainId), - ); - }); - }); -} 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 e8e3e3beeac..1f65ef61abd 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 @@ -1,5 +1,7 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_services/src/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; @@ -9,9 +11,8 @@ import 'package:test/scaffolding.dart'; import 'package:uuid/uuid.dart'; void main() { - late final KeychainProvider provider; - final UserStorage storage = SecureUserStorage(); - final dummyUserFactory = DummyUserFactory(); + late final KeychainProvider keychainProvider; + late final UserRepository userRepository; late UserService service; @@ -20,18 +21,17 @@ void main() { SharedPreferencesAsyncPlatform.instance = store; FlutterSecureStorage.setMockInitialValues({}); - provider = VaultKeychainProvider( + keychainProvider = VaultKeychainProvider( secureStorage: const FlutterSecureStorage(), sharedPreferences: SharedPreferencesAsync(), cacheConfig: const CacheConfig(), ); + userRepository = UserRepository(SecureUserStorage(), keychainProvider); }); setUp(() { service = UserService( - keychainProvider: provider, - userStorage: storage, - dummyUserFactory: dummyUserFactory, + userRepository: userRepository, ); }); @@ -40,83 +40,98 @@ void main() { await SharedPreferencesAsync().clear(); }); - group('Keychain', () { - test('when using keychain getter returns that keychain', () async { + group(UserService, () { + test('when using account getter returns that account', () async { // Given final keychainId = const Uuid().v4(); // When - final keychain = await provider.create(keychainId); + final keychain = await keychainProvider.create(keychainId); + final account = Account.dummy(keychain: keychain); - await service.useKeychain(keychain.id); + await service.useAccount(account); // Then - final currentKeychain = service.keychain; + final currentAccount = service.account; - expect(currentKeychain?.id, keychain.id); + expect(currentAccount?.id, account.id); + expect(currentAccount?.isActive, isTrue); }); - test('using different keychain emits update in stream', () async { + test('using different account emits update in stream', () async { // Given final keychainIdOne = const Uuid().v4(); final keychainIdTwo = const Uuid().v4(); // When - final keychainOne = await provider.create(keychainIdOne); - final keychainTwo = await provider.create(keychainIdTwo); + final keychainOne = await keychainProvider.create(keychainIdOne); + final keychainTwo = await keychainProvider.create(keychainIdTwo); + final accountOne = Account.dummy(keychain: keychainOne); + final accountTwo = Account.dummy(keychain: keychainTwo); - final keychainStream = service.watchKeychain; + final accountStream = service.watchAccount; // Then expect( - keychainStream, + accountStream, emitsInOrder([ isNull, - predicate((e) => e.id == keychainOne.id), - predicate((e) => e.id == keychainTwo.id), + predicate((e) => e?.id == accountOne.id), + predicate((e) => e?.id == accountTwo.id), + predicate((e) => e?.id == accountOne.id), isNull, ]), ); - await service.useKeychain(keychainOne.id); - await service.useKeychain(keychainTwo.id); - await service.removeCurrentKeychain(); + await service.useAccount(accountOne); + await service.useAccount(accountTwo); + + await service.removeAccount(accountTwo); + await service.removeAccount(accountOne); await service.dispose(); }); - test('keychains getter returns all initialized local instances', () async { + test('accounts getter returns all keychains initialized local instances', + () async { // Given final ids = List.generate(5, (_) => const Uuid().v4()); // When - final keychains = []; + final accounts = []; for (final id in ids) { - final keychain = await provider.create(id); - keychains.add(keychain); + final keychain = await keychainProvider.create(id); + final account = Account.dummy(keychain: keychain); + + accounts.add(account); } + await userRepository.saveUser(User(accounts: accounts)); + // Then - final serviceKeychains = await service.keychains; + final user = await service.getUser(); - expect(serviceKeychains.map((e) => e.id), keychains.map((e) => e.id)); + expect(user.accounts.map((e) => e.id), accounts.map((e) => e.id)); }); - }); - group('Account', () { - test('use last account restores previously stored keychain', () async { + test('use last account restores previously stored', () async { // Given final keychainId = const Uuid().v4(); // When - final expectedKeychain = await provider.create(keychainId); + final keychain = await keychainProvider.create(keychainId); + final lastAccount = Account.dummy( + keychain: keychain, + isActive: true, + ); - await storage.setUsedKeychainId(expectedKeychain.id); + final user = User(accounts: [lastAccount]); + await userRepository.saveUser(user); await service.useLastAccount(); // Then - expect(service.keychain?.id, expectedKeychain.id); + expect(service.account, lastAccount); }); test('use last account does nothing on clear instance', () async { @@ -126,7 +141,6 @@ void main() { await service.useLastAccount(); // Then - expect(service.keychain, isNull); expect(service.account, isNull); }); @@ -135,20 +149,25 @@ void main() { final keychainId = const Uuid().v4(); // When - final currentKeychain = await provider.create(keychainId); + final keychain = await keychainProvider.create(keychainId); + final account = Account.dummy( + keychain: keychain, + isActive: true, + ); - await storage.setUsedKeychainId(currentKeychain.id); + final user = User(accounts: [account]); + await userRepository.saveUser(user); await service.useLastAccount(); // Then - expect(service.keychain, isNotNull); + expect(service.account, isNotNull); - await service.removeCurrentKeychain(); + await service.removeAccount(account); - expect(service.keychain, isNull); - expect(await currentKeychain.isEmpty, isTrue); - expect(await provider.exists(keychainId), isFalse); + expect(service.account, isNull); + expect(await keychain.isEmpty, isTrue); + expect(await keychainProvider.exists(keychainId), isFalse); }); }); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index 932e4d9785e..a2b7d04c7c3 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -9,6 +9,11 @@ export 'crypto/local_crypto_service.dart'; export 'dependency/dependency_provider.dart'; export 'formatter/cryptocurrency_formatter.dart'; export 'formatter/wallet_address_formatter.dart'; +export 'keychain/keychain.dart'; +export 'keychain/keychain_provider.dart'; +export 'keychain/keychain_transformers.dart'; +export 'keychain/vault_keychain.dart'; +export 'keychain/vault_keychain_provider.dart'; export 'logging/logging_service.dart'; export 'platform/catalyst_platform.dart'; export 'platform_aware_builder/platform_aware_builder.dart'; @@ -25,6 +30,5 @@ export 'utils/active_aware.dart'; export 'utils/date_time_ext.dart'; export 'utils/future_ext.dart'; export 'utils/iterable_ext.dart'; -export 'utils/json_converters.dart'; export 'utils/lockable.dart'; export 'utils/typedefs.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart similarity index 93% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart index 8fe4aa8db13..4453d999ded 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart @@ -10,5 +10,5 @@ abstract interface class Keychain implements Lockable, ActiveAware { Future setMasterKey(Bip32Ed25519XPrivateKey key); - Future clear(); + Future erase(); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_provider.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_provider.dart similarity index 72% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_provider.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_provider.dart index ac523a80260..2289eee2a5d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_provider.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_provider.dart @@ -1,4 +1,4 @@ -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; abstract interface class KeychainProvider { Future create(String id); diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart new file mode 100644 index 00000000000..5bbafcd9df2 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +final class KeychainToUnlockTransformer + extends StreamTransformerBase { + KeychainToUnlockTransformer(); + + @override + Stream bind(Stream stream) { + return Stream.eventTransformed( + stream, + _KeychainUnlockStreamSink.new, + ); + } +} + +final class AccountToKeychainUnlockTransformer + extends StreamTransformerBase { + AccountToKeychainUnlockTransformer(); + + @override + Stream bind(Stream stream) { + return Stream.eventTransformed( + stream, + _AccountToKeychainUnlockStreamSink.new, + ); + } +} + +final class _KeychainUnlockStreamSink implements EventSink { + final EventSink _outputSink; + StreamSubscription? _streamSub; + + _KeychainUnlockStreamSink(this._outputSink); + + @override + void add(Keychain? event) { + final stream = event?.watchIsUnlocked ?? Stream.value(false); + + unawaited(_streamSub?.cancel()); + _streamSub = stream.listen(_outputSink.add); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _outputSink.addError(error, stackTrace); + } + + @override + void close() { + unawaited(_streamSub?.cancel()); + _streamSub = null; + _outputSink.close(); + } +} + +final class _AccountToKeychainUnlockStreamSink implements EventSink { + final EventSink _outputSink; + StreamSubscription? _streamSub; + + _AccountToKeychainUnlockStreamSink(this._outputSink); + + @override + void add(Account? event) { + final stream = event?.keychain.watchIsUnlocked ?? Stream.value(false); + + unawaited(_streamSub?.cancel()); + _streamSub = stream.listen(_outputSink.add); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _outputSink.addError(error, stackTrace); + } + + @override + void close() { + unawaited(_streamSub?.cancel()); + _streamSub = null; + _outputSink.close(); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart similarity index 95% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart index a7ec8b30668..8bab2a67207 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; const _rootKey = 'rootKey'; @@ -60,6 +59,9 @@ final class VaultKeychain extends SecureStorageVault implements Keychain { await writeString(data.toHex(), key: _rootKey); } + @override + Future erase() => clear(); + @override String toString() => 'VaultKeychain[$id]'; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain_provider.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain_provider.dart similarity index 94% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain_provider.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain_provider.dart index e6ac92a85b4..46e0b2249d5 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain_provider.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain_provider.dart @@ -1,7 +1,6 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; final _logger = Logger('VaultKeychainProvider'); @@ -60,7 +59,7 @@ final class VaultKeychainProvider implements KeychainProvider { if (!await keychain.isEmpty) { _logger.warning('Overriding existing keychain[$id]'); - await keychain.clear(); + await keychain.erase(); return _buildKeychain(id); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart index 997209af8a6..64f0622e903 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart @@ -107,6 +107,9 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { return _secureStorage.containsKey(key: effectiveKey); } + @override + bool get lastIsUnlocked => _isUnlocked; + @override Future get isUnlocked => _getIsUnlockedAndSync(); diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/json_converters.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/json_converters.dart deleted file mode 100644 index 67a1dfd4e31..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/json_converters.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -final class DurationConverter implements JsonConverter { - const DurationConverter(); - - @override - Duration fromJson(int json) => Duration(seconds: json); - - @override - int toJson(Duration object) => object.inSeconds; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart index 4f2b0fa4b08..28c12eb2e7f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart @@ -1,6 +1,10 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; abstract interface class Lockable { + /// Returns last known state of unlock. Effectively synchronous getter for + /// [watchIsUnlocked]. + bool get lastIsUnlocked; + /// Returns true when have sufficient [LockFactor] from [unlock]. Future get isUnlocked; diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml index 001e4c753d8..66de050feea 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: catalyst_voices_models: path: ../catalyst_voices_models collection: ^1.18.0 + convert: ^3.1.1 cryptography: ^2.7.0 flutter: sdk: flutter @@ -22,6 +23,7 @@ dependencies: json_annotation: ^4.9.0 logging: ^1.2.0 shared_preferences: ^2.3.3 + uuid: ^4.5.1 web: ^1.1.0 dev_dependencies: diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/keychain_transformers_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/keychain_transformers_test.dart similarity index 96% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/keychain_transformers_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/keychain_transformers_test.dart index a2da3d55a90..fea4ad84d06 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/keychain_transformers_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/keychain_transformers_test.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_provider_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_provider_test.dart similarity index 97% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_provider_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_provider_test.dart index aad93c8974e..9ad74709018 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_provider_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_provider_test.dart @@ -1,6 +1,6 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/keychain/vault_keychain_provider.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:convert/convert.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_test.dart similarity index 97% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_test.dart index ed73bf061bc..3a46aa5d362 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_test.dart @@ -1,6 +1,6 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:convert/convert.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mocktail/mocktail.dart';