diff --git a/integration_test/e2e_stake_unstake.dart b/integration_test/e2e_stake_unstake.dart index 39305d9b2..09716a68c 100644 --- a/integration_test/e2e_stake_unstake.dart +++ b/integration_test/e2e_stake_unstake.dart @@ -48,8 +48,9 @@ Future e2eStakeUnstakeTest(WidgetTester tester) async { await tester.pumpAndSettle(); await tapButton(tester, localization.stakeUnstake, semantics: true); await tapButton(tester, localization.stake); - expect(widgetByText(localization.sendStakeTransaction), findsOneWidget); - await tapButton(tester, localization.stakeUnstake, semantics: true); + expect(widgetByText(localization.disableStakeTitle), findsOneWidget); + await tapButton(tester, localization.close); + await tester.pumpAndSettle(); await tapButton(tester, localization.unstake); expect(widgetByText(localization.emptyStakeTitle), findsOneWidget); diff --git a/lib/constants.dart b/lib/constants.dart index e2394fdde..a6976bf59 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -193,3 +193,7 @@ const List CUSTOM_ICON_NAMES = [ 'myWitWallet-title', 'myWitWallet-title-dark' ]; +double ONE_WIT_TO_NANO = 1000000000; +double MAX_STAKING_AMOUNT_NANOWIT = 10000000 * ONE_WIT_TO_NANO; +double MIN_STAKING_AMOUNT_NANOWIT = 10000 * ONE_WIT_TO_NANO; +String DEFAULT_WALLET_ID = '00000000'; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c0b3cc097..9c4aa959f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -355,5 +355,7 @@ "feesAndRewards": "Fees and rewards", "totalDataSynced": "Scan summary", "welcomeBack": "Welcome back", - "deleteWalletSettings": "Settings: Delete wallet" + "deleteWalletSettings": "Settings: Delete wallet", + "disableStakeTitle": "You don't have enough balance to stake", + "disableStakeMessage": "The minimun amount to stake is 10,000 WIT" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index ecf8b2df3..41d857c2c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -355,5 +355,7 @@ "feesAndRewards": "Tasas y Recompensas", "totalDataSynced": "Resumen de la sincronización", "deleteWalletSettings": "Configuración: Eliminar wallet", - "welcomeBack": "Bienvenido de nuevo" + "welcomeBack": "Bienvenido de nuevo", + "disableStakeTitle": "No tienes suficiente balance para hacer Stake", + "disableStakeMessage": "La cantidad minima para hacer Stake es de 10.000 WIT" } \ No newline at end of file diff --git a/lib/screens/create_wallet/enc_xprv_card.dart b/lib/screens/create_wallet/enc_xprv_card.dart index e64b8614b..a734dde2d 100644 --- a/lib/screens/create_wallet/enc_xprv_card.dart +++ b/lib/screens/create_wallet/enc_xprv_card.dart @@ -100,10 +100,10 @@ class EnterXprvCardState extends State } void setXprv(String value) { - xprv = XprvInput.dirty( - xprvType: _xprvType, - allowValidation: validationUtils.isFormUnFocus(_formFocusElements), - value: value); + xprv = XprvInput.dirty( + xprvType: _xprvType, + allowValidation: validationUtils.isFormUnFocus(_formFocusElements), + value: value); } void clearForm() { diff --git a/lib/util/storage/database/wallet.dart b/lib/util/storage/database/wallet.dart index 5e2ecd215..18d4f60f8 100644 --- a/lib/util/storage/database/wallet.dart +++ b/lib/util/storage/database/wallet.dart @@ -48,7 +48,7 @@ class Wallet { required this.internalAccounts, this.lastSynced = -1, }) { - this.id = '00000000'; + this.id = DEFAULT_WALLET_ID; this.externalAccounts.forEach((key, Account account) { account.balance; }); diff --git a/lib/widgets/inputs/input_amount.dart b/lib/widgets/inputs/input_amount.dart index 452fdb582..38a1cd6a6 100644 --- a/lib/widgets/inputs/input_amount.dart +++ b/lib/widgets/inputs/input_amount.dart @@ -3,12 +3,9 @@ import 'package:my_wit_wallet/util/get_localization.dart'; import 'package:my_wit_wallet/constants.dart'; import 'package:my_wit_wallet/widgets/buttons/text_btn.dart'; import 'package:my_wit_wallet/widgets/inputs/input_text.dart'; -import 'package:my_wit_wallet/widgets/validations/vtt_amount_input.dart'; - class InputAmount extends InputText { InputAmount({ - required this.amount, required super.focusNode, required super.styledTextController, super.prefixIcon, @@ -17,7 +14,6 @@ class InputAmount extends InputText { super.hint, super.keyboardType, super.obscureText, - this.route, super.onChanged, super.onEditingComplete, super.onFieldSubmitted, @@ -27,16 +23,11 @@ class InputAmount extends InputText { super.inputFormatters, }); - final VttAmountInput amount; - final String? route; - @override _InputAmountState createState() => _InputAmountState(); } class _InputAmountState extends State { - TextSelection? lastSelection; - @override void initState() { super.initState(); diff --git a/lib/widgets/inputs/input_slider.dart b/lib/widgets/inputs/input_slider.dart index f4fdcbc83..4ee23ba0f 100644 --- a/lib/widgets/inputs/input_slider.dart +++ b/lib/widgets/inputs/input_slider.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:my_wit_wallet/util/get_localization.dart'; import 'package:my_wit_wallet/constants.dart'; +import 'package:my_wit_wallet/util/extensions/num_extensions.dart'; +import 'package:my_wit_wallet/widgets/inputs/input_amount.dart'; import 'input_text.dart'; class InputSlider extends InputText { InputSlider({ - String? route, required this.maxAmount, required this.minAmount, required super.focusNode, @@ -24,6 +25,7 @@ class InputSlider extends InputText { super.onTap, super.onSuffixTap, super.inputFormatters, + super.enabled, }); final double maxAmount; @@ -48,6 +50,16 @@ class _InputSliderState extends State { widget.focusNode.removeListener(widget.onFocusChange); } + double getSliderValue({maxAmount, value, minAmount}) { + if (value >= maxAmount) { + return maxAmount; + } else if (value <= minAmount) { + return minAmount; + } else { + return value; + } + } + Widget build(BuildContext context) { final theme = Theme.of(context); @@ -58,6 +70,9 @@ class _InputSliderState extends State { String? inputValue = widget.styledTextController.text; double sliderValue = 0; + bool isSliderDisabled = widget.maxAmount < widget.minAmount; + double maxAmount = isSliderDisabled ? 0 : widget.maxAmount; + double minAmount = isSliderDisabled ? 0 : widget.minAmount; try { sliderValue = inputValue != '' ? double.parse(inputValue) : 0; } catch (err) { @@ -65,35 +80,39 @@ class _InputSliderState extends State { } return Column(children: [ Container( - child: widget.buildInput( - context: context, - decoration: InputDecoration( - hintText: widget.hint ?? localization.inputAmountHint, - errorText: widget.errorText, - prefixIcon: - widget.prefixIcon != null ? Icon(widget.prefixIcon) : null, - suffixText: WIT_UNIT[WitUnit.Wit], - suffixIconConstraints: BoxConstraints(minHeight: 44), - ), + child: InputAmount( + hint: localization.amount, + validator: (String? amount) => widget.errorText ?? null, + errorText: widget.errorText, + styledTextController: widget.styledTextController, + focusNode: widget.focusNode, + keyboardType: TextInputType.number, + onChanged: widget.onChanged, + onTap: widget.onTap, + inputFormatters: widget.inputFormatters, + onFieldSubmitted: widget.onFieldSubmitted, + onEditingComplete: widget.onEditingComplete, ), ), SizedBox(height: 8), Column(children: [ Slider( - value: - sliderValue >= widget.maxAmount ? widget.maxAmount : sliderValue, - max: widget.maxAmount, - min: widget.minAmount, + value: getSliderValue( + maxAmount: maxAmount, minAmount: minAmount, value: sliderValue), + max: maxAmount, + min: minAmount, label: sliderValue.toString(), onChanged: (double value) => {widget.onChanged!(value.toStringAsFixed(9))}, ), Row( children: [ - Text('Min ${widget.minAmount} ${WIT_UNIT[WitUnit.Wit]}', + Text( + 'Min ${widget.minAmount.standardizeWitUnits(inputUnit: WitUnit.Wit, outputUnit: WitUnit.Wit)} ${WIT_UNIT[WitUnit.Wit]}', style: theme.textTheme.bodySmall), Spacer(), - Text('Max ${widget.maxAmount} ${WIT_UNIT[WitUnit.Wit]}', + Text( + 'Max ${widget.maxAmount.standardizeWitUnits(inputUnit: WitUnit.Wit, outputUnit: WitUnit.Wit)} ${WIT_UNIT[WitUnit.Wit]}', style: theme.textTheme.bodySmall), ], ) diff --git a/lib/widgets/inputs/input_text.dart b/lib/widgets/inputs/input_text.dart index 518a3bc00..54b31c509 100644 --- a/lib/widgets/inputs/input_text.dart +++ b/lib/widgets/inputs/input_text.dart @@ -30,6 +30,7 @@ abstract class InputText extends StatefulWidget { this.textInputAction, this.maxLines, this.minLines, + this.enabled = true, }); final IconData? prefixIcon; final FocusNode focusNode; @@ -52,11 +53,13 @@ abstract class InputText extends StatefulWidget { final int? maxLines; final int? minLines; final TextInputAction? textInputAction; + final bool enabled; Widget buildInput( {required BuildContext context, InputDecoration? decoration = null}) { return TextFormField( decoration: decoration ?? decoration, + enabled: enabled, minLines: minLines, maxLines: maxLines ?? 1, keyboardType: keyboardType, diff --git a/lib/widgets/layouts/dashboard_layout.dart b/lib/widgets/layouts/dashboard_layout.dart index 90156189f..f3021c5d4 100644 --- a/lib/widgets/layouts/dashboard_layout.dart +++ b/lib/widgets/layouts/dashboard_layout.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:my_wit_wallet/constants.dart'; +import 'package:my_wit_wallet/screens/dashboard/bloc/dashboard_bloc.dart'; +import 'package:my_wit_wallet/util/clear_and_redirect.dart'; import 'package:my_wit_wallet/util/current_route.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:my_wit_wallet/screens/login/bloc/login_bloc.dart'; @@ -91,6 +93,22 @@ class DashboardLayoutState extends State }); } + Widget _buildDashboardListener(Widget content) { + return BlocListener( + listenWhen: (previousState, currentState) { + if ((previousState.currentWalletId != DEFAULT_WALLET_ID) && + (previousState.currentWalletId != currentState.currentWalletId)) { + clearAndRedirectToDashboard(context); + } + return true; + }, + listener: (BuildContext context, DashboardState state) {}, + child: BlocBuilder( + builder: (BuildContext context, DashboardState state) { + return content; + })); + } + Widget _authBuilder() { final theme = Theme.of(context); return BlocListener( @@ -155,7 +173,7 @@ class DashboardLayoutState extends State .getNavigationActions(context), isDashboard: true, bottomNavigation: _buildBottomNavigation(), - widgetList: [_body, SizedBox(height: 16)], + widgetList: [_buildDashboardListener(_body), SizedBox(height: 16)], actions: [], slidingPanel: panelContent, ); diff --git a/lib/widgets/layouts/send_transaction_layout.dart b/lib/widgets/layouts/send_transaction_layout.dart index 3b14d8c7e..3c5c5d752 100644 --- a/lib/widgets/layouts/send_transaction_layout.dart +++ b/lib/widgets/layouts/send_transaction_layout.dart @@ -6,7 +6,6 @@ import 'package:my_wit_wallet/bloc/transactions/value_transfer/vtt_create/vtt_cr import 'package:my_wit_wallet/shared/api_database.dart'; import 'package:my_wit_wallet/shared/locator.dart'; import 'package:my_wit_wallet/util/storage/database/wallet_storage.dart'; -import 'package:my_wit_wallet/screens/dashboard/bloc/dashboard_bloc.dart'; import 'package:my_wit_wallet/widgets/buttons/custom_btn.dart'; import 'package:my_wit_wallet/widgets/layouts/dashboard_layout.dart'; import 'package:my_wit_wallet/widgets/step_bar.dart'; @@ -261,37 +260,6 @@ class SendTransactionLayoutState extends State ); } - BlocListener _dashboardBlocListener() { - return BlocListener( - listener: (BuildContext context, DashboardState state) { - BlocProvider.of(context).add(ResetTransactionEvent()); - Navigator.pushReplacement( - context, - CustomPageRoute( - builder: (BuildContext context) { - return SendTransactionLayout( - routeName: widget.routeName, - transactionType: widget.transactionType, - ); - }, - maintainState: false, - settings: RouteSettings(name: widget.routeName))); - }, - child: _dashboardBlocBuilder(), - ); - } - - BlocBuilder _dashboardBlocBuilder() { - return BlocBuilder( - builder: (BuildContext context, DashboardState state) { - return DashboardLayout( - scrollController: scrollController, - dashboardChild: _transactionBlocListener(), - actions: [], - ); - }); - } - BlocListener _transactionBlocListener() { final theme = Theme.of(context); return BlocListener( @@ -328,7 +296,11 @@ class SendTransactionLayoutState extends State Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return _dashboardBlocListener(); + return DashboardLayout( + scrollController: scrollController, + dashboardChild: _transactionBlocListener(), + actions: [], + ); }); } } diff --git a/lib/widgets/stake_unstake.dart b/lib/widgets/stake_unstake.dart index 87dffe235..c0ee601de 100644 --- a/lib/widgets/stake_unstake.dart +++ b/lib/widgets/stake_unstake.dart @@ -1,5 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:my_wit_wallet/bloc/transactions/value_transfer/vtt_create/vtt_create_bloc.dart'; +import 'package:my_wit_wallet/constants.dart'; import 'package:my_wit_wallet/screens/dashboard/view/dashboard_screen.dart'; import 'package:my_wit_wallet/screens/stake/stake_screen.dart'; import 'package:my_wit_wallet/screens/unstake/unstake_screen.dart'; @@ -15,6 +16,7 @@ import 'package:my_wit_wallet/widgets/buttons/custom_btn.dart'; import 'package:my_wit_wallet/widgets/buttons/icon_btn.dart'; import 'package:my_wit_wallet/widgets/layouts/dashboard_layout.dart'; import 'package:flutter/material.dart'; +import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/modals/disable_stake_modal.dart'; import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/modals/empty_stake_modal.dart'; typedef void VoidCallback(); @@ -28,17 +30,28 @@ class StakeUnstakeButtons extends StatelessWidget { ApiDatabase db = Locator.instance.get(); Wallet currentWallet = db.walletStorage.currentWallet; late StakedBalanceInfo stakeInfo = currentWallet.stakedNanoWit(); + bool allowStake = MIN_STAKING_AMOUNT_NANOWIT < + currentWallet.balanceNanoWit().availableNanoWit; Future _goToStakeScreen() async { - BlocProvider.of(context).add(ResetTransactionEvent()); - Navigator.push( - context, - CustomPageRoute( - builder: (BuildContext context) { - return StakeScreen(); - }, - maintainState: false, - settings: RouteSettings(name: StakeScreen.route))); + if (allowStake) { + BlocProvider.of(context).add(ResetTransactionEvent()); + Navigator.push( + context, + CustomPageRoute( + builder: (BuildContext context) { + return StakeScreen(); + }, + maintainState: false, + settings: RouteSettings(name: StakeScreen.route))); + } else { + ScaffoldMessenger.of(context).clearSnackBars(); + buildDisableStakeModal( + theme: theme, + context: context, + originRouteName: DashboardScreen.route, + originRoute: DashboardScreen()); + } } Future _goToUnstakeScreen() async { @@ -56,6 +69,7 @@ class StakeUnstakeButtons extends StatelessWidget { ScaffoldMessenger.of(context).clearSnackBars(); buildEmptyStakeModal( theme: theme, + allowStake: allowStake, context: context, originRouteName: DashboardScreen.route, originRoute: DashboardScreen()); diff --git a/lib/widgets/validations/fee_amount_input.dart b/lib/widgets/validations/fee_amount_input.dart index ba973b752..91e303581 100644 --- a/lib/widgets/validations/fee_amount_input.dart +++ b/lib/widgets/validations/fee_amount_input.dart @@ -1,5 +1,5 @@ import 'package:my_wit_wallet/constants.dart'; -import 'package:my_wit_wallet/widgets/validations/vtt_amount_input.dart'; +import 'package:my_wit_wallet/widgets/validations/tx_amount_input.dart'; import 'package:my_wit_wallet/widgets/validations/validation_utils.dart'; import 'package:my_wit_wallet/util/extensions/num_extensions.dart'; import 'package:my_wit_wallet/util/get_localization.dart'; @@ -10,7 +10,7 @@ Map errorMap = { FeeInputError.minFee: localization.validationMinFee, }; -class FeeAmountInput extends VttAmountInput { +class FeeAmountInput extends TxAmountInput { final int availableNanoWit; final int? weightedAmount; final bool allowZero; @@ -38,20 +38,30 @@ class FeeAmountInput extends VttAmountInput { this.allowZero = false, this.allowValidation = false}) : super.dirty( - value: value, - allowZero: allowZero, - allowValidation: allowValidation, - availableNanoWit: availableNanoWit, - weightedAmount: weightedAmount); + value: value, + allowZero: allowZero, + allowValidation: allowValidation, + availableNanoWit: availableNanoWit, + ); // Override notEnoughFunds to handle validating taking into account the vttAmount @override bool notEnoughFunds({bool avoidWeightedAmountCheck = false}) { - int nanoWitAmount = super - .getNanoWitAmount(avoidWeightedAmountCheck: avoidWeightedAmountCheck); + int nanoWitAmount = + getNanoWitAmount(avoidWeightedAmountCheck: avoidWeightedAmountCheck); return this.availableNanoWit < (nanoWitAmount + this.vttAmount); } + int getNanoWitAmount({bool avoidWeightedAmountCheck = false}) { + int nanoWitAmount; + if (!avoidWeightedAmountCheck) { + nanoWitAmount = this.weightedAmount ?? witAmountToNanoWitNumber(value); + } else { + nanoWitAmount = witAmountToNanoWitNumber(value); + } + return nanoWitAmount; + } + // Override validator to handle validating a given input value. @override String? validator(String value, {bool avoidWeightedAmountCheck = false}) { diff --git a/lib/widgets/validations/vtt_amount_input.dart b/lib/widgets/validations/tx_amount_input.dart similarity index 71% rename from lib/widgets/validations/vtt_amount_input.dart rename to lib/widgets/validations/tx_amount_input.dart index 939ca9109..33262f01a 100644 --- a/lib/widgets/validations/vtt_amount_input.dart +++ b/lib/widgets/validations/tx_amount_input.dart @@ -11,6 +11,8 @@ enum AmountInputError { zero, decimals, invalidNumber, + lessThanMin, + greaterThanMax } Map errorMap = { @@ -19,29 +21,32 @@ Map errorMap = { AmountInputError.zero: 'Amount cannot be zero', AmountInputError.invalid: 'Invalid amount', AmountInputError.decimals: 'Only 9 decimal digits supported', + AmountInputError.lessThanMin: 'The amount is less than the minimum required', + AmountInputError.greaterThanMax: + 'The amount is greater than the maximum allowed' }; // Extend FormzInput and provide the input type and error type. -class VttAmountInput extends AmountInput { +class TxAmountInput extends AmountInput { final int availableNanoWit; - final int? weightedAmount; final bool allowZero; final bool allowValidation; + final bool isStakeAmount; // Call super.pure to represent an unmodified form input. - VttAmountInput.pure() + TxAmountInput.pure() : availableNanoWit = 0, allowZero = false, - weightedAmount = null, + isStakeAmount = false, allowValidation = false, super.pure(); // Call super.dirty to represent a modified form input. - VttAmountInput.dirty( + TxAmountInput.dirty( {required this.availableNanoWit, value = '', - this.weightedAmount, this.allowZero = false, + this.isStakeAmount = false, this.allowValidation = false}) : super.dirty( value: value, @@ -61,22 +66,20 @@ class VttAmountInput extends AmountInput { } } - int getNanoWitAmount({bool avoidWeightedAmountCheck = false}) { - int nanoWitAmount; - if (!avoidWeightedAmountCheck) { - nanoWitAmount = this.weightedAmount ?? witAmountToNanoWitNumber(value); - } else { - nanoWitAmount = witAmountToNanoWitNumber(value); - } - return nanoWitAmount; + int getNanoWitAmount() { + return witAmountToNanoWitNumber(value); } bool notEnoughFunds({bool avoidWeightedAmountCheck = false}) { - int nanoWitAmount = - getNanoWitAmount(avoidWeightedAmountCheck: avoidWeightedAmountCheck); + int nanoWitAmount = getNanoWitAmount(); return this.availableNanoWit < nanoWitAmount; } + bool get lessThanMinimum => + getNanoWitAmount() < MIN_STAKING_AMOUNT_NANOWIT.toInt(); + bool get greaterThanMaximum => + getNanoWitAmount() > MAX_STAKING_AMOUNT_NANOWIT.toInt(); + // Override validator to handle validating a given input value. @override String? validator(String value, {bool avoidWeightedAmountCheck = false}) { @@ -86,6 +89,13 @@ class VttAmountInput extends AmountInput { if (error != null) { return error; } + + if (isStakeAmount && greaterThanMaximum) { + return validationUtils.getErrorText(AmountInputError.greaterThanMax); + } + if (isStakeAmount && lessThanMinimum) { + return validationUtils.getErrorText(AmountInputError.lessThanMin); + } if (notEnoughFunds(avoidWeightedAmountCheck: avoidWeightedAmountCheck)) return validationUtils.getErrorText(AmountInputError.notEnough); return null; diff --git a/lib/widgets/wallet_list.dart b/lib/widgets/wallet_list.dart index 7894a18ea..3789f1efd 100644 --- a/lib/widgets/wallet_list.dart +++ b/lib/widgets/wallet_list.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:my_wit_wallet/util/clear_and_redirect.dart'; import 'package:my_wit_wallet/util/get_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:my_wit_wallet/constants.dart'; @@ -171,14 +170,10 @@ class WalletListState extends State { .formatWithCommaSeparator(), address: address ?? '', onChanged: (walletId) => { - setState(() { - selectedWallet = database.walletStorage.wallets[walletId]!; - }), BlocProvider.of(context).add( DashboardUpdateWalletEvent( - currentWallet: selectedWallet, + currentWallet: database.walletStorage.wallets[walletId]!, currentAddress: selectedAccount!.address)), - clearAndRedirectToDashboard(context), }, ); } else { diff --git a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart index 28b15cf1f..1a937f965 100644 --- a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart +++ b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart @@ -18,7 +18,7 @@ import 'package:my_wit_wallet/widgets/snack_bars.dart'; import 'package:my_wit_wallet/widgets/validations/address_input.dart'; import 'package:my_wit_wallet/widgets/validations/authorization_input.dart'; import 'package:my_wit_wallet/widgets/validations/validation_utils.dart'; -import 'package:my_wit_wallet/widgets/validations/vtt_amount_input.dart'; +import 'package:my_wit_wallet/widgets/validations/tx_amount_input.dart'; import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/timelock_input.dart'; import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/timelock_picker.dart'; import 'package:witnet/schema.dart'; @@ -62,13 +62,10 @@ class RecipientStepState extends State late AnimationController _loadingController; final _formKey = GlobalKey(); AddressInput _address = AddressInput.pure(); - VttAmountInput _amount = VttAmountInput.pure(); - VttAmountInput _stakeAmount = VttAmountInput.pure(); + TxAmountInput _amount = TxAmountInput.pure(); AuthorizationInput _authorization = AuthorizationInput.pure(); final _amountController = StyledTextController(); final _amountFocusNode = FocusNode(); - final _stakeAmountController = StyledTextController(); - final _stakeAmountFocusNode = FocusNode(); final _addressController = StyledTextController(); final _addressFocusNode = FocusNode(); final _authorizationController = StyledTextController(); @@ -183,10 +180,11 @@ class RecipientStepState extends State void setAmount(String value, {bool? validate}) { setState(() { - _amount = VttAmountInput.dirty( + _amount = TxAmountInput.dirty( availableNanoWit: isUnstakeTransaction ? stakeInfo.stakedNanoWit : balanceInfo.availableNanoWit, + isStakeAmount: isStakeTarnsaction, allowValidation: validate ?? validationUtils.isFormUnFocus(_formFocusElements()), value: value); @@ -202,14 +200,19 @@ class RecipientStepState extends State } void setAuthorization(String value, {bool? validate}) { - _authorization = AuthorizationInput.dirty( - withdrawerAddress: _address.value, - allowValidation: - validate ?? validationUtils.isFormUnFocus(_formFocusElements()), - value: value); + _authorization = AuthorizationInput.dirty( + withdrawerAddress: _address.value, + allowValidation: + validate ?? validationUtils.isFormUnFocus(_formFocusElements()), + value: value); } void _setSavedTxData() { + if (isStakeTarnsaction) { + _amountController.text = + MIN_STAKING_AMOUNT_NANOWIT.standardizeWitUnits().toString(); + setAmount(_amountController.text, validate: false); + } if (vttBloc.state.transaction.hasOutput(widget.transactionType)) { String? savedAddress = vttBloc.state.transaction.get(widget.transactionType) != null @@ -385,8 +388,6 @@ class RecipientStepState extends State onTapOutside: (PointerDownEvent? p) { _amountFocusNode.unfocus(); }, - amount: _amount, - route: widget.routeName, ), ), ]; @@ -396,28 +397,38 @@ class RecipientStepState extends State int balance = isUnstakeTransaction ? stakeInfo.stakedNanoWit : balanceInfo.availableNanoWit; + double standardizedBalance = + balance.standardizeWitUnits(truncate: -1).toDouble(); + double maxWitAmount = + MAX_STAKING_AMOUNT_NANOWIT.standardizeWitUnits(truncate: -1).toDouble(); + double minWitAmount = + MIN_STAKING_AMOUNT_NANOWIT.standardizeWitUnits(truncate: -1).toDouble(); + double maxAmount = + standardizedBalance > maxWitAmount ? maxWitAmount : standardizedBalance; return [ SizedBox(height: 16), LabeledFormEntry( label: localization.amount, formEntry: InputSlider( hint: localization.amount, - minAmount: 0.0, - maxAmount: balance.standardizeWitUnits(truncate: -1).toDouble(), - errorText: _stakeAmount.error, - styledTextController: _stakeAmountController, - focusNode: _stakeAmountFocusNode, + enabled: minWitAmount < maxAmount, + minAmount: minWitAmount, + inputFormatters: [WitValueFormatter()], + maxAmount: maxAmount, + errorText: _amount.error, + styledTextController: _amountController, + focusNode: _amountFocusNode, keyboardType: TextInputType.number, onChanged: (String value) { - _stakeAmountController.text = value; + _amountController.text = value; setAmount(value); }, onSuffixTap: () => { + _amountController.text = maxAmountWit, setAmount(maxAmountWit), - _stakeAmountController.text = maxAmountWit, }, onTap: () { - _stakeAmountFocusNode.requestFocus(); + _amountFocusNode.requestFocus(); }, onFieldSubmitted: (String value) { // hide keyboard diff --git a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart index c2fecdb68..c536b7ac5 100644 --- a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart +++ b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart @@ -275,7 +275,6 @@ class SelectMinerFeeStepState extends State onEditingComplete: () { _setAbsoluteFee(); }, - amount: _minerFeeWit, ), SizedBox(height: 16), Row( diff --git a/lib/widgets/witnet/transactions/value_transfer/modals/disable_stake_modal.dart b/lib/widgets/witnet/transactions/value_transfer/modals/disable_stake_modal.dart new file mode 100644 index 000000000..6e38ee7f6 --- /dev/null +++ b/lib/widgets/witnet/transactions/value_transfer/modals/disable_stake_modal.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:my_wit_wallet/util/get_localization.dart'; +import 'package:my_wit_wallet/theme/wallet_theme.dart'; +import 'package:my_wit_wallet/widgets/alert_dialog.dart'; +import 'package:my_wit_wallet/widgets/buttons/custom_btn.dart'; + +void buildDisableStakeModal({ + required ThemeData theme, + required BuildContext context, + required String originRouteName, + required Widget originRoute, + String iconName = 'empty', +}) { + return buildAlertDialog( + context: context, + actions: [ + CustomButton( + padding: EdgeInsets.zero, + text: localization.close, + sizeCover: false, + type: CustomBtnType.primary, + enabled: true, + onPressed: () => { + Navigator.popUntil( + context, ModalRoute.withName(originRouteName)), + ScaffoldMessenger.of(context).clearSnackBars(), + }), + ], + image: Container( + width: 100, height: 100, child: svgImage(name: iconName, height: 50)), + title: localization.disableStakeTitle, + content: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(localization.disableStakeMessage, + style: theme.textTheme.bodyLarge), + ])); +} diff --git a/lib/widgets/witnet/transactions/value_transfer/modals/empty_stake_modal.dart b/lib/widgets/witnet/transactions/value_transfer/modals/empty_stake_modal.dart index ad1e5a7dc..41057d1fa 100644 --- a/lib/widgets/witnet/transactions/value_transfer/modals/empty_stake_modal.dart +++ b/lib/widgets/witnet/transactions/value_transfer/modals/empty_stake_modal.dart @@ -10,6 +10,7 @@ void buildEmptyStakeModal({ required ThemeData theme, required BuildContext context, required String originRouteName, + bool allowStake = true, required Widget originRoute, String iconName = 'empty', }) { @@ -20,30 +21,33 @@ void buildEmptyStakeModal({ padding: EdgeInsets.zero, text: localization.close, sizeCover: false, - type: CustomBtnType.secondary, + type: allowStake ? CustomBtnType.secondary : CustomBtnType.primary, enabled: true, onPressed: () => { Navigator.popUntil( context, ModalRoute.withName(originRouteName)), ScaffoldMessenger.of(context).clearSnackBars(), }), - CustomButton( - padding: EdgeInsets.zero, - text: localization.stake, - sizeCover: false, - type: CustomBtnType.primary, - enabled: true, - onPressed: () => { - Navigator.push( - context, - CustomPageRoute( - builder: (BuildContext context) { - return StakeScreen(); - }, - maintainState: false, - settings: RouteSettings(name: StakeScreen.route))), - ScaffoldMessenger.of(context).clearSnackBars(), - }), + allowStake + ? CustomButton( + padding: EdgeInsets.zero, + text: localization.stake, + sizeCover: false, + type: CustomBtnType.primary, + enabled: true, + onPressed: () => { + Navigator.push( + context, + CustomPageRoute( + builder: (BuildContext context) { + return StakeScreen(); + }, + maintainState: false, + settings: + RouteSettings(name: StakeScreen.route))), + ScaffoldMessenger.of(context).clearSnackBars(), + }) + : Container(), ], image: Container( width: 100, height: 100, child: svgImage(name: iconName, height: 50)), diff --git a/test/validations/vtt_amount_input_test.dart b/test/validations/tx_amount_input_test.dart similarity index 58% rename from test/validations/vtt_amount_input_test.dart rename to test/validations/tx_amount_input_test.dart index 5b0db35b6..2914d704e 100644 --- a/test/validations/vtt_amount_input_test.dart +++ b/test/validations/tx_amount_input_test.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:my_wit_wallet/widgets/validations/vtt_amount_input.dart'; +import 'package:my_wit_wallet/widgets/validations/tx_amount_input.dart'; import 'package:test/test.dart'; void main() async { @@ -7,7 +7,7 @@ void main() async { test('Decimal number has more than 9 digits', () async { String amount = '0.00000000001'; - VttAmountInput _amount = VttAmountInput.dirty( + TxAmountInput _amount = TxAmountInput.dirty( value: amount, availableNanoWit: 1000, allowValidation: true); expect(_amount.validator(amount, avoidWeightedAmountCheck: true), @@ -15,7 +15,7 @@ void main() async { }); test('Not enough Funds', () async { String amount = '0.000000001'; - VttAmountInput _amount = VttAmountInput.dirty( + TxAmountInput _amount = TxAmountInput.dirty( value: amount, availableNanoWit: 0, allowValidation: true); expect(_amount.validator(amount, avoidWeightedAmountCheck: true), @@ -23,7 +23,7 @@ void main() async { }); test('Amount cannot be zero', () async { String amount = '0'; - VttAmountInput _amount = VttAmountInput.dirty( + TxAmountInput _amount = TxAmountInput.dirty( value: amount, availableNanoWit: 0, allowValidation: true); expect(_amount.validator(amount, avoidWeightedAmountCheck: true), @@ -31,7 +31,7 @@ void main() async { }); test('Amount can be zero', () async { String amount = '0'; - VttAmountInput _amount = VttAmountInput.dirty( + TxAmountInput _amount = TxAmountInput.dirty( value: amount, availableNanoWit: 0, allowValidation: true, @@ -41,7 +41,7 @@ void main() async { }); test('Invalid amount', () async { String amount = '0.'; - VttAmountInput _amount = VttAmountInput.dirty( + TxAmountInput _amount = TxAmountInput.dirty( value: amount, availableNanoWit: 0, allowValidation: true, @@ -50,4 +50,26 @@ void main() async { expect(_amount.validator(amount, avoidWeightedAmountCheck: true), errorMap[AmountInputError.invalid]); }); + test('Stake Amount is less than min', () async { + String amount = '0.2'; + TxAmountInput _amount = TxAmountInput.dirty( + value: amount, + availableNanoWit: 0, + allowValidation: true, + isStakeAmount: true); + + expect(_amount.validator(amount, avoidWeightedAmountCheck: true), + errorMap[AmountInputError.lessThanMin]); + }); + test('Stake Amount is greater than max', () async { + String amount = '20000000'; + TxAmountInput _amount = TxAmountInput.dirty( + value: amount, + availableNanoWit: 200000000000000000, + allowValidation: true, + isStakeAmount: true); + + expect(_amount.validator(amount, avoidWeightedAmountCheck: true), + errorMap[AmountInputError.greaterThanMax]); + }); }