From d3e97ac045616bdb3d69bb74793fc937ee3bba26 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:22:31 -0300 Subject: [PATCH 1/5] feat(user balance): - fetch the balance async unlocking drive explorer faster - update ardrive auth user api - implement/update unit tests --- lib/authentication/ardrive_auth.dart | 11 +++ lib/blocs/profile/profile_cubit.dart | 7 ++ lib/blocs/profile/profile_state.dart | 2 +- lib/components/profile_card.dart | 2 +- lib/main.dart | 2 + lib/user/repositories/user_repository.dart | 22 ++--- lib/user/user.dart | 3 +- test/authentication/ardrive_auth_test.dart | 78 +++++++++++++++- .../repositories/user_repository_test.dart | 91 +++++++++++++++++++ 9 files changed, 203 insertions(+), 15 deletions(-) diff --git a/lib/authentication/ardrive_auth.dart b/lib/authentication/ardrive_auth.dart index fd0f6c3024..2551b6b2e0 100644 --- a/lib/authentication/ardrive_auth.dart +++ b/lib/authentication/ardrive_auth.dart @@ -169,6 +169,8 @@ class ArDriveAuthImpl implements ArDriveAuth { currentUser = await _userRepository.getUser(password); + _updateBalance(); + if (await _biometricAuthentication.isEnabled()) { logger.i('Saving password in secure storage'); @@ -306,11 +308,20 @@ class ArDriveAuthImpl implements ArDriveAuth { currentUser = await _userRepository.getUser(password); + _updateBalance(); + _userStreamController.add(_currentUser); return currentUser; } + void _updateBalance() { + _userRepository.getIOTokens(currentUser.wallet).then( + (value) => _currentUser = _currentUser!.copyWith(ioTokens: value)); + _userRepository.getBalance(currentUser.wallet).then( + (value) => _currentUser = _currentUser!.copyWith(walletBalance: value)); + } + Future _saveUser( String password, ProfileType profileType, diff --git a/lib/blocs/profile/profile_cubit.dart b/lib/blocs/profile/profile_cubit.dart index 56536d7e60..6ebbf79e80 100644 --- a/lib/blocs/profile/profile_cubit.dart +++ b/lib/blocs/profile/profile_cubit.dart @@ -36,6 +36,13 @@ class ProfileCubit extends Cubit { _arDriveAuth = arDriveAuth, super(ProfileCheckingAvailability()) { promptToAuthenticate(); + + _arDriveAuth.onAuthStateChanged().listen((user) { + if (user != null) { + emit(ProfileLoggedIn( + user: user, useTurbo: _turboUploadService.useTurboUpload)); + } + }); } Future isCurrentProfileArConnect() async { diff --git a/lib/blocs/profile/profile_state.dart b/lib/blocs/profile/profile_state.dart index 0195106608..2c55c2bbfd 100644 --- a/lib/blocs/profile/profile_state.dart +++ b/lib/blocs/profile/profile_state.dart @@ -49,7 +49,7 @@ class ProfileLoggedIn extends ProfileAvailable { useTurbo; @override - List get props => [user]; + List get props => [user, useTurbo]; } class ProfilePromptAdd extends ProfileUnavailable {} diff --git a/lib/components/profile_card.dart b/lib/components/profile_card.dart index d084cb8d8d..4da434ba48 100644 --- a/lib/components/profile_card.dart +++ b/lib/components/profile_card.dart @@ -500,7 +500,7 @@ class _ProfileCardState extends State { ), if (ioTokens == null) Text( - 'An error occurred while fetching IO tokens', + 'Loading...', style: typography.paragraphNormal( color: colorTokens.textLow, fontWeight: ArFontWeight.semiBold, diff --git a/lib/main.dart b/lib/main.dart index 7071764017..0532bd1c55 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -44,6 +44,7 @@ import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:ario_sdk/ario_sdk.dart'; import 'package:arweave/arweave.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -391,6 +392,7 @@ class AppState extends State { create: (context) => UserRepository( context.read(), context.read(), + ArioSDKFactory().create(), ), ), RepositoryProvider( diff --git a/lib/user/repositories/user_repository.dart b/lib/user/repositories/user_repository.dart index 9384757c5d..8c5fe7e848 100644 --- a/lib/user/repositories/user_repository.dart +++ b/lib/user/repositories/user_repository.dart @@ -17,23 +17,29 @@ abstract class UserRepository { Future deleteUser(); Future getOwnerOfDefaultProfile(); Future getBalance(Wallet wallet); + Future getIOTokens(Wallet wallet); - factory UserRepository(ProfileDao profileDao, ArweaveService arweave) => + factory UserRepository( + ProfileDao profileDao, ArweaveService arweave, ArioSDK arioSDK) => _UserRepository( profileDao: profileDao, arweave: arweave, + arioSDK: arioSDK, ); } class _UserRepository implements UserRepository { final ProfileDao _profileDao; final ArweaveService _arweave; + final ArioSDK _arioSDK; _UserRepository({ required ProfileDao profileDao, required ArweaveService arweave, + required ArioSDK arioSDK, }) : _profileDao = profileDao, - _arweave = arweave; + _arweave = arweave, + _arioSDK = arioSDK; // TODO: Check ProfileDAO to implement only one source for user data @@ -48,8 +54,6 @@ class _UserRepository implements UserRepository { final profileDetails = await _profileDao.loadDefaultProfile(password); - final ioTokens = await _getIOTokens(profileDetails: profileDetails); - final user = User( profileType: ProfileType.values[profileDetails.details.profileType], wallet: profileDetails.wallet, @@ -59,7 +63,6 @@ class _UserRepository implements UserRepository { walletBalance: await _arweave.getWalletBalance( await profileDetails.wallet.getAddress(), ), - ioTokens: ioTokens, ); logger.d('Loaded user'); @@ -107,16 +110,13 @@ class _UserRepository implements UserRepository { return profile.walletPublicKey; } - Future _getIOTokens({ - required ProfileLoadDetails profileDetails, - }) async { + @override + Future getIOTokens(Wallet wallet) async { try { String? ioTokens; if (isArioSDKSupportedOnPlatform()) { - ioTokens = await ArioSDKFactory() - .create() - .getIOTokens(await profileDetails.wallet.getAddress()); + ioTokens = await _arioSDK.getIOTokens(await wallet.getAddress()); } return ioTokens; diff --git a/lib/user/user.dart b/lib/user/user.dart index 713abdd93e..413d410be3 100644 --- a/lib/user/user.dart +++ b/lib/user/user.dart @@ -71,13 +71,14 @@ class _User implements User { }); @override - List get props => [ + List get props => [ password, walletAddress, walletBalance, cipherKey, profileType, wallet, + ioTokens, ]; @override diff --git a/test/authentication/ardrive_auth_test.dart b/test/authentication/ardrive_auth_test.dart index 00716e5ecf..90591d0699 100644 --- a/test/authentication/ardrive_auth_test.dart +++ b/test/authentication/ardrive_auth_test.dart @@ -140,7 +140,10 @@ void main() { when(() => mockArweaveService.getFirstPrivateDriveTxId(wallet, maxRetries: any(named: 'maxRetries'))) .thenAnswer((_) async => 'some_id'); - + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); @@ -222,6 +225,10 @@ void main() { when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); when( () => mockArweaveService.getLatestDriveEntityWithId( @@ -277,6 +284,11 @@ void main() { when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); + when(() => mockUserRepository.deleteUser()) .thenAnswer((invocation) async {}); @@ -321,6 +333,11 @@ void main() { when(() => mockUserRepository.deleteUser()) .thenAnswer((invocation) async {}); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); + when(() => mockUserRepository.saveUser( 'password', ProfileType.json, wallet)) .thenAnswer((invocation) => Future.value(null)); @@ -390,6 +407,11 @@ void main() { when(() => mockUserRepository.hasUser()) .thenAnswer((_) async => true); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); + when(() => mockUserRepository.getUser('password123')) .thenAnswer((invocation) => Future.value(loggedUser)); @@ -491,6 +513,10 @@ void main() { .thenAnswer((_) async => false); when(() => mockUserRepository.getUser('password')) .thenAnswer((invocation) async => unlockedUser); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); @@ -532,6 +558,11 @@ void main() { .thenAnswer((_) async => false); when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); + when(() => mockUserRepository.getUser('password')) .thenAnswer((invocation) async => unlockedUser); when(() => mockBiometricAuthentication.isEnabled()) @@ -597,6 +628,11 @@ void main() { ).thenAnswer((_) async => 'some_id'); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); + when( () => mockArDriveCrypto.deriveDriveKey( wallet, @@ -674,6 +710,11 @@ void main() { ).thenAnswer((invocation) => Future.value(SecretKey([]))); when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => BigInt.one); + when( () => mockArweaveService.getLatestDriveEntityWithId( any(), @@ -774,5 +815,40 @@ void main() { expect(walletAddress, null); }); + + group('refreshBalance method', () { + test('should update current user balance and notify listeners', () async { + // Arrange + final initialBalance = BigInt.one; + final updatedBalance = BigInt.two; + final initialUser = User( + password: 'password', + wallet: wallet, + walletAddress: 'walletAddress', + walletBalance: initialBalance, + cipherKey: SecretKey([]), + profileType: ProfileType.json, + ); + // Updated user with new balance + final updatedUser = initialUser.copyWith(walletBalance: updatedBalance); + + arDriveAuth.currentUser = initialUser; + + when(() => mockUserRepository.getBalance(wallet)) + .thenAnswer((_) async => updatedBalance); + + // Act + await arDriveAuth.refreshBalance(); + + // Assert + expect(arDriveAuth.currentUser.walletBalance, equals(updatedBalance)); + verify(() => mockUserRepository.getBalance(wallet)).called(1); + + // Verify that listeners are notified + arDriveAuth.onAuthStateChanged().listen((user) { + expect(user, equals(updatedUser)); + }); + }); + }); }); } diff --git a/test/user/repositories/user_repository_test.dart b/test/user/repositories/user_repository_test.dart index 2d8512e943..aa86bca5c3 100644 --- a/test/user/repositories/user_repository_test.dart +++ b/test/user/repositories/user_repository_test.dart @@ -3,6 +3,8 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/arweave/arweave.dart'; import 'package:ardrive/user/repositories/user_repository.dart'; import 'package:ardrive/user/user.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:ario_sdk/ario_sdk.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,10 +14,13 @@ import '../../test_utils/utils.dart'; class MockTransaction extends Mock implements TransactionCommonMixin {} +class MockArioSDK extends Mock implements ArioSDK {} + void main() { late UserRepository userRepository; late ArweaveService mockArweaveService; late ProfileDao mockProfileDao; + late MockArioSDK mockArioSDK; const rightPassword = 'right-password'; @@ -27,10 +32,12 @@ void main() { setUp(() { mockArweaveService = MockArweaveService(); mockProfileDao = MockProfileDao(); + mockArioSDK = MockArioSDK(); userRepository = UserRepository( mockProfileDao, mockArweaveService, + mockArioSDK, ); // register fallback values @@ -233,5 +240,89 @@ void main() { verify(() => mockProfileDao.getDefaultProfile()).called(1); }); }); + + group('getIOTokens method', () { + test('should return IO tokens when ArioSDK is supported', () async { + final wallet = getTestWallet(); + const expectedIOTokens = '100'; + final walletAddress = await wallet.getAddress(); + + AppPlatform.setMockPlatform(platform: SystemPlatform.Web); + + when(() => mockArioSDK.getIOTokens(walletAddress)) + .thenAnswer((_) async => expectedIOTokens); + + final result = await userRepository.getIOTokens(wallet); + + expect(result, expectedIOTokens); + verify(() => mockArioSDK.getIOTokens(walletAddress)).called(1); + }); + + test('should return null when ArioSDK is not supported', () async { + final wallet = getTestWallet(); + + AppPlatform.setMockPlatform(platform: SystemPlatform.Android); + + final result = await userRepository.getIOTokens(wallet); + + expect(result, isNull); + + verifyNever(() => mockArioSDK.getIOTokens(any())); + }); + + test('should return null when ArioSDK is not supported', () async { + final wallet = getTestWallet(); + + AppPlatform.setMockPlatform(platform: SystemPlatform.iOS); + + final result = await userRepository.getIOTokens(wallet); + + expect(result, isNull); + + verifyNever(() => mockArioSDK.getIOTokens(any())); + }); + }); + + group('getBalance method', () { + test('should return the correct balance', () async { + final wallet = getTestWallet(); + final expectedBalance = BigInt.from(100); + final walletAddress = await wallet.getAddress(); + + when(() => mockArweaveService.getWalletBalance(walletAddress)) + .thenAnswer((_) async => expectedBalance); + when(() => mockArweaveService.getPendingTxFees(walletAddress)) + .thenAnswer((_) async => BigInt.from(0)); + + final result = await userRepository.getBalance(wallet); + + expect(result, expectedBalance); + verify(() => mockArweaveService.getWalletBalance(walletAddress)) + .called(1); + verify(() => mockArweaveService.getPendingTxFees(walletAddress)) + .called(1); + }); + + test('should return the correct balance when has pending transactions', + () async { + final wallet = getTestWallet(); + final expectedBalance = BigInt.from(100); + final expectedPendingTxFees = BigInt.from(10); + final walletAddress = await wallet.getAddress(); + + when(() => mockArweaveService.getWalletBalance(walletAddress)) + .thenAnswer((_) async => expectedBalance); + when(() => mockArweaveService.getPendingTxFees(walletAddress)) + .thenAnswer((_) async => expectedPendingTxFees); + + final result = await userRepository.getBalance(wallet); + + expect(result, expectedBalance - expectedPendingTxFees); + verify(() => mockArweaveService.getWalletBalance(walletAddress)) + .called(1); + verify(() => mockArweaveService.getPendingTxFees(walletAddress)) + .called(1); + }); + }); }); } From 18e7e154b55dd60bebf0ba6d32c84c588155cb65 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:40:11 -0300 Subject: [PATCH 2/5] feat(io tokens) - load tokens async - show error state when the io tokens fails to fetch - add refresh button --- lib/authentication/ardrive_auth.dart | 27 ++++-- lib/components/profile_card.dart | 89 ++++++++++++------ lib/user/balance/user_balance_bloc.dart | 93 +++++++++++++++++++ lib/user/balance/user_balance_event.dart | 12 +++ lib/user/balance/user_balance_state.dart | 30 ++++++ lib/user/repositories/user_repository.dart | 17 ++-- lib/user/user.dart | 10 ++ test/authentication/ardrive_auth_test.dart | 10 +- .../login/blocs/login_bloc_test.dart | 6 ++ test/blocs/create_snapshot_cubit_test.dart | 2 + test/blocs/drive_create_cubit_test.dart | 1 + test/blocs/fs_entry_license_bloc_test.dart | 1 + test/blocs/fs_entry_move_bloc_test.dart | 1 + test/blocs/prompt_to_snapshot_bloc_test.dart | 2 + test/blocs/upload_cubit_test.dart | 4 + test/core/upload/uploader_test.dart | 4 +- test/test_utils/fake_user.dart | 3 + .../repositories/user_repository_test.dart | 2 + test/utils/user_utils.dart | 2 + 19 files changed, 267 insertions(+), 49 deletions(-) create mode 100644 lib/user/balance/user_balance_bloc.dart create mode 100644 lib/user/balance/user_balance_event.dart create mode 100644 lib/user/balance/user_balance_state.dart diff --git a/lib/authentication/ardrive_auth.dart b/lib/authentication/ardrive_auth.dart index a942a39d16..19ab84b0cf 100644 --- a/lib/authentication/ardrive_auth.dart +++ b/lib/authentication/ardrive_auth.dart @@ -318,10 +318,23 @@ class ArDriveAuthImpl implements ArDriveAuth { } void _updateBalance() { - _userRepository.getIOTokens(currentUser.wallet).then( - (value) => _currentUser = _currentUser!.copyWith(ioTokens: value)); - _userRepository.getBalance(currentUser.wallet).then( - (value) => _currentUser = _currentUser!.copyWith(walletBalance: value)); + _userRepository.getIOTokens(currentUser.wallet).then((value) { + _currentUser = _currentUser!.copyWith( + ioTokens: value, + errorFetchingIOTokens: false, + ); + _userStreamController.add(_currentUser); + }).catchError((e) { + _currentUser = _currentUser!.copyWith( + errorFetchingIOTokens: true, + ); + _userStreamController.add(_currentUser); + return Future.value(null); + }); + _userRepository.getBalance(currentUser.wallet).then((value) { + _currentUser = _currentUser!.copyWith(walletBalance: value); + _userStreamController.add(_currentUser); + }); } Future _saveUser( @@ -375,11 +388,7 @@ class ArDriveAuthImpl implements ArDriveAuth { @override Future refreshBalance() async { - final balance = await _userRepository.getBalance(currentUser.wallet); - - currentUser = currentUser.copyWith(walletBalance: balance); - - _userStreamController.add(_currentUser); + _updateBalance(); } } diff --git a/lib/components/profile_card.dart b/lib/components/profile_card.dart index 4da434ba48..927e527ac2 100644 --- a/lib/components/profile_card.dart +++ b/lib/components/profile_card.dart @@ -15,6 +15,7 @@ import 'package:ardrive/services/config/config.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/topup/components/turbo_balance_widget.dart'; import 'package:ardrive/turbo/utils/utils.dart'; +import 'package:ardrive/user/balance/user_balance_bloc.dart'; import 'package:ardrive/user/download_wallet/download_wallet_modal.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/open_url.dart'; @@ -477,36 +478,66 @@ class _ProfileCardState extends State { final ioTokens = state.user.ioTokens; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'tIO Tokens', - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - color: colorTokens.textHigh, - ), - ), - if (ioTokens != null) - Text( - ioTokens, - style: typography.paragraphNormal( - color: colorTokens.textLow, - fontWeight: ArFontWeight.semiBold, - ), - ), - if (ioTokens == null) - Text( - 'Loading...', - style: typography.paragraphNormal( - color: colorTokens.textLow, - fontWeight: ArFontWeight.semiBold, + return BlocProvider( + create: (context) => UserBalanceBloc(auth: context.read()) + ..add(GetUserBalance()), + child: BlocBuilder( + builder: (context, state) { + if (state is UserBalanceLoaded) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'tIO Tokens', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textHigh, + ), + ), + if (state is UserBalanceLoadingIOTokens && + !state.errorFetchingIOTokens) + const Padding( + padding: EdgeInsets.only(top: 8), + child: LinearProgressIndicator(), + ), + if (ioTokens != null) + Text( + ioTokens, + style: typography.paragraphNormal( + color: colorTokens.textLow, + fontWeight: ArFontWeight.semiBold, + ), + ), + if (state.errorFetchingIOTokens) ...[ + Row( + children: [ + Text( + 'Error fetching IO Tokens', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textRed, + ), + ), + ArDriveIconButton( + icon: ArDriveIcons.refresh(), + onPressed: () { + context + .read() + .add(RefreshUserBalance()); + }, + ) + ], + ), + ] + ], ), - ), - ], + ); + } + return const SizedBox.shrink(); + }, ), ); } diff --git a/lib/user/balance/user_balance_bloc.dart b/lib/user/balance/user_balance_bloc.dart new file mode 100644 index 0000000000..54a990dad5 --- /dev/null +++ b/lib/user/balance/user_balance_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/user/user.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'user_balance_event.dart'; +part 'user_balance_state.dart'; + +class UserBalanceBloc extends Bloc { + final ArDriveAuth _auth; + StreamSubscription? _userSubscription; + + UserBalanceBloc({required ArDriveAuth auth}) + : _auth = auth, + super(UserBalanceInitial()) { + on(_onGetUserBalance); + on(_onRefreshUserBalance); + } + + Future _onGetUserBalance( + GetUserBalance event, Emitter emit) async { + await _cancelSubscription(); + + final user = _auth.currentUser; + _emitUserBalanceState(user, emit); + + _userSubscription = _auth.onAuthStateChanged().listen((user) { + if (isClosed) return; + _emitUserBalanceState(user, emit); + if (user?.ioTokens != null) { + _cancelSubscription(); + } + }); + + await _userSubscription?.asFuture(); + } + + Future _onRefreshUserBalance( + RefreshUserBalance event, Emitter emit) async { + if (isClosed) return; + + await _cancelSubscription(); + + emit(UserBalanceLoadingIOTokens( + arBalance: _auth.currentUser.walletBalance, + errorFetchingIOTokens: false, + )); + + _auth.refreshBalance(); + + _userSubscription = _auth.onAuthStateChanged().listen((user) { + if (isClosed) return; + _emitUserBalanceState(user, emit); + if (user?.ioTokens != null) { + _cancelSubscription(); + } + }); + + await _userSubscription?.asFuture(); + } + + Future _cancelSubscription() async { + if (_userSubscription != null) { + await _userSubscription!.cancel(); + _userSubscription = null; + } + } + + void _emitUserBalanceState(User? user, Emitter emit) { + if (user == null) return; + + if (user.ioTokens == null && !user.errorFetchingIOTokens) { + emit(UserBalanceLoadingIOTokens( + arBalance: user.walletBalance, + errorFetchingIOTokens: user.errorFetchingIOTokens, + )); + } else { + emit(UserBalanceLoaded( + arBalance: user.walletBalance, + ioTokens: user.ioTokens, + errorFetchingIOTokens: user.errorFetchingIOTokens, + )); + } + } + + @override + Future close() async { + await _cancelSubscription(); + return super.close(); + } +} diff --git a/lib/user/balance/user_balance_event.dart b/lib/user/balance/user_balance_event.dart new file mode 100644 index 0000000000..9d2e0b4f45 --- /dev/null +++ b/lib/user/balance/user_balance_event.dart @@ -0,0 +1,12 @@ +part of 'user_balance_bloc.dart'; + +sealed class UserBalanceEvent extends Equatable { + const UserBalanceEvent(); + + @override + List get props => []; +} + +final class GetUserBalance extends UserBalanceEvent {} + +final class RefreshUserBalance extends UserBalanceEvent {} diff --git a/lib/user/balance/user_balance_state.dart b/lib/user/balance/user_balance_state.dart new file mode 100644 index 0000000000..185e120bde --- /dev/null +++ b/lib/user/balance/user_balance_state.dart @@ -0,0 +1,30 @@ +part of 'user_balance_bloc.dart'; + +sealed class UserBalanceState extends Equatable { + const UserBalanceState(); + + @override + List get props => []; +} + +final class UserBalanceInitial extends UserBalanceState {} + +final class UserBalanceLoaded extends UserBalanceState { + final BigInt arBalance; + final String? ioTokens; + final bool errorFetchingIOTokens; + + const UserBalanceLoaded({ + required this.arBalance, + required this.ioTokens, + required this.errorFetchingIOTokens, + }); +} + +final class UserBalanceLoadingIOTokens extends UserBalanceLoaded { + const UserBalanceLoadingIOTokens({ + required super.arBalance, + super.ioTokens, + super.errorFetchingIOTokens = false, + }); +} diff --git a/lib/user/repositories/user_repository.dart b/lib/user/repositories/user_repository.dart index 8c5fe7e848..725f089cc4 100644 --- a/lib/user/repositories/user_repository.dart +++ b/lib/user/repositories/user_repository.dart @@ -63,6 +63,7 @@ class _UserRepository implements UserRepository { walletBalance: await _arweave.getWalletBalance( await profileDetails.wallet.getAddress(), ), + errorFetchingIOTokens: false, ); logger.d('Loaded user'); @@ -112,18 +113,16 @@ class _UserRepository implements UserRepository { @override Future getIOTokens(Wallet wallet) async { - try { - String? ioTokens; + await Future.delayed(Duration(seconds: 2)) + .then((value) => throw Exception('Error fetching IOTokens')); - if (isArioSDKSupportedOnPlatform()) { - ioTokens = await _arioSDK.getIOTokens(await wallet.getAddress()); - } + String? ioTokens; - return ioTokens; - } catch (e, stacktrace) { - logger.e('Failed to get IO tokens', e, stacktrace); - return null; + if (isArioSDKSupportedOnPlatform()) { + ioTokens = await _arioSDK.getIOTokens(await wallet.getAddress()); } + + return ioTokens; } @override diff --git a/lib/user/user.dart b/lib/user/user.dart index 413d410be3..3366c7d8be 100644 --- a/lib/user/user.dart +++ b/lib/user/user.dart @@ -13,6 +13,7 @@ abstract class User with EquatableMixin { late final SecretKey cipherKey; late final ProfileType profileType; abstract final String? ioTokens; + abstract final bool errorFetchingIOTokens; factory User({ required String password, @@ -22,6 +23,7 @@ abstract class User with EquatableMixin { required SecretKey cipherKey, required ProfileType profileType, String? ioTokens, + required bool errorFetchingIOTokens, }) => _User( password: password, @@ -31,6 +33,7 @@ abstract class User with EquatableMixin { cipherKey: cipherKey, profileType: profileType, ioTokens: ioTokens, + errorFetchingIOTokens: errorFetchingIOTokens, ); User copyWith({ @@ -41,6 +44,7 @@ abstract class User with EquatableMixin { SecretKey? cipherKey, ProfileType? profileType, String? ioTokens, + bool? errorFetchingIOTokens, }); } @@ -59,6 +63,8 @@ class _User implements User { late final ProfileType profileType; @override final String? ioTokens; + @override + final bool errorFetchingIOTokens; _User({ required this.password, @@ -68,6 +74,7 @@ class _User implements User { required this.cipherKey, required this.profileType, this.ioTokens, + required this.errorFetchingIOTokens, }); @override @@ -96,6 +103,7 @@ class _User implements User { SecretKey? cipherKey, ProfileType? profileType, String? ioTokens, + bool? errorFetchingIOTokens, }) { return _User( password: password ?? this.password, @@ -105,6 +113,8 @@ class _User implements User { cipherKey: cipherKey ?? this.cipherKey, profileType: profileType ?? this.profileType, ioTokens: ioTokens ?? this.ioTokens, + errorFetchingIOTokens: + errorFetchingIOTokens ?? this.errorFetchingIOTokens, ); } } diff --git a/test/authentication/ardrive_auth_test.dart b/test/authentication/ardrive_auth_test.dart index 90591d0699..132bdbfa2c 100644 --- a/test/authentication/ardrive_auth_test.dart +++ b/test/authentication/ardrive_auth_test.dart @@ -133,6 +133,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); test( 'should return the user when has private drives and login with sucess. ', @@ -389,6 +390,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); test( @@ -506,6 +508,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); test('should return the user when password is correct', () async { @@ -549,6 +552,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); test( @@ -619,6 +623,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); when( () => mockArweaveService.getFirstPrivateDriveTxId( @@ -695,6 +700,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); @@ -748,7 +754,8 @@ void main() { walletAddress: 'walletAddress', walletBalance: BigInt.one, cipherKey: SecretKey([]), - profileType: ProfileType.json, + profileType: ProfileType.json, + errorFetchingIOTokens: false, ); arDriveAuth.onAuthStateChanged().listen((user) { @@ -828,6 +835,7 @@ void main() { walletBalance: initialBalance, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); // Updated user with new balance final updatedUser = initialUser.copyWith(walletBalance: updatedBalance); diff --git a/test/authentication/login/blocs/login_bloc_test.dart b/test/authentication/login/blocs/login_bloc_test.dart index 0fae361e90..9c256a8a24 100644 --- a/test/authentication/login/blocs/login_bloc_test.dart +++ b/test/authentication/login/blocs/login_bloc_test.dart @@ -186,6 +186,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); blocTest( 'should emit the event to show onboarding when user is not an existing one', @@ -355,6 +356,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, )); }, act: (bloc) async { @@ -422,6 +424,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); blocTest( @@ -486,6 +489,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); blocTest( @@ -809,6 +813,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), ); }, @@ -833,6 +838,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); blocTest( diff --git a/test/blocs/create_snapshot_cubit_test.dart b/test/blocs/create_snapshot_cubit_test.dart index 90ed376fca..c3c5b60254 100644 --- a/test/blocs/create_snapshot_cubit_test.dart +++ b/test/blocs/create_snapshot_cubit_test.dart @@ -160,6 +160,7 @@ void main() { ), profileType: ProfileType.json, ioTokens: 'ioTokens', + errorFetchingIOTokens: false, ), useTurbo: false, ), @@ -214,6 +215,7 @@ void main() { walletBalance: BigInt.one, cipherKey: cipher, profileType: ProfileType.json, + errorFetchingIOTokens: false, )); when(() => appConfig.allowedDataItemSizeForTurbo) diff --git a/test/blocs/drive_create_cubit_test.dart b/test/blocs/drive_create_cubit_test.dart index f6a2dca3a8..cafdf2a799 100644 --- a/test/blocs/drive_create_cubit_test.dart +++ b/test/blocs/drive_create_cubit_test.dart @@ -72,6 +72,7 @@ void main() { cipherKey: SecretKey(keyBytes), profileType: ProfileType.json, ioTokens: 'ioTokens', + errorFetchingIOTokens: false, ), useTurbo: turboUploadService.useTurboUpload, ), diff --git a/test/blocs/fs_entry_license_bloc_test.dart b/test/blocs/fs_entry_license_bloc_test.dart index efa6a209da..0340f6a2e1 100644 --- a/test/blocs/fs_entry_license_bloc_test.dart +++ b/test/blocs/fs_entry_license_bloc_test.dart @@ -271,6 +271,7 @@ void main() { walletBalance: BigInt.one, profileType: ProfileType.json, ioTokens: 'ioTokens', + errorFetchingIOTokens: false, ), useTurbo: false, ), diff --git a/test/blocs/fs_entry_move_bloc_test.dart b/test/blocs/fs_entry_move_bloc_test.dart index 7ccfa7b7f9..75d2658a12 100644 --- a/test/blocs/fs_entry_move_bloc_test.dart +++ b/test/blocs/fs_entry_move_bloc_test.dart @@ -273,6 +273,7 @@ void main() { walletBalance: BigInt.one, profileType: ProfileType.json, ioTokens: 'ioTokens', + errorFetchingIOTokens: false, ), useTurbo: false, ), diff --git a/test/blocs/prompt_to_snapshot_bloc_test.dart b/test/blocs/prompt_to_snapshot_bloc_test.dart index b2701bd4e1..8edba0e6b7 100644 --- a/test/blocs/prompt_to_snapshot_bloc_test.dart +++ b/test/blocs/prompt_to_snapshot_bloc_test.dart @@ -59,6 +59,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey(List.generate(32, (index) => index)), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), useTurbo: false, )); @@ -240,6 +241,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey(List.generate(32, (index) => index)), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), useTurbo: false, )); diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index e2a9fb6558..513ed451a2 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -308,6 +308,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey(tKeyBytes), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), useTurbo: false, ), @@ -335,6 +336,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), ); @@ -418,6 +420,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey(tKeyBytes), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), ), ); @@ -523,6 +526,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey(tKeyBytes), profileType: ProfileType.json, + errorFetchingIOTokens: false, ), useTurbo: false, ), diff --git a/test/core/upload/uploader_test.dart b/test/core/upload/uploader_test.dart index f25a266686..e06dfa5b94 100644 --- a/test/core/upload/uploader_test.dart +++ b/test/core/upload/uploader_test.dart @@ -1016,7 +1016,9 @@ User getFakeUser() => User( walletAddress: 'walletAddress', walletBalance: BigInt.one, cipherKey: SecretKey([]), - profileType: ProfileType.arConnect); + profileType: ProfileType.arConnect, + errorFetchingIOTokens: false, + ); FolderEntry getFakeFolder() => FolderEntry( id: 'id', diff --git a/test/test_utils/fake_user.dart b/test/test_utils/fake_user.dart index a6f5c837fe..73fb04b395 100644 --- a/test/test_utils/fake_user.dart +++ b/test/test_utils/fake_user.dart @@ -11,6 +11,7 @@ final fakeUserJson = User( walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); final fakeUserArConnect = User( @@ -20,4 +21,6 @@ final fakeUserArConnect = User( walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.arConnect, + errorFetchingIOTokens: false, + ); diff --git a/test/user/repositories/user_repository_test.dart b/test/user/repositories/user_repository_test.dart index aa86bca5c3..0f76329911 100644 --- a/test/user/repositories/user_repository_test.dart +++ b/test/user/repositories/user_repository_test.dart @@ -87,6 +87,7 @@ void main() { walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); expect(result, isNotNull); @@ -153,6 +154,7 @@ void main() { walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); when(() => mockProfileDao.addProfile( diff --git a/test/utils/user_utils.dart b/test/utils/user_utils.dart index cd0fd68a99..05a08e8c87 100644 --- a/test/utils/user_utils.dart +++ b/test/utils/user_utils.dart @@ -23,6 +23,7 @@ void main() { walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); // arrange when(() => auth.currentUser).thenReturn(user); @@ -46,6 +47,7 @@ void main() { walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); // arrange when(() => auth.currentUser).thenReturn(user); From e1117b72fe8fa932aa100165e8b1f02d91c976ff Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:40:25 -0300 Subject: [PATCH 3/5] Update user_repository.dart --- lib/user/repositories/user_repository.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/user/repositories/user_repository.dart b/lib/user/repositories/user_repository.dart index 725f089cc4..9d25b83540 100644 --- a/lib/user/repositories/user_repository.dart +++ b/lib/user/repositories/user_repository.dart @@ -113,9 +113,6 @@ class _UserRepository implements UserRepository { @override Future getIOTokens(Wallet wallet) async { - await Future.delayed(Duration(seconds: 2)) - .then((value) => throw Exception('Error fetching IOTokens')); - String? ioTokens; if (isArioSDKSupportedOnPlatform()) { From ef606b36168386b9a5429f639ee6b230f73d9e7d Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:42:44 -0300 Subject: [PATCH 4/5] Update ardrive_auth_test.dart --- test/authentication/ardrive_auth_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/authentication/ardrive_auth_test.dart b/test/authentication/ardrive_auth_test.dart index 132bdbfa2c..2650bc383e 100644 --- a/test/authentication/ardrive_auth_test.dart +++ b/test/authentication/ardrive_auth_test.dart @@ -754,7 +754,7 @@ void main() { walletAddress: 'walletAddress', walletBalance: BigInt.one, cipherKey: SecretKey([]), - profileType: ProfileType.json, + profileType: ProfileType.json, errorFetchingIOTokens: false, ); @@ -844,12 +844,15 @@ void main() { when(() => mockUserRepository.getBalance(wallet)) .thenAnswer((_) async => updatedBalance); + when(() => mockUserRepository.getIOTokens(wallet)) + .thenAnswer((_) async => '0.4'); // Act await arDriveAuth.refreshBalance(); // Assert expect(arDriveAuth.currentUser.walletBalance, equals(updatedBalance)); + expect(arDriveAuth.currentUser.errorFetchingIOTokens, false); verify(() => mockUserRepository.getBalance(wallet)).called(1); // Verify that listeners are notified From 629ca5bbe9fa1ebe0f1c3dbbf00c215ea4b4c1c5 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:13:04 -0300 Subject: [PATCH 5/5] fix - if returns null throw exception - set as not failed when refreshing the balance --- lib/authentication/ardrive_auth.dart | 6 ++++++ lib/components/profile_card.dart | 8 ++++++-- lib/user/repositories/user_repository.dart | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/authentication/ardrive_auth.dart b/lib/authentication/ardrive_auth.dart index 19ab84b0cf..4266983045 100644 --- a/lib/authentication/ardrive_auth.dart +++ b/lib/authentication/ardrive_auth.dart @@ -388,6 +388,12 @@ class ArDriveAuthImpl implements ArDriveAuth { @override Future refreshBalance() async { + _currentUser = _currentUser!.copyWith( + errorFetchingIOTokens: false, + ); + + _userStreamController.add(_currentUser); + _updateBalance(); } } diff --git a/lib/components/profile_card.dart b/lib/components/profile_card.dart index 927e527ac2..3d03d69210 100644 --- a/lib/components/profile_card.dart +++ b/lib/components/profile_card.dart @@ -513,12 +513,16 @@ class _ProfileCardState extends State { ), if (state.errorFetchingIOTokens) ...[ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Error fetching IO Tokens', + 'Error fetching tIO balance', style: typography.paragraphNormal( fontWeight: ArFontWeight.semiBold, - color: colorTokens.textRed, + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, ), ), ArDriveIconButton( diff --git a/lib/user/repositories/user_repository.dart b/lib/user/repositories/user_repository.dart index 9d25b83540..789a10e2a7 100644 --- a/lib/user/repositories/user_repository.dart +++ b/lib/user/repositories/user_repository.dart @@ -117,6 +117,9 @@ class _UserRepository implements UserRepository { if (isArioSDKSupportedOnPlatform()) { ioTokens = await _arioSDK.getIOTokens(await wallet.getAddress()); + if (ioTokens == 'null') { + throw Exception('Error fetching IOTokens'); + } } return ioTokens;