diff --git a/lib/blocs/create_snapshot/create_snapshot_cubit.dart b/lib/blocs/create_snapshot/create_snapshot_cubit.dart index 456a270290..6c1c733f9a 100644 --- a/lib/blocs/create_snapshot/create_snapshot_cubit.dart +++ b/lib/blocs/create_snapshot/create_snapshot_cubit.dart @@ -2,15 +2,20 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io' show BytesBuilder; +import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/constants.dart'; import 'package:ardrive/blocs/profile/profile_cubit.dart'; +import 'package:ardrive/blocs/upload/upload_cubit.dart'; import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/entities/snapshot_entity.dart'; import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/daos/daos.dart'; -import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive/services/pst/pst.dart'; +import 'package:ardrive/services/services.dart'; +import 'package:ardrive/turbo/services/payment_service.dart'; +import 'package:ardrive/turbo/services/upload_service.dart'; +import 'package:ardrive/turbo/turbo.dart'; +import 'package:ardrive/turbo/utils/utils.dart'; import 'package:ardrive/utils/html/html_util.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive/utils/metadata_cache.dart'; @@ -18,7 +23,6 @@ import 'package:ardrive/utils/snapshots/height_range.dart'; import 'package:ardrive/utils/snapshots/range.dart'; import 'package:ardrive/utils/snapshots/snapshot_item_to_be_created.dart'; import 'package:arweave/arweave.dart'; -import 'package:arweave/utils.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,10 +33,15 @@ part 'create_snapshot_state.dart'; class CreateSnapshotCubit extends Cubit { final ArweaveService _arweave; + final PaymentService _paymentService; final DriveDao _driveDao; final ProfileCubit _profileCubit; final PstService _pst; final TabVisibilitySingleton _tabVisibility; + final TurboBalanceRetriever turboBalanceRetriever; + final ArDriveAuth auth; + final ConfigService configService; + final TurboUploadService turboService; @visibleForTesting bool throwOnDataComputingForTesting; @visibleForTesting @@ -44,23 +53,48 @@ class CreateSnapshotCubit extends Cubit { late String _ownerAddress; late Range _range; late int _currentHeight; - late Transaction _preparedTx; - + Uint8List? data; + DataItem? _preparedDataItem; + Transaction? _preparedTx; + + UploadMethod _uploadMethod = UploadMethod.ar; + UploadCostEstimate _costEstimateAr = UploadCostEstimate.zero(); + UploadCostEstimate _costEstimateTurbo = UploadCostEstimate.zero(); + bool _hasNoTurboBalance = false; + String _arBalance = ''; + String _turboCredits = ''; + BigInt _turboBalance = BigInt.zero; + bool _isButtonToUploadEnabled = false; + bool _isTurboUploadPossible = true; + bool _sufficentCreditsBalance = false; + bool _sufficientArBalance = false; + bool _isFreeThanksToTurbo = false; bool _wasSnapshotDataComputingCanceled = false; + bool get _useTurboUpload => + _uploadMethod == UploadMethod.turbo || _isFreeThanksToTurbo; + + AppConfig get appConfig => configService.config; + SnapshotItemToBeCreated? _itemToBeCreated; SnapshotEntity? _snapshotEntity; CreateSnapshotCubit({ required ArweaveService arweave, + required PaymentService paymentService, required ProfileCubit profileCubit, required DriveDao driveDao, required PstService pst, required TabVisibilitySingleton tabVisibility, + required this.turboBalanceRetriever, + required this.auth, + required this.configService, + required this.turboService, this.throwOnDataComputingForTesting = false, this.throwOnSignTxForTesting = false, this.returnWithoutSigningForTesting = false, }) : _arweave = arweave, + _paymentService = paymentService, _profileCubit = profileCubit, _driveDao = driveDao, _pst = pst, @@ -98,15 +132,16 @@ class CreateSnapshotCubit extends Cubit { _setupSnapshotEntityWithBlob(data); - await _prepareAndSignTx( - _snapshotEntity!, - data, - ); - - final costResult = await _computeCostAndCheckBalance(); - if (costResult == null) return; + await _computeCost(); + await _computeBalanceEstimate(); + _computeIsSufficientBalance(); + _computeIsTurboEnabled(); + _computeIsFreeThanksToTurbo(); + _computeIsButtonEnabled(); - await _emitConfirming(costResult, data.length); + await _emitConfirming( + dataSize: data.length, + ); } bool _wasCancelled() { @@ -180,7 +215,6 @@ class CreateSnapshotCubit extends Cubit { Future _prepareAndSignTx( SnapshotEntity snapshotEntity, - Uint8List data, ) async { logger.i('About to prepare and sign snapshot transaction'); @@ -191,10 +225,13 @@ class CreateSnapshotCubit extends Cubit { )); await prepareTx(isArConnectProfile); - await _pst.addCommunityTipToTx(_preparedTx); await signTx(isArConnectProfile); - snapshotEntity.txId = _preparedTx.id; + if (_useTurboUpload) { + snapshotEntity.txId = _preparedDataItem!.id; + } else { + snapshotEntity.txId = _preparedTx!.id; + } } @visibleForTesting @@ -207,13 +244,22 @@ class CreateSnapshotCubit extends Cubit { 'Preparing snapshot transaction with ${isArConnectProfile ? 'ArConnect' : 'JSON wallet'}', ); - _preparedTx = await _arweave.prepareEntityTx( - _snapshotEntity!, - wallet, - null, - // We'll sign it just after adding the tip - skipSignature: true, - ); + if (_useTurboUpload) { + _preparedDataItem = await _arweave.prepareEntityDataItem( + _snapshotEntity!, + wallet, + // We'll sign it just after adding the tip + skipSignature: true, + ); + } else { + _preparedTx = await _arweave.prepareEntityTx( + _snapshotEntity!, + wallet, + null, + // We'll sign it just after adding the tip + skipSignature: true, + ); + } } catch (e) { final isTabFocused = _tabVisibility.isTabFocused(); if (isArConnectProfile && !isTabFocused) { @@ -249,7 +295,11 @@ class CreateSnapshotCubit extends Cubit { return; } - await _preparedTx.sign(wallet); + if (_useTurboUpload) { + await _preparedDataItem!.sign(wallet); + } else { + await _preparedTx!.sign(wallet); + } } catch (e) { final isTabFocused = _tabVisibility.isTabFocused(); if (isArConnectProfile && !isTabFocused) { @@ -323,39 +373,180 @@ class CreateSnapshotCubit extends Cubit { _snapshotEntity = snapshotEntity; } - Future _computeCostAndCheckBalance() async { - final totalCost = _preparedTx.reward + _preparedTx.quantity; + Future _computeCost() async { + final profileState = _profileCubit.state as ProfileLoggedIn; + final wallet = profileState.wallet; - final profile = _profileCubit.state as ProfileLoggedIn; - final walletBalance = profile.walletBalance; - - if (walletBalance < totalCost) { - emit(CreateSnapshotInsufficientBalance( - walletBalance: walletBalance.toString(), - arCost: winstonToAr(totalCost), - )); - return null; + UploadCostEstimateCalculatorForAR costCalculatorForAr = + UploadCostEstimateCalculatorForAR( + arweaveService: _arweave, + pstService: _pst, + arCostToUsd: ConvertArToUSD(arweave: _arweave), + ); + + final turboCostCalc = TurboCostCalculator(paymentService: _paymentService); + TurboUploadCostCalculator costCalculatorForTurbo = + TurboUploadCostCalculator( + turboCostCalculator: turboCostCalc, + priceEstimator: TurboPriceEstimator( + wallet: wallet, + paymentService: _paymentService, + costCalculator: turboCostCalc, + ), + ); + + _costEstimateAr = await costCalculatorForAr.calculateCost( + totalSize: _snapshotEntity!.data!.length, + ); + _costEstimateTurbo = await costCalculatorForTurbo.calculateCost( + totalSize: _snapshotEntity!.data!.length, + ); + } + + Future refreshTurboBalance() async { + final profileState = _profileCubit.state as ProfileLoggedIn; + final wallet = profileState.wallet; + + final BigInt? fakeTurboCredits = appConfig.fakeTurboCredits; + + /// necessary to wait for backend update the balance + await Future.delayed(const Duration(seconds: 2)); + + final BigInt turboBalance = fakeTurboCredits ?? + await turboBalanceRetriever.getBalance(wallet).catchError((e) { + logger.e('Error while retrieving turbo balance', e); + return BigInt.zero; + }); + + logger.d('Balance after topping up: $turboBalance'); + + _turboBalance = turboBalance; + _hasNoTurboBalance = turboBalance == BigInt.zero; + _turboCredits = convertCreditsToLiteralString(turboBalance); + _sufficentCreditsBalance = _costEstimateTurbo.totalCost <= _turboBalance; + _computeIsTurboEnabled(); + _computeIsButtonEnabled(); + + if (state is ConfirmingSnapshotCreation) { + final stateAsConfirming = state as ConfirmingSnapshotCreation; + + logger.d('Refreshing turbo balance'); + logger.d('Turbo balance: $_turboCredits'); + logger.d('Has no turbo balance: $_hasNoTurboBalance'); + logger + .d('Sufficient balance to pay with turbo: $_sufficentCreditsBalance'); + logger.d('Upload method: $_uploadMethod'); + + emit( + stateAsConfirming.copyWith( + turboCredits: _turboCredits, + hasNoTurboBalance: _hasNoTurboBalance, + sufficientBalanceToPayWithTurbo: _sufficentCreditsBalance, + uploadMethod: _uploadMethod, + isButtonToUploadEnabled: _isButtonToUploadEnabled, + ), + ); } + } + + Future _computeBalanceEstimate() async { + final ProfileLoggedIn profileState = _profileCubit.state as ProfileLoggedIn; + final Wallet wallet = profileState.wallet; + + final BigInt? fakeTurboCredits = appConfig.fakeTurboCredits; - return totalCost; + final BigInt turboBalance = fakeTurboCredits ?? + await turboBalanceRetriever.getBalance(wallet).catchError((e) { + logger.e('Error while retrieving turbo balance', e); + return BigInt.zero; + }); + + logger.d('Balance before topping up: $turboBalance'); + + _turboBalance = turboBalance; + _hasNoTurboBalance = turboBalance == BigInt.zero; + _turboCredits = convertCreditsToLiteralString(turboBalance); + _arBalance = convertCreditsToLiteralString(auth.currentUser.walletBalance); } - Future _emitConfirming( - BigInt totalCost, - int dataSize, - ) async { - final arUploadCost = winstonToAr(totalCost); + void _computeIsTurboEnabled() async { + bool isTurboEnabled = appConfig.useTurboUpload; + _isTurboUploadPossible = isTurboEnabled && _sufficentCreditsBalance; + } + + void _computeIsSufficientBalance() { + final profileState = _profileCubit.state as ProfileLoggedIn; + + bool sufficientBalanceToPayWithAR = + profileState.walletBalance >= _costEstimateAr.totalCost; + bool sufficientBalanceToPayWithTurbo = + _costEstimateTurbo.totalCost <= _turboBalance; + + _sufficientArBalance = sufficientBalanceToPayWithAR; + _sufficentCreditsBalance = sufficientBalanceToPayWithTurbo; + } - final double? usdUploadCost = await ConvertArToUSD(arweave: _arweave) - .convertForUSD(double.parse(arUploadCost)); + void _computeIsFreeThanksToTurbo() { + final allowedDataItemSizeForTurbo = appConfig.allowedDataItemSizeForTurbo; + final forceNoFreeThanksToTurbo = appConfig.forceNoFreeThanksToTurbo; + final isFreeThanksToTurbo = + _snapshotEntity!.data!.length <= allowedDataItemSizeForTurbo; + _isFreeThanksToTurbo = isFreeThanksToTurbo && !forceNoFreeThanksToTurbo; + } + Future _emitConfirming({required int dataSize}) async { emit(ConfirmingSnapshotCreation( snapshotSize: dataSize, - arUploadCost: arUploadCost, - usdUploadCost: usdUploadCost, + costEstimateAr: _costEstimateAr, + costEstimateTurbo: _costEstimateTurbo, + hasNoTurboBalance: _hasNoTurboBalance, + isTurboUploadPossible: _isTurboUploadPossible, + arBalance: _arBalance, + turboCredits: _turboCredits, + uploadMethod: _uploadMethod, + isButtonToUploadEnabled: _isButtonToUploadEnabled, + sufficientBalanceToPayWithAr: _sufficientArBalance, + sufficientBalanceToPayWithTurbo: _sufficentCreditsBalance, + isFreeThanksToTurbo: _isFreeThanksToTurbo, )); } + void setUploadMethod(UploadMethod method) { + logger.d('Upload method set to $method'); + _uploadMethod = method; + + _computeIsButtonEnabled(); + if (state is ConfirmingSnapshotCreation) { + final stateAsConfirming = state as ConfirmingSnapshotCreation; + emit( + stateAsConfirming.copyWith( + uploadMethod: method, + isButtonToUploadEnabled: _isButtonToUploadEnabled, + ), + ); + } + } + + void _computeIsButtonEnabled() { + _isButtonToUploadEnabled = false; + + logger.d('Sufficient Balance To Pay With AR: $_sufficientArBalance'); + if (_uploadMethod == UploadMethod.ar && _sufficientArBalance) { + logger.d('Enabling button for AR payment method'); + _isButtonToUploadEnabled = true; + } else if (_uploadMethod == UploadMethod.turbo && + _isTurboUploadPossible && + _sufficentCreditsBalance) { + logger.d('Enabling button for Turbo payment method'); + _isButtonToUploadEnabled = true; + } else if (_isFreeThanksToTurbo) { + logger.d('Enabling button for free upload using Turbo'); + _isButtonToUploadEnabled = true; + } else { + logger.d('Disabling button'); + } + } + Future _jsonMetadataOfTxId(String txId) async { final drive = await _driveDao.driveById(driveId: _driveId).getSingleOrNull(); @@ -396,10 +587,20 @@ class CreateSnapshotCubit extends Cubit { return; } + await _prepareAndSignTx( + _snapshotEntity!, + ); + try { emit(UploadingSnapshot()); - await _arweave.postTx(_preparedTx); + if (_useTurboUpload) { + await _postTurboDataItem( + dataItem: _preparedDataItem!, + ); + } else { + await _arweave.postTx(_preparedTx!); + } emit(SnapshotUploadSuccess()); } catch (err, stacktrace) { @@ -408,6 +609,18 @@ class CreateSnapshotCubit extends Cubit { } } + Future _postTurboDataItem({required DataItem dataItem}) async { + final profile = _profileCubit.state as ProfileLoggedIn; + final wallet = profile.wallet; + + logger.d('Posting snapshot transaction for drive $_driveId'); + + await turboService.postDataItem( + dataItem: dataItem, + wallet: wallet, + ); + } + void cancelSnapshotCreation() { logger.i('User cancelled the snapshot creation'); diff --git a/lib/blocs/create_snapshot/create_snapshot_state.dart b/lib/blocs/create_snapshot/create_snapshot_state.dart index dd63bcaa86..924b4ff13c 100644 --- a/lib/blocs/create_snapshot/create_snapshot_state.dart +++ b/lib/blocs/create_snapshot/create_snapshot_state.dart @@ -54,20 +54,85 @@ class CreateSnapshotInsufficientBalance extends CreateSnapshotState { class ConfirmingSnapshotCreation extends CreateSnapshotState { final int snapshotSize; - final String arUploadCost; - final double? usdUploadCost; + + final UploadCostEstimate costEstimateAr; + final UploadCostEstimate? costEstimateTurbo; + final bool hasNoTurboBalance; + final bool isTurboUploadPossible; + final String arBalance; + final String turboCredits; + final UploadMethod uploadMethod; + final bool isButtonToUploadEnabled; + final bool sufficientBalanceToPayWithAr; + final bool sufficientBalanceToPayWithTurbo; + final bool isFreeThanksToTurbo; ConfirmingSnapshotCreation({ required this.snapshotSize, - required this.arUploadCost, - required this.usdUploadCost, + required this.costEstimateAr, + required this.costEstimateTurbo, + required this.hasNoTurboBalance, + required this.isTurboUploadPossible, + required this.arBalance, + required this.turboCredits, + required this.uploadMethod, + required this.isButtonToUploadEnabled, + required this.sufficientBalanceToPayWithAr, + required this.sufficientBalanceToPayWithTurbo, + required this.isFreeThanksToTurbo, }); @override List get props => [ snapshotSize, - arUploadCost, + costEstimateAr, + costEstimateTurbo ?? '', + hasNoTurboBalance, + isTurboUploadPossible, + arBalance, + turboCredits, + uploadMethod, + isButtonToUploadEnabled, + sufficientBalanceToPayWithAr, + sufficientBalanceToPayWithTurbo, + isFreeThanksToTurbo, ]; + + ConfirmingSnapshotCreation copyWith({ + int? snapshotSize, + String? arUploadCost, + double? usdUploadCost, + UploadCostEstimate? costEstimateAr, + UploadCostEstimate? costEstimateTurbo, + bool? hasNoTurboBalance, + bool? isTurboUploadPossible, + String? arBalance, + String? turboCredits, + UploadMethod? uploadMethod, + bool? isButtonToUploadEnabled, + bool? sufficientBalanceToPayWithAr, + bool? sufficientBalanceToPayWithTurbo, + bool? isFreeThanksToTurbo, + }) { + return ConfirmingSnapshotCreation( + snapshotSize: snapshotSize ?? this.snapshotSize, + costEstimateAr: costEstimateAr ?? this.costEstimateAr, + costEstimateTurbo: costEstimateTurbo ?? this.costEstimateTurbo, + hasNoTurboBalance: hasNoTurboBalance ?? this.hasNoTurboBalance, + isTurboUploadPossible: + isTurboUploadPossible ?? this.isTurboUploadPossible, + arBalance: arBalance ?? this.arBalance, + turboCredits: turboCredits ?? this.turboCredits, + uploadMethod: uploadMethod ?? this.uploadMethod, + isButtonToUploadEnabled: + isButtonToUploadEnabled ?? this.isButtonToUploadEnabled, + sufficientBalanceToPayWithAr: + sufficientBalanceToPayWithAr ?? this.sufficientBalanceToPayWithAr, + sufficientBalanceToPayWithTurbo: sufficientBalanceToPayWithTurbo ?? + this.sufficientBalanceToPayWithTurbo, + isFreeThanksToTurbo: isFreeThanksToTurbo ?? this.isFreeThanksToTurbo, + ); + } } class UploadingSnapshot extends CreateSnapshotState {} diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 006d8d0817..6a6f685f6c 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -71,8 +71,10 @@ class UploadCubit extends Cubit { logger.d('Disabling button'); } - emit((state as UploadReady).copyWith( - uploadMethod: method, isButtonToUploadEnabled: isButtonEnabled)); + emit(uploadReady.copyWith( + uploadMethod: method, + isButtonToUploadEnabled: isButtonEnabled, + )); } } diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 0016b3a182..d3096eeea8 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -155,11 +155,42 @@ class UploadReady extends UploadState { @override List get props => [ costEstimateAr, + costEstimateTurbo, sufficientArBalance, + isZeroBalance, + sufficentCreditsBalance, + uploadIsPublic, uploadPlanForAR, + uploadPlanForTurbo, + isTurboUploadPossible, isFreeThanksToTurbo, + uploadSize, + credits, + arBalance, + turboCredits, + uploadMethod, isButtonToUploadEnabled, ]; + + @override + toString() => 'UploadReady { ' + 'costEstimateAr: $costEstimateAr, ' + 'costEstimateTurbo: $costEstimateTurbo, ' + 'sufficientArBalance: $sufficientArBalance, ' + 'isZeroBalance: $isZeroBalance, ' + 'sufficentCreditsBalance: $sufficentCreditsBalance, ' + 'uploadIsPublic: $uploadIsPublic, ' + 'uploadPlanForAR: $uploadPlanForAR, ' + 'uploadPlanForTurbo: $uploadPlanForTurbo, ' + 'isTurboUploadPossible: $isTurboUploadPossible, ' + 'isFreeThanksToTurbo: $isFreeThanksToTurbo, ' + 'uploadSize: $uploadSize, ' + 'credits: $credits, ' + 'arBalance: $arBalance, ' + 'turboCredits: $turboCredits, ' + 'uploadMethod: $uploadMethod, ' + 'isButtonToUploadEnabled: $isButtonToUploadEnabled, ' + '}'; } class UploadInProgress extends UploadState { diff --git a/lib/components/create_snapshot_dialog.dart b/lib/components/create_snapshot_dialog.dart index b0951bddce..1203565c16 100644 --- a/lib/components/create_snapshot_dialog.dart +++ b/lib/components/create_snapshot_dialog.dart @@ -1,28 +1,31 @@ +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_snapshot/create_snapshot_cubit.dart'; -import 'package:ardrive/blocs/profile/profile_cubit.dart'; import 'package:ardrive/components/components.dart'; +import 'package:ardrive/components/payment_method_selector_widget.dart'; import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/arweave/arweave.dart'; +import 'package:ardrive/services/config/config.dart'; import 'package:ardrive/services/pst/pst.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/turbo/services/payment_service.dart'; +import 'package:ardrive/turbo/services/upload_service.dart'; +import 'package:ardrive/turbo/turbo.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/filesize.dart'; import 'package:ardrive/utils/html/html_util.dart'; import 'package:ardrive/utils/logger/logger.dart'; -import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive/utils/split_localizations.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../utils/usd_upload_cost_to_string.dart'; - Future promptToCreateSnapshot( BuildContext context, Drive drive, ) async { - return showArDriveDialog( + return showAnimatedDialog( context, barrierDismissible: false, content: BlocProvider( @@ -32,6 +35,13 @@ Future promptToCreateSnapshot( profileCubit: context.read(), pst: context.read(), tabVisibility: TabVisibilitySingleton(), + auth: context.read(), + paymentService: context.read(), + turboBalanceRetriever: TurboBalanceRetriever( + paymentService: context.read(), + ), + configService: context.read(), + turboService: context.read(), ), child: CreateSnapshotDialog( drive: drive, @@ -365,31 +375,6 @@ Widget _confirmDialog( ), style: ArDriveTypography.body.buttonNormalRegular(), ), - const Divider(), - const SizedBox(height: 16), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: appLocalizationsOf(context).cost( - state.arUploadCost, - ), - ), - if (state.usdUploadCost != null) - TextSpan( - text: usdUploadCostToString( - state.usdUploadCost!, - ), - ) - else - TextSpan( - text: - ' ${appLocalizationsOf(context).usdPriceNotAvailable}', - ), - ], - style: ArDriveTypography.body.buttonNormalRegular(), - ), - ), Text.rich( TextSpan( children: [ @@ -402,6 +387,43 @@ Widget _confirmDialog( style: ArDriveTypography.body.buttonNormalRegular(), ), ), + const Divider(), + const SizedBox(height: 16), + if (state.isFreeThanksToTurbo) ...{ + Text( + appLocalizationsOf(context).freeTurboTransaction, + style: ArDriveTypography.body.buttonNormalRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), + ), + } else ...{ + PaymentMethodSelector( + uploadMethod: state.uploadMethod, + costEstimateTurbo: state.costEstimateTurbo, + costEstimateAr: state.costEstimateAr, + hasNoTurboBalance: state.hasNoTurboBalance, + isTurboUploadPossible: true, + arBalance: state.arBalance, + sufficientArBalance: state.sufficientBalanceToPayWithAr, + turboCredits: state.turboCredits, + sufficentCreditsBalance: + state.sufficientBalanceToPayWithTurbo, + isFreeThanksToTurbo: false, + onTurboTopupSucess: () { + createSnapshotCubit.refreshTurboBalance(); + }, + onArSelect: () { + createSnapshotCubit.setUploadMethod(UploadMethod.ar); + }, + onTurboSelect: () { + createSnapshotCubit + .setUploadMethod(UploadMethod.turbo); + }, + ), + } ], ), ), @@ -425,6 +447,7 @@ Widget _confirmDialog( await createSnapshotCubit.confirmSnapshotCreation(), }, title: appLocalizationsOf(context).uploadEmphasized, + isEnable: state.isButtonToUploadEnabled, ), } ], diff --git a/lib/components/new_button/new_button.dart b/lib/components/new_button/new_button.dart index ea28b3bc80..cd2475c810 100644 --- a/lib/components/new_button/new_button.dart +++ b/lib/components/new_button/new_button.dart @@ -304,8 +304,7 @@ class NewButton extends StatelessWidget { icon: ArDriveIcons.iconAttachDrive(size: defaultIconSize), ), if (driveDetailState is DriveDetailLoadSuccess && drive != null) ...[ - if (driveDetailState.currentDrive.privacy == 'public' && - drive != null) + if (driveDetailState.currentDrive.privacy == 'public') ArDriveNewButtonItem( onClick: () { promptToCreateManifest( @@ -319,8 +318,7 @@ class NewButton extends StatelessWidget { name: appLocalizations.createManifest, icon: ArDriveIcons.tournament(size: defaultIconSize), ), - if (context.read().config.enableQuickSyncAuthoring && - drive != null) + if (context.read().config.enableQuickSyncAuthoring) ArDriveNewButtonItem( onClick: () { promptToCreateSnapshot( @@ -329,10 +327,7 @@ class NewButton extends StatelessWidget { ); }, isDisabled: !driveDetailState.hasWritePermissions || - driveDetailState.driveIsEmpty || - !profile.hasMinimumBalanceForUpload( - minimumWalletBalance: minimumWalletBalance, - ), + driveDetailState.driveIsEmpty, name: appLocalizations.createSnapshot, icon: ArDriveIcons.iconCreateSnapshot(size: defaultIconSize), ), diff --git a/lib/components/payment_method_selector_widget.dart b/lib/components/payment_method_selector_widget.dart new file mode 100644 index 0000000000..cc188de72d --- /dev/null +++ b/lib/components/payment_method_selector_widget.dart @@ -0,0 +1,268 @@ +import 'package:ardrive/blocs/upload/upload_cubit.dart'; +import 'package:ardrive/core/upload/cost_calculator.dart'; +import 'package:ardrive/turbo/topup/views/topup_modal.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:arweave/utils.dart'; +import 'package:flutter/material.dart'; + +class PaymentMethodSelector extends StatelessWidget { + final UploadMethod uploadMethod; + final UploadCostEstimate? costEstimateTurbo; + final UploadCostEstimate costEstimateAr; + final bool hasNoTurboBalance; + final bool isTurboUploadPossible; + final String arBalance; + final bool sufficientArBalance; + final String turboCredits; + final bool sufficentCreditsBalance; + final bool isFreeThanksToTurbo; + final void Function() onTurboTopupSucess; + final void Function() onArSelect; + final void Function() onTurboSelect; + + const PaymentMethodSelector({ + super.key, + required this.uploadMethod, + required this.costEstimateTurbo, + required this.costEstimateAr, + required this.hasNoTurboBalance, + required this.isTurboUploadPossible, + required this.arBalance, + required this.sufficientArBalance, + required this.turboCredits, + required this.sufficentCreditsBalance, + required this.isFreeThanksToTurbo, + required this.onTurboTopupSucess, + required this.onArSelect, + required this.onTurboSelect, + }); + + @override + Widget build(context) { + return Column( + children: [ + if (!isFreeThanksToTurbo) ...[ + _buildContent(context), + const SizedBox(height: 16), + _getInsufficientBalanceMessage(context: context), + ], + ], + ); + } + + Widget _buildContent(BuildContext context) { + return Column( + children: [ + Text( + 'Payment method:', // TODO: localize + style: ArDriveTypography.body.buttonLargeBold(), + ), + const SizedBox( + height: 8, + ), + ArDriveRadioButtonGroup( + size: 15, + onChanged: (index, value) { + switch (index) { + case 0: + if (value) { + onArSelect(); + } + break; + + case 1: + if (value) { + onTurboSelect(); + } + break; + } + }, + options: [ + // FIXME: rename to RadioButtonOption + RadioButtonOptions( + value: uploadMethod == UploadMethod.ar, + // TODO: Localization + text: 'Cost: ${winstonToAr(costEstimateAr.totalCost)} AR', + textStyle: ArDriveTypography.body.buttonLargeBold(), + ), + if (costEstimateTurbo != null && isTurboUploadPossible) + RadioButtonOptions( + value: uploadMethod == UploadMethod.turbo, + // TODO: Localization + text: hasNoTurboBalance + ? '' + : 'Cost: ${winstonToAr(costEstimateTurbo!.totalCost)} Credits', + textStyle: ArDriveTypography.body.buttonLargeBold(), + content: hasNoTurboBalance + ? GestureDetector( + onTap: () { + showTurboTopupModal(context, onSuccess: () { + onTurboTopupSucess(); + }); + }, + child: ArDriveClickArea( + child: RichText( + text: TextSpan( + children: [ + // TODO: use text with multiple styles + TextSpan( + text: 'Use Turbo Credits', // TODO: localize + style: ArDriveTypography.body + .buttonLargeBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ) + .copyWith( + decoration: TextDecoration.underline, + ), + ), + TextSpan( + text: + ' for faster uploads.', // TODO: localize + style: ArDriveTypography.body.buttonLargeBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), + ), + ], + ), + ), + ), + ) + : null, + ) + ], + builder: (index, radioButton) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + radioButton, + Padding( + padding: const EdgeInsets.only(left: 24.0), + child: Text( + index == 0 + // TODO: localize + ? 'Wallet Balance: $arBalance AR' + : 'Turbo Balance: $turboCredits Credits', + style: ArDriveTypography.body.buttonNormalBold( + color: + ArDriveTheme.of(context).themeData.colors.themeFgMuted, + ), + ), + ), + ], + ), + ), + ], + ); + } + + Widget _getInsufficientBalanceMessage({ + required BuildContext context, + }) { + if (uploadMethod == UploadMethod.turbo && + !sufficentCreditsBalance && + sufficientArBalance) { + return GestureDetector( + onTap: () { + showTurboTopupModal(context, onSuccess: () { + onTurboTopupSucess(); + }); + }, + child: ArDriveClickArea( + child: Text.rich( + TextSpan( + text: 'Insufficient Credit balance for purchase. ', + style: ArDriveTypography.body.captionBold( + color: + ArDriveTheme.of(context).themeData.colors.themeErrorDefault, + ), + children: [ + TextSpan( + text: 'Add Credits', + style: ArDriveTypography.body + .captionBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, + ) + .copyWith(decoration: TextDecoration.underline), + ), + TextSpan( + text: ' to use Turbo.', + style: ArDriveTypography.body.captionBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, + ), + ), + ], + ), + ), + ), + ); + } else if (uploadMethod == UploadMethod.ar && !sufficientArBalance) { + return Text( + 'Insufficient AR balance for purchase.', + style: ArDriveTypography.body.captionBold( + color: ArDriveTheme.of(context).themeData.colors.themeErrorDefault, + ), + ); + } else if (!sufficentCreditsBalance && !sufficientArBalance) { + return GestureDetector( + onTap: () { + showTurboTopupModal(context, onSuccess: () { + onTurboTopupSucess(); + }); + }, + child: ArDriveClickArea( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + 'Insufficient balance to pay for this upload. You can either', + style: ArDriveTypography.body.captionBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, + ), + ), + TextSpan( + text: ' add Turbo credits to your profile', + style: ArDriveTypography.body + .captionBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, + ) + .copyWith( + decoration: TextDecoration.underline, + ), + ), + TextSpan( + text: ' or use AR', + style: ArDriveTypography.body.captionBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorDefault, + ), + ), + ], + ), + ), + ), + ); + } + return const SizedBox(); + } +} diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index ded7d5aaf7..a169b7c685 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -7,6 +7,7 @@ import 'package:ardrive/blocs/upload/models/upload_file.dart'; import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/blocs/upload/upload_handles/file_v2_upload_handle.dart'; import 'package:ardrive/components/file_picker_modal.dart'; +import 'package:ardrive/components/payment_method_selector_widget.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/core/upload/uploader.dart'; @@ -16,7 +17,6 @@ import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; -import 'package:ardrive/turbo/topup/views/topup_modal.dart'; import 'package:ardrive/turbo/turbo.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/filesize.dart'; @@ -25,7 +25,6 @@ import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:arweave/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -137,7 +136,6 @@ class UploadForm extends StatefulWidget { class _UploadFormState extends State { final _scrollController = ScrollController(); - UploadMethod? _uploadMethod; @override Widget build(BuildContext context) => BlocConsumer( @@ -340,10 +338,9 @@ class _UploadFormState extends State { final numberOfV2Files = state.uploadPlanForAR.fileV2UploadHandles.length; - _uploadMethod = state.uploadMethod; - logger.d( - ' is button to upload enabled: ${state.isButtonToUploadEnabled}'); + ' is button to upload enabled: ${state.isButtonToUploadEnabled}', + ); final v2Files = state.uploadPlanForAR.fileV2UploadHandles.values .map((e) => e) @@ -496,130 +493,33 @@ class _UploadFormState extends State { height: 8, ), }, - if (!state.isFreeThanksToTurbo) ...[ - Text( - 'Payment method:', - style: ArDriveTypography.body.buttonLargeBold(), - ), - const SizedBox( - height: 8, - ), - ArDriveRadioButtonGroup( - size: 15, - onChanged: (index, value) { - switch (index) { - case 0: - if (value) { - context - .read() - .setUploadMethod(UploadMethod.ar); - } - break; - - case 1: - if (value) { - context - .read() - .setUploadMethod(UploadMethod.turbo); - } - break; - } - }, - options: [ - RadioButtonOptions( - value: state.uploadMethod == UploadMethod.ar, - // TODO: Localization - text: - 'Cost: ${winstonToAr(state.costEstimateAr.totalCost)} AR', - textStyle: ArDriveTypography.body.buttonLargeBold(), - ), - if (state.costEstimateTurbo != null && - state.isTurboUploadPossible) - RadioButtonOptions( - value: state.uploadMethod == UploadMethod.turbo, - // TODO: Localization - text: state.isZeroBalance - ? '' - : 'Cost: ${winstonToAr(state.costEstimateTurbo!.totalCost)} Credits', - textStyle: ArDriveTypography.body.buttonLargeBold(), - content: state.isZeroBalance - ? GestureDetector( - onTap: () { - showTurboModal(context, onSuccess: () { - context - .read() - .startUploadPreparation( - isRetryingToPayWithTurbo: true, - ); - }); - }, - child: ArDriveClickArea( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'Use Turbo Credits', - style: ArDriveTypography.body - .buttonLargeBold( - color: - ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ) - .copyWith( - decoration: TextDecoration - .underline, - ), - ), - TextSpan( - text: ' for faster uploads.', - style: ArDriveTypography.body - .buttonLargeBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ), - ), - ], - ), - ), - ), - ) - : null, - ) - ], - builder: (index, radioButton) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - radioButton, - Padding( - padding: const EdgeInsets.only(left: 24.0), - child: Text( - index == 0 - ? 'Wallet Balance: ${state.arBalance} AR' - : 'Turbo Balance: ${state.turboCredits} Credits', - style: ArDriveTypography.body.buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgMuted, - ), - ), - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - _getInsufficientBalanceMessage( - sufficentCreditsBalance: state.sufficentCreditsBalance, - sufficientArBalance: state.sufficientArBalance, - ), - ] + PaymentMethodSelector( + uploadMethod: state.uploadMethod, + costEstimateTurbo: state.costEstimateTurbo, + costEstimateAr: state.costEstimateAr, + hasNoTurboBalance: state.isZeroBalance, + isTurboUploadPossible: state.isTurboUploadPossible, + arBalance: state.arBalance, + sufficientArBalance: state.sufficientArBalance, + turboCredits: state.turboCredits, + sufficentCreditsBalance: state.sufficentCreditsBalance, + isFreeThanksToTurbo: state.isFreeThanksToTurbo, + onArSelect: () { + context + .read() + .setUploadMethod(UploadMethod.ar); + }, + onTurboSelect: () { + context + .read() + .setUploadMethod(UploadMethod.turbo); + }, + onTurboTopupSucess: () { + context.read().startUploadPreparation( + isRetryingToPayWithTurbo: true, + ); + }, + ), ], ), actions: [ @@ -848,114 +748,4 @@ class _UploadFormState extends State { return const SizedBox(); }, ); - - Widget _getInsufficientBalanceMessage({ - required bool sufficientArBalance, - required bool sufficentCreditsBalance, - }) { - if (_uploadMethod == UploadMethod.turbo && - !sufficentCreditsBalance && - sufficientArBalance) { - return GestureDetector( - onTap: () { - showTurboModal(context, onSuccess: () { - context.read().startUploadPreparation( - isRetryingToPayWithTurbo: true, - ); - }); - }, - child: ArDriveClickArea( - child: Text.rich( - TextSpan( - text: 'Insufficient Credit balance for purchase. ', - style: ArDriveTypography.body.captionBold( - color: - ArDriveTheme.of(context).themeData.colors.themeErrorDefault, - ), - children: [ - TextSpan( - text: 'Add Credits', - style: ArDriveTypography.body - .captionBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeErrorDefault, - ) - .copyWith(decoration: TextDecoration.underline), - ), - TextSpan( - text: ' to use Turbo.', - style: ArDriveTypography.body.captionBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeErrorDefault, - ), - ), - ], - ), - ), - ), - ); - } else if (_uploadMethod == UploadMethod.ar && !sufficientArBalance) { - return Text( - 'Insufficient AR balance for purchase.', - style: ArDriveTypography.body.captionBold( - color: ArDriveTheme.of(context).themeData.colors.themeErrorDefault, - ), - ); - } else if (!sufficentCreditsBalance && !sufficientArBalance) { - return GestureDetector( - onTap: () { - showTurboModal(context, onSuccess: () { - context.read().startUploadPreparation( - isRetryingToPayWithTurbo: true, - ); - }); - }, - child: ArDriveClickArea( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: - 'Insufficient balance to pay for this upload. You can either', - style: ArDriveTypography.body.captionBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeErrorDefault, - ), - ), - TextSpan( - text: ' add Turbo credits to your profile', - style: ArDriveTypography.body - .captionBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeErrorDefault, - ) - .copyWith( - decoration: TextDecoration.underline, - ), - ), - TextSpan( - text: ' or use AR', - style: ArDriveTypography.body.captionBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeErrorDefault, - ), - ), - ], - ), - ), - ), - ); - } - return const SizedBox(); - } } diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index 89d49b9e92..230c37b14f 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -98,161 +98,165 @@ class AppConfigWindowManagerState extends State { @override Widget build(BuildContext context) { - final settings = context.read().config; - final configService = context.read(); + final ConfigService configService = context.read(); + final AppConfig config = configService.config; - ArDriveDevToolOption defaultArweaveGatewayUrlOption = ArDriveDevToolOption( + final ArDriveDevToolOption defaultArweaveGatewayUrlOption = + ArDriveDevToolOption( name: 'defaultArweaveGatewayUrl', - value: settings.defaultArweaveGatewayUrl, + value: config.defaultArweaveGatewayUrl, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(defaultArweaveGatewayUrl: value), + config.copyWith(defaultArweaveGatewayUrl: value), ); }); }, type: ArDriveDevToolOptionType.text, ); - ArDriveDevToolOption useTurboOption = ArDriveDevToolOption( + final ArDriveDevToolOption useTurboOption = ArDriveDevToolOption( name: 'useTurboUpload', - value: settings.useTurboUpload, + value: config.useTurboUpload, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(useTurboUpload: value), + config.copyWith(useTurboUpload: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption useTurboPaymentOption = ArDriveDevToolOption( + final ArDriveDevToolOption useTurboPaymentOption = ArDriveDevToolOption( name: 'useTurboPayment', - value: settings.useTurboPayment, + value: config.useTurboPayment, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(useTurboPayment: value), + config.copyWith(useTurboPayment: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption defaultTurboUrlOption = ArDriveDevToolOption( + final ArDriveDevToolOption defaultTurboUrlOption = ArDriveDevToolOption( name: 'defaultTurboUrl', - value: settings.defaultTurboUploadUrl, + value: config.defaultTurboUploadUrl, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(defaultTurboUploadUrl: value), + config.copyWith(defaultTurboUploadUrl: value), ); }); }, type: ArDriveDevToolOptionType.text, ); - ArDriveDevToolOption defaultTurboPaymentUrlOption = ArDriveDevToolOption( + final ArDriveDevToolOption defaultTurboPaymentUrlOption = + ArDriveDevToolOption( name: 'defaultTurboUrl', - value: settings.defaultTurboPaymentUrl, + value: config.defaultTurboPaymentUrl, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(defaultTurboPaymentUrl: value), + config.copyWith(defaultTurboPaymentUrl: value), ); }); }, type: ArDriveDevToolOptionType.text, ); - ArDriveDevToolOption stripePublishableKey = ArDriveDevToolOption( + final ArDriveDevToolOption stripePublishableKey = ArDriveDevToolOption( name: 'stripePublishableKey', - value: settings.stripePublishableKey, + value: config.stripePublishableKey, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(stripePublishableKey: value), + config.copyWith(stripePublishableKey: value), ); }); }, type: ArDriveDevToolOptionType.text, ); - ArDriveDevToolOption allowedDataItemSizeForTurboOption = + final ArDriveDevToolOption allowedDataItemSizeForTurboOption = ArDriveDevToolOption( name: 'allowedDataItemSizeForTurbo', - value: settings.allowedDataItemSizeForTurbo, + value: config.allowedDataItemSizeForTurbo, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(allowedDataItemSizeForTurbo: value), + config.copyWith(allowedDataItemSizeForTurbo: value), ); }); }, type: ArDriveDevToolOptionType.number, ); - ArDriveDevToolOption enableQuickSyncAuthoringOption = ArDriveDevToolOption( + final ArDriveDevToolOption enableQuickSyncAuthoringOption = + ArDriveDevToolOption( name: 'enableQuickSyncAuthoring', - value: settings.enableQuickSyncAuthoring, + value: config.enableQuickSyncAuthoring, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(enableQuickSyncAuthoring: value), + config.copyWith(enableQuickSyncAuthoring: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption enableMultipleFileDownloadOption = + final ArDriveDevToolOption enableMultipleFileDownloadOption = ArDriveDevToolOption( name: 'enableMultipleFileDownload', - value: settings.enableMultipleFileDownload, + value: config.enableMultipleFileDownload, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(enableMultipleFileDownload: value), + config.copyWith(enableMultipleFileDownload: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption enableVideoPreviewOption = ArDriveDevToolOption( + final ArDriveDevToolOption enableVideoPreviewOption = ArDriveDevToolOption( name: 'enableVideoPreview', - value: settings.enableVideoPreview, + value: config.enableVideoPreview, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(enableVideoPreview: value), + config.copyWith(enableVideoPreview: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption enableAudioPreviewOption = ArDriveDevToolOption( + final ArDriveDevToolOption enableAudioPreviewOption = ArDriveDevToolOption( name: 'enableAudioPreview', - value: settings.enableAudioPreview, + value: config.enableAudioPreview, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(enableAudioPreview: value), + config.copyWith(enableAudioPreview: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption autoSyncIntervalInSecondsOption = ArDriveDevToolOption( + final ArDriveDevToolOption autoSyncIntervalInSecondsOption = + ArDriveDevToolOption( name: 'autoSyncIntervalInSeconds', - value: settings.autoSyncIntervalInSeconds, + value: config.autoSyncIntervalInSeconds, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(autoSyncIntervalInSeconds: value), + config.copyWith(autoSyncIntervalInSeconds: value), ); }); }, @@ -260,7 +264,7 @@ class AppConfigWindowManagerState extends State { ); // reload option - ArDriveDevToolOption reloadOption = ArDriveDevToolOption( + final ArDriveDevToolOption reloadOption = ArDriveDevToolOption( name: 'Reload', value: '', onChange: (value) { @@ -269,7 +273,7 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.button, ); - ArDriveDevToolOption resetOptions = ArDriveDevToolOption( + final ArDriveDevToolOption resetOptions = ArDriveDevToolOption( name: 'Reset options', value: '', onChange: (value) async { @@ -284,26 +288,27 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.buttonTertiary, ); - ArDriveDevToolOption enableSyncFromSnapshotOption = ArDriveDevToolOption( + final ArDriveDevToolOption enableSyncFromSnapshotOption = + ArDriveDevToolOption( name: 'enableSyncFromSnapshot', - value: settings.enableSyncFromSnapshot, + value: config.enableSyncFromSnapshot, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(enableSyncFromSnapshot: value), + config.copyWith(enableSyncFromSnapshot: value), ); }); }, type: ArDriveDevToolOptionType.bool, ); - ArDriveDevToolOption enableSeedPhreaseLogin = ArDriveDevToolOption( + final ArDriveDevToolOption enableSeedPhreaseLogin = ArDriveDevToolOption( name: 'enableSeedPhreaseLogin', - value: settings.enableSeedPhraseLogin, + value: config.enableSeedPhraseLogin, onChange: (value) { setState(() { configService.updateAppConfig( - settings.copyWith(enableSeedPhraseLogin: value), + config.copyWith(enableSeedPhraseLogin: value), ); }); }, @@ -311,7 +316,7 @@ class AppConfigWindowManagerState extends State { ); // reload option - ArDriveDevToolOption turboSetDefaultData = ArDriveDevToolOption( + final ArDriveDevToolOption turboSetDefaultData = ArDriveDevToolOption( name: 'setDefaultDataOnPaymentForm', value: '', onChange: (value) {}, @@ -327,7 +332,50 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.button, ); - List options = [ + final ArDriveDevToolOption forceNoFreeThanksToTurbo = ArDriveDevToolOption( + name: 'forceNoFreeThanksToTurbo', + value: config.forceNoFreeThanksToTurbo, + onChange: (value) { + setState(() { + configService.updateAppConfig( + config.copyWith(forceNoFreeThanksToTurbo: value), + ); + }); + }, + type: ArDriveDevToolOptionType.bool, + ); + + final ArDriveDevToolOption fakeTurboCredits = ArDriveDevToolOption( + name: 'fakeTurboCredits', + value: config.fakeTurboCredits, + onChange: (value) { + late AppConfig newConfig; + if (value == null) { + newConfig = config.copyWith(unsetFakeTurboCredits: true); + } else { + newConfig = config.copyWith(fakeTurboCredits: value); + } + setState(() { + configService.updateAppConfig(newConfig); + }); + }, + type: ArDriveDevToolOptionType.turboCredits, + ); + + final ArDriveDevToolOption topUpDryRun = ArDriveDevToolOption( + name: 'topUpDryRun', + value: config.topUpDryRun, + onChange: (value) { + setState(() { + configService.updateAppConfig( + config.copyWith(topUpDryRun: value), + ); + }); + }, + type: ArDriveDevToolOptionType.bool, + ); + + final List options = [ useTurboOption, useTurboPaymentOption, defaultTurboPaymentUrlOption, @@ -343,6 +391,9 @@ class AppConfigWindowManagerState extends State { defaultTurboUrlOption, autoSyncIntervalInSecondsOption, turboSetDefaultData, + forceNoFreeThanksToTurbo, + fakeTurboCredits, + topUpDryRun, reloadOption, resetOptions, ]; @@ -465,12 +516,39 @@ class AppConfigWindowManagerState extends State { option.onInteraction?.call(); }, ); + case ArDriveDevToolOptionType.buttonTertiary: return ArDriveButton( style: ArDriveButtonStyle.tertiary, text: option.name, onPressed: () => option.onChange(option.value), ); + + case ArDriveDevToolOptionType.turboCredits: + final optionAsBigInt = option as ArDriveDevToolOption; + return ArDriveTextField( + label: optionAsBigInt.name, + initialValue: optionAsBigInt.value != null + ? (optionAsBigInt.value! / BigInt.from(1000000000000)).toString() + : '', + onFieldSubmitted: (value) { + final doubleVaue = double.tryParse(value); + if (doubleVaue == null) { + optionAsBigInt.onChange(null); + showOptionSavedMessage(); + return; + } + + final winstonCredits = BigInt.from( + (doubleVaue * 1000000000000).floor(), + ); + optionAsBigInt.onChange(winstonCredits); + showOptionSavedMessage(); + }, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,12}')), + ], + ); } } @@ -600,14 +678,21 @@ class DraggableWindow extends HookWidget { } } -enum ArDriveDevToolOptionType { text, bool, number, button, buttonTertiary } +enum ArDriveDevToolOptionType { + text, + bool, + number, + button, + buttonTertiary, + turboCredits, +} -typedef OnChange = void Function(dynamic value); +typedef OnChange = void Function(T value); -class ArDriveDevToolOption { +class ArDriveDevToolOption { final String name; - dynamic value; - final OnChange onChange; + T value; + final OnChange onChange; final ArDriveDevToolOptionType type; final Function? onInteraction; diff --git a/lib/entities/snapshot_entity.dart b/lib/entities/snapshot_entity.dart index 98d901bae7..5b03f7edcc 100644 --- a/lib/entities/snapshot_entity.dart +++ b/lib/entities/snapshot_entity.dart @@ -117,4 +117,22 @@ class SnapshotEntity extends Entity { return tx; } + + @override + Future asDataItem(SecretKey? key) async { + if (key != null) { + throw UnsupportedError('Snapshot entities are not encrypted.'); + } + + final item = DataItem.withBlobData(data: data!); + final packageInfo = await PackageInfo.fromPlatform(); + + item.addTag(EntityTag.contentType, ContentType.json); + addEntityTagsToTransaction(item); + item.addApplicationTags( + version: packageInfo.version, + ); + + return item; + } } diff --git a/lib/services/config/app_config.dart b/lib/services/config/app_config.dart index 3b3725c5a0..8d2600754c 100644 --- a/lib/services/config/app_config.dart +++ b/lib/services/config/app_config.dart @@ -1,3 +1,4 @@ +import 'package:ardrive/utils/logger/logger.dart'; import 'package:json_annotation/json_annotation.dart'; part 'app_config.g.dart'; @@ -18,6 +19,9 @@ class AppConfig { final bool enableSyncFromSnapshot; final bool enableSeedPhraseLogin; final String stripePublishableKey; + final bool forceNoFreeThanksToTurbo; + final BigInt? fakeTurboCredits; + final bool topUpDryRun; AppConfig({ this.defaultArweaveGatewayUrl, @@ -34,6 +38,9 @@ class AppConfig { this.enableSyncFromSnapshot = true, this.enableSeedPhraseLogin = true, required this.stripePublishableKey, + this.forceNoFreeThanksToTurbo = false, + this.fakeTurboCredits, + this.topUpDryRun = false, }); AppConfig copyWith({ @@ -51,7 +58,15 @@ class AppConfig { bool? enableSyncFromSnapshot, bool? enableSeedPhraseLogin, String? stripePublishableKey, + bool? forceNoFreeThanksToTurbo, + BigInt? fakeTurboCredits, + bool? topUpDryRun, + bool? unsetFakeTurboCredits, }) { + final theFakeTurboCredits = unsetFakeTurboCredits == true + ? null + : fakeTurboCredits ?? this.fakeTurboCredits; + return AppConfig( defaultArweaveGatewayUrl: defaultArweaveGatewayUrl ?? this.defaultArweaveGatewayUrl, @@ -76,9 +91,41 @@ class AppConfig { enableSeedPhraseLogin: enableSeedPhraseLogin ?? this.enableSeedPhraseLogin, stripePublishableKey: stripePublishableKey ?? this.stripePublishableKey, + forceNoFreeThanksToTurbo: + forceNoFreeThanksToTurbo ?? this.forceNoFreeThanksToTurbo, + fakeTurboCredits: theFakeTurboCredits, + topUpDryRun: topUpDryRun ?? this.topUpDryRun, ); } + String diff(AppConfig other) { + // Compares this and the given AppConfig and returns a csv string + /// representing the differences. + + final thisJson = toJson(); + final otherJson = other.toJson(); + + final keysOfThis = thisJson.keys; + final keysOfOther = otherJson.keys; + final Set allKeys = {...keysOfThis, ...keysOfOther}; + + logger.d('All keys: $allKeys'); + logger.d('This: $thisJson'); + logger.d('Other: $otherJson'); + + final List diffs = []; + for (final key in allKeys) { + final valueOfThis = thisJson[key]; + final valueOfOther = otherJson[key]; + + if (valueOfThis != valueOfOther) { + diffs.add('$key: $valueOfThis -> $valueOfOther'); + } + } + + return diffs.join(', '); + } + @override String toString() => 'AppConfig(${toJson()})'; diff --git a/lib/services/config/config_service.dart b/lib/services/config/config_service.dart index b7c12fc67b..fd312396cf 100644 --- a/lib/services/config/config_service.dart +++ b/lib/services/config/config_service.dart @@ -55,9 +55,10 @@ class ConfigService { } } - void updateAppConfig(AppConfig config) { - _configFetcher.saveConfigOnDevToolsPrefs(config); - _config = config; + void updateAppConfig(AppConfig newConfig) { + // logger.d('App config updated: ${config.diff(newConfig)}'); + _configFetcher.saveConfigOnDevToolsPrefs(newConfig); + _config = newConfig; } Future resetDevToolsPrefs() async { diff --git a/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart b/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart index 8a55fb8f6c..fad9f3a497 100644 --- a/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart +++ b/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart @@ -47,7 +47,9 @@ class PaymentReviewBloc extends Bloc { userAcceptedToReceiveEmails: event.userAcceptedToReceiveEmails, ); - final paymentStatus = await turbo.confirmPayment(); + final paymentStatus = await turbo.confirmPayment( + dryRun: event.dryRun, + ); if (paymentStatus == PaymentStatus.success) { _emitPaymentSuccess(emit); diff --git a/lib/turbo/topup/blocs/payment_review/payment_review_event.dart b/lib/turbo/topup/blocs/payment_review/payment_review_event.dart index e5abbd7285..97ac2d813c 100644 --- a/lib/turbo/topup/blocs/payment_review/payment_review_event.dart +++ b/lib/turbo/topup/blocs/payment_review/payment_review_event.dart @@ -10,10 +10,12 @@ abstract class PaymentReviewEvent extends Equatable { class PaymentReviewFinishPayment extends PaymentReviewEvent { final String? email; final bool userAcceptedToReceiveEmails; + final bool dryRun; const PaymentReviewFinishPayment({ this.email, this.userAcceptedToReceiveEmails = false, + this.dryRun = false, }); @override diff --git a/lib/turbo/topup/components/turbo_balance_widget.dart b/lib/turbo/topup/components/turbo_balance_widget.dart index 8ca586a67b..e5d55e080a 100644 --- a/lib/turbo/topup/components/turbo_balance_widget.dart +++ b/lib/turbo/topup/components/turbo_balance_widget.dart @@ -59,7 +59,7 @@ class _TurboBalanceState extends State { borderRadius: 20, onPressed: () { showCookiePolicyConsentModal(context, (context) { - showTurboModal(context); + showTurboTopupModal(context); }); widget.onTapAddButton?.call(); diff --git a/lib/turbo/topup/views/topup_modal.dart b/lib/turbo/topup/views/topup_modal.dart index 832af999b3..b7d0b4c2d2 100644 --- a/lib/turbo/topup/views/topup_modal.dart +++ b/lib/turbo/topup/views/topup_modal.dart @@ -2,7 +2,7 @@ import 'package:animations/animations.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/components/top_up_dialog.dart'; import 'package:ardrive/core/activity_tracker.dart'; -import 'package:ardrive/services/config/config_service.dart'; +import 'package:ardrive/services/services.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/topup/blocs/payment_form/payment_form_bloc.dart'; import 'package:ardrive/turbo/topup/blocs/payment_review/payment_review_bloc.dart'; @@ -20,9 +20,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; -void showTurboModal(BuildContext context, {Function()? onSuccess}) { +void showTurboTopupModal(BuildContext context, {Function()? onSuccess}) { final activityTracker = context.read(); final sessionManager = TurboSessionManager(); + final appConfig = context.read().config; final costCalculator = TurboCostCalculator( paymentService: context.read(), @@ -56,7 +57,7 @@ void showTurboModal(BuildContext context, {Function()? onSuccess}) { supportedCountriesRetriever: turboSupportedCountriesRetriever, ); - initializeStripe(context.read().config); + initializeStripe(appConfig); activityTracker.setToppingUp(true); @@ -76,7 +77,10 @@ void showTurboModal(BuildContext context, {Function()? onSuccess}) { )..add(LoadInitialData()), ), ], - child: TurboModal(parentContext: modalContext), + child: TurboModal( + parentContext: modalContext, + appConfig: appConfig, + ), ), barrierDismissible: false, barrierColor: @@ -84,8 +88,6 @@ void showTurboModal(BuildContext context, {Function()? onSuccess}) { ).then((value) { logger.d('Turbo modal closed with value: ${turbo.paymentStatus}'); - activityTracker.setToppingUp(false); - if (turbo.paymentStatus == PaymentStatus.success) { logger.d('Turbo payment success'); @@ -93,14 +95,21 @@ void showTurboModal(BuildContext context, {Function()? onSuccess}) { } turbo.dispose(); + }).whenComplete(() { + activityTracker.setToppingUp(false); }); } class TurboModal extends StatefulWidget { - const TurboModal({super.key, required this.parentContext}); - + final AppConfig _appConfig; final BuildContext parentContext; + const TurboModal({ + super.key, + required this.parentContext, + required AppConfig appConfig, + }) : _appConfig = appConfig; + @override State createState() => _TurboModalState(); } @@ -206,9 +215,12 @@ class _TurboModalState extends State with TickerProviderStateMixin { state.priceEstimate, )..add(PaymentReviewLoadPaymentModel()), child: Container( - color: - ArDriveTheme.of(context).themeData.colors.themeBgCanvas, - child: const TurboReviewView()), + color: + ArDriveTheme.of(context).themeData.colors.themeBgCanvas, + child: TurboReviewView( + dryRun: widget._appConfig.topUpDryRun, + ), + ), ), ], ); @@ -282,7 +294,7 @@ class _TurboModalState extends State with TickerProviderStateMixin { Navigator.of(modalContext).pop(); Navigator.of(context).pop(); - showTurboModal(parentContext); + showTurboTopupModal(parentContext); }, ), ), diff --git a/lib/turbo/topup/views/topup_review_view.dart b/lib/turbo/topup/views/topup_review_view.dart index 391d25029d..0d858fee9b 100644 --- a/lib/turbo/topup/views/topup_review_view.dart +++ b/lib/turbo/topup/views/topup_review_view.dart @@ -17,7 +17,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:responsive_builder/responsive_builder.dart'; class TurboReviewView extends StatefulWidget { - const TurboReviewView({super.key}); + final bool dryRun; + const TurboReviewView({ + super.key, + required this.dryRun, + }); @override State createState() => _TurboReviewViewState(); @@ -582,6 +586,8 @@ class _TurboReviewViewState extends State { BlocBuilder( builder: (context, state) { return ScreenTypeLayout.builder( + // FIXME: the desktop section is never gonna be rendered + /// because its wrapped in another layout builder for mobile. desktop: (context) => ArDriveButton( maxHeight: 44, maxWidth: 143, @@ -611,6 +617,7 @@ class _TurboReviewViewState extends State { PaymentReviewFinishPayment( email: _emailController.text, userAcceptedToReceiveEmails: _emailChecked, + dryRun: widget.dryRun, ), ); }, @@ -644,6 +651,7 @@ class _TurboReviewViewState extends State { PaymentReviewFinishPayment( email: _emailController.text, userAcceptedToReceiveEmails: _emailChecked, + dryRun: widget.dryRun, ), ); }, @@ -730,6 +738,7 @@ class _TurboReviewViewState extends State { PaymentReviewFinishPayment( email: _emailController.text, userAcceptedToReceiveEmails: _emailChecked, + dryRun: widget.dryRun, ), ); }, @@ -763,6 +772,7 @@ class _TurboReviewViewState extends State { PaymentReviewFinishPayment( email: _emailController.text, userAcceptedToReceiveEmails: _emailChecked, + dryRun: widget.dryRun, ), ); }, diff --git a/lib/turbo/turbo.dart b/lib/turbo/turbo.dart index 9796b914b6..dc0e49bc2a 100644 --- a/lib/turbo/turbo.dart +++ b/lib/turbo/turbo.dart @@ -182,14 +182,21 @@ class Turbo extends Disposable { return _currentPaymentIntent!; } - Future confirmPayment() async { + Future confirmPayment({ + bool dryRun = false, + }) async { if (_currentPaymentIntent == null) { throw Exception( 'Current payment intent is null. You should create it before calling this method.'); } - logger.d('Confirming payment with payment provider'); + if (dryRun) { + logger.d('Confirming payment with dry run'); + _paymentStatus = PaymentStatus.success; + return _paymentStatus!; + } + logger.d('Confirming payment with payment provider'); _paymentStatus = await _paymentProvider.confirmPayment( paymentUserInformation: paymentUserInformation, paymentModel: _currentPaymentIntent!, diff --git a/pubspec.lock b/pubspec.lock index 009901e51d..9d98abad6c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -79,11 +79,11 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.11.0" - resolved-ref: "357ce9113b6c90eb00d3ae4528112a8228d9d11f" + ref: "v1.12.0" + resolved-ref: e8af5815f7e1c27fa72544a379971d4630599805 url: "https://github.com/ar-io/ardrive_ui.git" source: git - version: "1.11.0" + version: "1.12.0" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7aaa6d73b0..3bea42c1b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: ardrive_ui: git: url: https://github.com/ar-io/ardrive_ui.git - ref: v1.11.0 + ref: v1.12.0 artemis: ^7.0.0-beta.13 arweave: git: diff --git a/test/blocs/create_snapshot_cubit_test.dart b/test/blocs/create_snapshot_cubit_test.dart index d844bcb7e3..332c8f04a8 100644 --- a/test/blocs/create_snapshot_cubit_test.dart +++ b/test/blocs/create_snapshot_cubit_test.dart @@ -1,6 +1,12 @@ import 'package:ardrive/blocs/create_snapshot/create_snapshot_cubit.dart'; import 'package:ardrive/blocs/profile/profile_cubit.dart'; +import 'package:ardrive/entities/profile_types.dart'; import 'package:ardrive/entities/snapshot_entity.dart'; +import 'package:ardrive/services/config/app_config.dart'; +import 'package:ardrive/turbo/services/payment_service.dart'; +import 'package:ardrive/turbo/services/upload_service.dart'; +import 'package:ardrive/types/winston.dart'; +import 'package:ardrive/user/user.dart'; import 'package:ardrive/utils/snapshots/range.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart'; @@ -11,6 +17,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../test_utils/utils.dart'; +import '../turbo/turbo_test.dart'; Future fakePrepareTransaction(invocation) async { final entity = invocation.positionalArguments[0] as SnapshotEntity; @@ -28,6 +35,22 @@ Future fakePrepareTransaction(invocation) async { return transaction; } +Future fakePrepareDataItem(invocation) async { + final entity = invocation.positionalArguments[0] as SnapshotEntity; + final wallet = invocation.positionalArguments[1] as Wallet; + + final dataItem = await entity.asDataItem(null); + dataItem.setOwner(await wallet.getOwner()); + + await dataItem.sign(wallet); + + return dataItem; +} + +class MockAppConfig extends Mock implements AppConfig {} + +class MockTurboUploadService extends Mock implements TurboUploadService {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -40,6 +63,12 @@ void main() { final pst = MockPstService(); final tabVisibility = MockTabVisibilitySingleton(); final testWallet = getTestWallet(); + final configService = MockConfigService(); + final appConfig = MockAppConfig(); + final auth = MockArDriveAuth(); + final paymentService = MockPaymentService(); + final turboBalanceRetriever = MockTurboBalanceRetriever(); + final turboService = MockTurboUploadService(); setUpAll(() async { registerFallbackValue(SnapshotEntity()); @@ -47,11 +76,15 @@ void main() { registerFallbackValue( await getTestTransaction('test/fixtures/signed_v2_tx.json'), ); + registerFallbackValue( + await getTestDataItem('test/fixtures/signed_v2_tx.json'), + ); registerFallbackValue(Future.value()); }); setUp(() async { - // mocks the getSegmentedTransactionsFromDrive method of ardrive + registerFallbackValue(BigInt.one); + when( () => arweave.getSegmentedTransactionsFromDrive( any(), @@ -79,6 +112,12 @@ void main() { ), ).thenAnswer(fakePrepareTransaction); + when(() => arweave.prepareEntityDataItem( + any(), + any(), + skipSignature: any(named: 'skipSignature'), + )).thenAnswer(fakePrepareDataItem); + when(() => arweave.postTx(any())).thenAnswer( (_) async => Future.value(), ); @@ -125,6 +164,58 @@ void main() { (_) => Future.value(stubArToUsdFactor), ); + when(() => arweave.getPrice(byteSize: any(named: 'byteSize'))) + .thenAnswer((invocation) async => BigInt.one); + + when(() => pst.getPSTFee(any())) + .thenAnswer((invocation) async => Winston(BigInt.one)); + + when(() => paymentService.getPriceForBytes( + byteSize: any(named: 'byteSize'))) + .thenAnswer((invocation) async => BigInt.one); + + when(() => paymentService.getPriceForFiat( + wallet: null, + amount: any(named: 'amount'), + currency: any(named: 'currency'), + )).thenAnswer((invocation) async => PriceForFiat.zero()); + + when(() => turboBalanceRetriever.getBalance(any())) + .thenAnswer((invocation) async => BigInt.one); + + final MockWallet wallet = MockWallet(); + const address = 'addr'; + final cipher = SecretKey([1, 2, 3, 4, 5]); + + when(() => auth.currentUser).thenAnswer((invocation) => User( + password: 'password', + wallet: wallet, + walletAddress: address, + walletBalance: BigInt.one, + cipherKey: cipher, + profileType: ProfileType.json, + )); + + when(() => appConfig.allowedDataItemSizeForTurbo) + .thenAnswer((invocation) => 100); + when(() => appConfig.useTurboUpload).thenAnswer((invocation) => true); + when(() => appConfig.forceNoFreeThanksToTurbo) + .thenAnswer((invocation) => false); + when(() => appConfig.fakeTurboCredits).thenAnswer((invocation) => null); + when(() => appConfig.topUpDryRun).thenAnswer((invocation) => false); + + when(() => configService.config).thenAnswer((invocation) => appConfig); + + when(() => tabVisibility.isTabFocused()) + .thenAnswer((invocation) => true); + + when( + () => turboService.postDataItem( + dataItem: any(named: 'dataItem'), + wallet: any(named: 'wallet'), + ), + ).thenAnswer((invocation) => Future.value(null)); + // mocks PackageInfo PackageInfo.setMockInitialValues( appName: 'appName', @@ -143,6 +234,11 @@ void main() { driveDao: driveDao, tabVisibility: tabVisibility, pst: pst, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), expect: () => [], ); @@ -155,6 +251,11 @@ void main() { driveDao: driveDao, tabVisibility: tabVisibility, pst: pst, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) => cubit.confirmDriveAndHeighRange( 'driveId', @@ -165,8 +266,6 @@ void main() { driveId: 'driveId', range: Range(start: 0, end: 1), ), - PreparingAndSigningTransaction(isArConnectProfile: false), - // can't check for the actual value because it contains a signed transaction isA(), ], ); @@ -179,6 +278,11 @@ void main() { driveDao: driveDao, tabVisibility: tabVisibility, pst: pst, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) => cubit .confirmDriveAndHeighRange( @@ -191,9 +295,8 @@ void main() { driveId: 'driveId', range: Range(start: 0, end: 1), ), - PreparingAndSigningTransaction(isArConnectProfile: false), - // can't check for the actual value because it contains a signed transaction isA(), + PreparingAndSigningTransaction(isArConnectProfile: false), UploadingSnapshot(), SnapshotUploadSuccess(), ], @@ -208,6 +311,11 @@ void main() { tabVisibility: tabVisibility, pst: pst, throwOnDataComputingForTesting: true, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) => cubit.confirmDriveAndHeighRange( 'driveId', @@ -230,6 +338,11 @@ void main() { driveDao: driveDao, tabVisibility: tabVisibility, pst: pst, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) => cubit.confirmDriveAndHeighRange( 'driveId', @@ -240,7 +353,6 @@ void main() { driveId: 'driveId', range: Range(start: 0, end: 85), ), - PreparingAndSigningTransaction(isArConnectProfile: false), isA(), ], ); @@ -254,6 +366,11 @@ void main() { tabVisibility: tabVisibility, pst: pst, throwOnDataComputingForTesting: true, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) => cubit.confirmDriveAndHeighRange( 'driveId', @@ -276,6 +393,11 @@ void main() { driveDao: driveDao, tabVisibility: tabVisibility, pst: pst, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) async { await Future.wait([ @@ -355,6 +477,11 @@ void main() { driveDao: driveDao, tabVisibility: tabVisibility, pst: pst, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) async { await cubit.confirmDriveAndHeighRange( @@ -367,7 +494,6 @@ void main() { driveId: 'driveId', range: Range(start: 0, end: 1), ), - PreparingAndSigningTransaction(isArConnectProfile: true), isA(), ], ); @@ -381,6 +507,11 @@ void main() { tabVisibility: tabVisibility, pst: pst, throwOnSignTxForTesting: true, + auth: auth, + configService: configService, + paymentService: paymentService, + turboBalanceRetriever: turboBalanceRetriever, + turboService: turboService, ), act: (cubit) async { Future.delayed(const Duration(milliseconds: 8)).then((_) {