diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index 952d50ba8..a29b74188 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'dart:math'; -import 'package:intl/intl.dart'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/balance.dart'; @@ -12,17 +11,10 @@ class EVMChainERC20Balance extends Balance { final int exponent; @override - String get formattedAdditionalBalance => _balance(); + String get formattedAdditionalBalance => formatFixed(balance, exponent, fractionalDigits: 12); @override - String get formattedAvailableBalance => _balance(); - - String _balance() { - NumberFormat formatter = NumberFormat('0.00##########', 'en_US'); - double numBalance = (balance / BigInt.from(10).pow(exponent)).toDouble(); - String formattedBalance = formatter.format(numBalance); - return formattedBalance; - } + String get formattedAvailableBalance => formatFixed(balance, exponent, fractionalDigits: 12); String toJSON() => json.encode({ 'balanceInWei': balance.toString(), diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart index 38054c09d..8aee7fd83 100644 --- a/lib/src/screens/integrations/deuro/savings_page.dart +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -9,7 +9,9 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -37,86 +39,71 @@ class DEuroSavingsPage extends BasePage { @override String get title => S.current.deuro_savings; - Widget trailing(BuildContext context) => MergeSemantics( - child: SizedBox( - height: 37, - width: 37, - child: ButtonTheme( - minWidth: double.minPositive, - child: Semantics( - label: "Refresh", - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onSurface, - overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent), - ), - onPressed: _dEuroViewModel.reloadSavingsUserData, - child: Icon( - Icons.refresh, - color: pageIconColor(context), - size: 20, - ), - ), - ), - ), - ), - ); - @override Widget body(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, _dEuroViewModel)); - return Container( - width: double.infinity, - child: Column( - children: [ - Observer( - builder: (_) => SavingsCard( - isDarkTheme: currentTheme.isDark, - interestRate: "${_dEuroViewModel.interestRate}%", - savingsBalance: _dEuroViewModel.savingsBalance, - fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalance, - currency: CryptoCurrency.deuro, - fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, - onAddSavingsPressed: () => _onSavingsAdd(context), - onRemoveSavingsPressed: () => _onSavingsRemove(context), - onApproveSavingsPressed: _dEuroViewModel.prepareApproval, - onTooltipPressed: () => _onSavingsTooltipPressed(context), - isEnabled: _dEuroViewModel.isEnabled, - isLoading: _dEuroViewModel.isLoading, - ), - ), - Observer( - builder: (_) => InterestCardWidget( - isDarkTheme: currentTheme.isDark, - title: S.of(context).deuro_savings_collect_interest, - fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterest, - fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, - accruedInterest: _dEuroViewModel.accruedInterest, - onCollectInterest: _dEuroViewModel.prepareCollectInterest, - onReinvestInterest: _dEuroViewModel.prepareReinvestInterest, - onTooltipPressed: () => _onInterestTooltipPressed(context), - isEnabled: _dEuroViewModel.isEnabled, - ), - ), - Spacer(), - SafeArea( - child: Padding( - padding: EdgeInsets.only(bottom: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - InfoChip( - label: S.of(context).deuro_about_deuro, - icon: CupertinoIcons.info, - onPressed: () => _showWelcomeTooltip(context), + return RefreshIndicator( + displacement: responsiveLayoutUtil.screenHeight * 0.1, + onRefresh: _dEuroViewModel.reloadSavingsUserData, + child: CustomScrollView( + slivers: [ + SliverFillRemaining( + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Observer( + builder: (_) => SavingsCard( + isDarkTheme: currentTheme.isDark, + interestRate: "${_dEuroViewModel.interestRateFormated}%", + savingsBalance: _dEuroViewModel.savingsBalanceFormated, + fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated, + currency: CryptoCurrency.deuro, + fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, + onAddSavingsPressed: () => _onSavingsAdd(context), + onRemoveSavingsPressed: () => _onSavingsRemove(context), + onApproveSavingsPressed: _dEuroViewModel.prepareApproval, + onTooltipPressed: () => _onSavingsTooltipPressed(context), + isEnabled: _dEuroViewModel.isEnabled, + isLoading: _dEuroViewModel.isLoading, + ), + ), + Observer( + builder: (_) => InterestCardWidget( + isDarkTheme: currentTheme.isDark, + title: S.of(context).deuro_savings_collect_interest, + fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterestFormated, + fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, + accruedInterest: _dEuroViewModel.accruedInterestFormated, + onCollectInterest: _onCollectInterest, + onReinvestInterest: _onReinvestInterest, + onTooltipPressed: () => _onInterestTooltipPressed(context), + isEnabled: _dEuroViewModel.isSavingsActionsEnabled, + ), + ), + Spacer(), + SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InfoChip( + label: S.of(context).deuro_about_deuro, + icon: CupertinoIcons.info, + onPressed: () => _showWelcomeTooltip(context), + ), + SizedBox(width: 10), + InfoChip( + label: S.of(context).website, + icon: CupertinoIcons.globe, + onPressed: () => launchUrlString("https://deuro.com/"), + ) + ], + ), + ), ), - SizedBox(width: 10), - InfoChip( - label: S.of(context).website, - icon: CupertinoIcons.globe, - onPressed: () => launchUrlString("https://deuro.com/"), - ) ], ), ), @@ -126,14 +113,36 @@ class DEuroSavingsPage extends BasePage { ); } + bool _editSheetIsOpen = false; + Future _onSavingsAdd(BuildContext context) async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; final amount = await _showEditBottomSheet(context, isAdding: true); if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, true); + _editSheetIsOpen = false; } Future _onSavingsRemove(BuildContext context) async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; final amount = await _showEditBottomSheet(context, isAdding: false); if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, false); + _editSheetIsOpen = false; + } + + Future _onReinvestInterest() async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; + await _dEuroViewModel.prepareReinvestInterest(); + _editSheetIsOpen = false; + } + + Future _onCollectInterest() async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; + await _dEuroViewModel.prepareCollectInterest(); + _editSheetIsOpen = false; } bool _isReactionsSet = false; @@ -175,10 +184,10 @@ class DEuroSavingsPage extends BasePage { currency: CryptoCurrency.deuro, amount: S.of(bottomSheetContext).send_amount, amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest - ? _dEuroViewModel.accruedInterest + ? _dEuroViewModel.accruedInterestFormated : tx.amountFormatted, fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest - ? _dEuroViewModel.fiatAccruedInterest + ? _dEuroViewModel.fiatAccruedInterestFormated : _dEuroViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(bottomSheetContext).send_estimated_fee, feeValue: tx.feeFormatted, @@ -301,11 +310,11 @@ class DEuroSavingsPage extends BasePage { showModalBottomSheet( context: context, - builder: (BuildContext bottomSheetContext) => InfoBottomSheet( + builder: (BuildContext bottomSheetContext) => TooltipSheet( currentTheme: currentTheme, titleText: title, titleIconPath: CryptoCurrency.deuro.iconPath, - content: content, + tooltip: content, footerType: FooterType.doubleActionButton, doubleActionRightButtonText: S.of(context).close, rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'), @@ -358,7 +367,8 @@ class DEuroSavingsPage extends BasePage { content: S.of(context).deuro_tooltip_no_eth, singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'), singleActionButtonText: S.of(context).close, - onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton, + onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), + footerType: FooterType.singleActionButton, ), ); } @@ -375,7 +385,9 @@ class DEuroSavingsPage extends BasePage { balanceTitle: isAdding ? S.of(context).deuro_savings_available_to_add : S.of(context).deuro_savings_available_to_remove, - balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance, + balance: isAdding + ? _dEuroViewModel.accountBalanceFormated + : _dEuroViewModel.savingsBalanceFormated, footerType: FooterType.none, maxHeight: MediaQuery.of(context).size.height * 0.8, ), diff --git a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart index 2c9b66d57..558a39182 100644 --- a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart +++ b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cw_core/parse_fixed.dart'; import 'package:flutter/material.dart'; class SavingsEditSheet extends BaseBottomSheet { @@ -14,7 +15,8 @@ class SavingsEditSheet extends BaseBottomSheet { super.titleIconPath, this.balance, this.balanceTitle, - required super.footerType, required super.maxHeight, + required super.footerType, + required super.maxHeight, }); @override @@ -53,8 +55,35 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { } String amount = '0'; + bool isValid = false; final FocusNode _numpadFocusNode = FocusNode(); + void _onPressedAll() => setState(() { + amount = widget.balance!; + isValid = _validate(); + }); + + void _onNumberPressed(int i) => setState(() { + amount = amount == '0' ? i.toString() : '${amount}${i}'; + isValid = _validate(); + }); + + void _onDeletePressed() => setState(() { + amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0'; + isValid = _validate(); + }); + + void _onDecimalPressed() => setState(() { + amount = '${amount.replaceAll('.', '')}.'; + isValid = _validate(); + }); + + bool _validate() { + final amountBigInt = parseFixed(amount, 18); + final balanceBigInt = parseFixed(widget.balance!, 18); + return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero; + } + @override Widget build(BuildContext context) => SafeArea( child: Column(children: [ @@ -80,18 +109,14 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { AssetBalanceRow( title: widget.balanceTitle!, amount: widget.balance!, - onAllPressed: () => setState(() => amount = widget.balance!), + onAllPressed: _onPressedAll, ), ], NumberPad( focusNode: _numpadFocusNode, - onNumberPressed: (i) => setState( - () => amount = amount == '0' ? i.toString() : '${amount}${i}', - ), - onDeletePressed: () => setState( - () => amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0', - ), - onDecimalPressed: () => setState(() => amount = '${amount.replaceAll('.', '')}.'), + onNumberPressed: _onNumberPressed, + onDeletePressed: _onDeletePressed, + onDecimalPressed: _onDecimalPressed, ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), @@ -101,7 +126,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, isLoading: false, - isDisabled: false, + isDisabled: !isValid, ), ) ]), diff --git a/lib/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart new file mode 100644 index 000000000..6b75d3753 --- /dev/null +++ b/lib/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart @@ -0,0 +1,46 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; +import 'package:flutter/material.dart'; + +class TooltipSheet extends BaseBottomSheet { + final String tooltip; + + const TooltipSheet({ + required super.currentTheme, + required super.titleText, + super.titleIconPath, + required this.tooltip, + required super.footerType, + super.maxHeight = 900, + super.singleActionButtonText, + super.onSingleActionButtonPressed, + super.singleActionButtonKey, + super.doubleActionLeftButtonText, + super.doubleActionRightButtonText, + super.onLeftActionButtonPressed, + super.onRightActionButtonPressed, + super.leftActionButtonKey, + super.rightActionButtonKey, + }); + + @override + Widget contentWidget(BuildContext context) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.surfaceContainer), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.all(12), + child: AutoSizeText( + tooltip, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + decoration: TextDecoration.none, + ), + ), + ); + + Widget footerWidget(BuildContext context) => SizedBox.shrink(); +} diff --git a/lib/view_model/integrations/deuro_view_model.dart b/lib/view_model/integrations/deuro_view_model.dart index 5d989233a..31c3ff4a6 100644 --- a/lib/view_model/integrations/deuro_view_model.dart +++ b/lib/view_model/integrations/deuro_view_model.dart @@ -21,6 +21,8 @@ class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel; abstract class DEuroViewModelBase with Store { final AppStore _appStore; + static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000"); + DEuroViewModelBase( this._appStore, this.balanceViewModel, @@ -44,39 +46,31 @@ abstract class DEuroViewModelBase with Store { @action void acceptDisclaimer() => _settingsStore.shouldShowDEuroDisclaimer = false; - @computed - String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}'; - - @computed - String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}'; - FiatCurrency get fiat => _settingsStore.fiatCurrency; @computed - String get pendingTransactionFiatAmount => - transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted); - - @computed - String get pendingTransactionFeeFiatAmount { - try { - if (transaction != null) { - final currency = CryptoCurrency.eth; - return calculateFiatAmount( - price: _fiatConversationStore.prices[currency]!, - cryptoAmount: transaction!.feeFormattedValue, - ); - } else { - return '0.00'; - } - } catch (_) { - return '0.00'; - } + String get pendingTransactionFiatAmountFormatted { + final amount = transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted); + return isFiatDisabled ? '' : '$amount ${fiat.title}'; } @computed - String get accountBalance { + String get pendingTransactionFeeFiatAmountFormatted { + var amount = '0.00'; + try { + if (transaction != null) { + final feeCurrency = CryptoCurrency.eth; + amount = calculateFiatAmount( + price: _fiatConversationStore.prices[feeCurrency]!, + cryptoAmount: transaction!.feeFormattedValue, + ); + } + } catch (_) {} + return isFiatDisabled ? '' : '$amount ${fiat.title}'; + } + + @computed + String get accountBalanceFormated { final dEuroKey = balanceViewModel.balances.keys .firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title); if (dEuroKey == null) return '0.00'; @@ -84,22 +78,30 @@ abstract class DEuroViewModelBase with Store { } @observable - String savingsBalance = '0.00'; + BigInt savingsBalance = BigInt.zero; @computed - String get fiatSavingsBalance => _getDEuroFiatAmount(savingsBalance); + String get savingsBalanceFormated => + ethereum!.formatterEthereumAmountToDouble(amount: savingsBalance).toStringAsFixed(6); + + @computed + String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalanceFormated); @observable ExecutionState state = InitialExecutionState(); @observable - String interestRate = '0'; + String interestRateFormated = '0'; @observable - String accruedInterest = '0.00'; + BigInt accruedInterest = BigInt.zero; @computed - String get fiatAccruedInterest => _getDEuroFiatAmount(accruedInterest); + String get accruedInterestFormated => + ethereum!.formatterEthereumAmountToDouble(amount: accruedInterest).toStringAsFixed(6); + + @computed + String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterestFormated); @observable BigInt approvedTokens = BigInt.zero; @@ -107,6 +109,9 @@ abstract class DEuroViewModelBase with Store { @computed bool get isEnabled => approvedTokens > BigInt.zero; + @computed + bool get isSavingsActionsEnabled => isEnabled && accruedInterest >= MIN_ACCRUED_INTEREST; + @observable bool isLoading = true; @@ -121,17 +126,9 @@ abstract class DEuroViewModelBase with Store { @action Future reloadSavingsUserData() async { - final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!); - final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!); - approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!); - - savingsBalance = ethereum! - .formatterEthereumAmountToDouble(amount: await savingsBalanceRaw) - .toStringAsFixed(6); - accruedInterest = ethereum! - .formatterEthereumAmountToDouble(amount: await accruedInterestRaw) - .toStringAsFixed(6); + savingsBalance = await ethereum!.getDEuroSavingsBalance(_appStore.wallet!); + accruedInterest = await ethereum!.getDEuroAccruedInterest(_appStore.wallet!); isLoading = false; } @@ -139,7 +136,7 @@ abstract class DEuroViewModelBase with Store { Future reloadInterestRate() async { final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!); - interestRate = (interestRateRaw / BigInt.from(10000)).toString(); + interestRateFormated = (interestRateRaw / BigInt.from(10000)).toString(); } @action @@ -175,7 +172,7 @@ abstract class DEuroViewModelBase with Store { } } - Future prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false); + Future prepareCollectInterest() => prepareSavingsEdit(accruedInterestFormated, false); Future prepareReinvestInterest() async { try { @@ -230,13 +227,13 @@ abstract class DEuroViewModelBase with Store { String _getDEuroFiatAmount(String amount) { try { - CryptoCurrency deuro = CryptoCurrency.deuro; + var dEuro = CryptoCurrency.deuro; final keys = _fiatConversationStore.prices.keys.toList(); for (final key in keys) { - if (key.title == "DEURO") deuro = key; + if (key.title == "DEURO") dEuro = key; } return calculateFiatAmount( - price: _fiatConversationStore.prices[deuro]!, + price: _fiatConversationStore.prices[dEuro]!, cryptoAmount: amount, ); } catch (_) {