diff --git a/lib/authentication/ardrive_auth.dart b/lib/authentication/ardrive_auth.dart index d7c9dfa138..4266983045 100644 --- a/lib/authentication/ardrive_auth.dart +++ b/lib/authentication/ardrive_auth.dart @@ -171,6 +171,8 @@ class ArDriveAuthImpl implements ArDriveAuth { currentUser = await _userRepository.getUser(password); + _updateBalance(); + if (await _biometricAuthentication.isEnabled()) { logger.i('Saving password in secure storage'); @@ -308,11 +310,33 @@ 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, + 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( String password, ProfileType profileType, @@ -364,11 +388,13 @@ class ArDriveAuthImpl implements ArDriveAuth { @override Future refreshBalance() async { - final balance = await _userRepository.getBalance(currentUser.wallet); - - currentUser = currentUser.copyWith(walletBalance: balance); + _currentUser = _currentUser!.copyWith( + errorFetchingIOTokens: false, + ); _userStreamController.add(_currentUser); + + _updateBalance(); } } 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..3d03d69210 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,70 @@ 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( - 'An error occurred while fetching IO tokens', - 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( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Error fetching tIO balance', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, + ), + ), + ArDriveIconButton( + icon: ArDriveIcons.refresh(), + onPressed: () { + context + .read() + .add(RefreshUserBalance()); + }, + ) + ], + ), + ] + ], ), - ), - ], + ); + } + return const SizedBox.shrink(); + }, ), ); } diff --git a/lib/main.dart b/lib/main.dart index 9b8873c46e..3b968f7d0b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -394,6 +394,7 @@ class AppState extends State { create: (context) => UserRepository( context.read(), context.read(), + ArioSDKFactory().create(), ), ), RepositoryProvider( 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 9384757c5d..789a10e2a7 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,7 @@ class _UserRepository implements UserRepository { walletBalance: await _arweave.getWalletBalance( await profileDetails.wallet.getAddress(), ), - ioTokens: ioTokens, + errorFetchingIOTokens: false, ); logger.d('Loaded user'); @@ -107,23 +111,18 @@ class _UserRepository implements UserRepository { return profile.walletPublicKey; } - Future _getIOTokens({ - required ProfileLoadDetails profileDetails, - }) async { - try { - String? ioTokens; + @override + Future getIOTokens(Wallet wallet) async { + String? ioTokens; - if (isArioSDKSupportedOnPlatform()) { - ioTokens = await ArioSDKFactory() - .create() - .getIOTokens(await profileDetails.wallet.getAddress()); + if (isArioSDKSupportedOnPlatform()) { + ioTokens = await _arioSDK.getIOTokens(await wallet.getAddress()); + if (ioTokens == 'null') { + throw Exception('Error fetching IOTokens'); } - - return ioTokens; - } catch (e, stacktrace) { - logger.e('Failed to get IO tokens', e, stacktrace); - return null; } + + return ioTokens; } @override diff --git a/lib/user/user.dart b/lib/user/user.dart index 713abdd93e..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,16 +74,18 @@ class _User implements User { required this.cipherKey, required this.profileType, this.ioTokens, + required this.errorFetchingIOTokens, }); @override - List get props => [ + List get props => [ password, walletAddress, walletBalance, cipherKey, profileType, wallet, + ioTokens, ]; @override @@ -95,6 +103,7 @@ class _User implements User { SecretKey? cipherKey, ProfileType? profileType, String? ioTokens, + bool? errorFetchingIOTokens, }) { return _User( password: password ?? this.password, @@ -104,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 00716e5ecf..2650bc383e 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. ', @@ -140,7 +141,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 +226,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 +285,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 +334,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)); @@ -372,6 +390,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); test( @@ -390,6 +409,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)); @@ -484,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 { @@ -491,6 +516,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); @@ -523,6 +552,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); test( @@ -532,6 +562,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()) @@ -588,6 +623,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); when( () => mockArweaveService.getFirstPrivateDriveTxId( @@ -597,6 +633,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, @@ -659,6 +700,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); @@ -674,6 +716,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(), @@ -708,6 +755,7 @@ void main() { walletBalance: BigInt.one, cipherKey: SecretKey([]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); arDriveAuth.onAuthStateChanged().listen((user) { @@ -774,5 +822,44 @@ 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, + errorFetchingIOTokens: false, + ); + // Updated user with new balance + final updatedUser = initialUser.copyWith(walletBalance: updatedBalance); + + arDriveAuth.currentUser = initialUser; + + 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 + arDriveAuth.onAuthStateChanged().listen((user) { + expect(user, equals(updatedUser)); + }); + }); + }); }); } 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 2d8512e943..0f76329911 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 @@ -80,6 +87,7 @@ void main() { walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); expect(result, isNotNull); @@ -146,6 +154,7 @@ void main() { walletBalance: BigInt.zero, cipherKey: SecretKey([1, 2, 3]), profileType: ProfileType.json, + errorFetchingIOTokens: false, ); when(() => mockProfileDao.addProfile( @@ -233,5 +242,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); + }); + }); }); } 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);