From e6cfa2b6365ca0d47da69b32e5a46245a98ed1a4 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Fri, 16 Jul 2021 12:32:36 +0300 Subject: [PATCH 01/17] CAKE-345 | reworked send page for batch sending; added send_item.dart, send_card.dart, send_template_view_model.dart and parse_address_from_domain_alert.dart to the app; reworked send template page; fixed exchange_trade_page.dart and exchange_trade_view_model.dart --- lib/di.dart | 13 +- .../exchange_trade/exchange_trade_page.dart | 6 +- lib/src/screens/send/send_page.dart | 900 ++++++------------ lib/src/screens/send/send_template_page.dart | 121 +-- .../parse_address_from_domain_alert.dart | 17 + lib/src/screens/send/widgets/send_card.dart | 521 ++++++++++ lib/themes/bright_theme.dart | 2 + lib/themes/dark_theme.dart | 2 + lib/themes/light_theme.dart | 2 + .../exchange/exchange_trade_view_model.dart | 7 +- lib/view_model/send/send_item.dart | 198 ++++ .../send/send_template_view_model.dart | 66 ++ lib/view_model/send/send_view_model.dart | 210 +--- 13 files changed, 1189 insertions(+), 876 deletions(-) create mode 100644 lib/src/screens/send/widgets/parse_address_from_domain_alert.dart create mode 100644 lib/src/screens/send/widgets/send_card.dart create mode 100644 lib/view_model/send/send_item.dart create mode 100644 lib/view_model/send/send_template_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index 37b6c5968..e5e298d5d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -72,6 +72,7 @@ import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model. import 'package:cake_wallet/view_model/order_details_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; @@ -307,10 +308,17 @@ Future setup( addressEditOrCreateViewModel: getIt.get(param1: item))); - getIt.registerFactory(() => SendViewModel( + getIt.registerFactory(() => SendTemplateViewModel( getIt.get().wallet, getIt.get().settingsStore, getIt.get(), + getIt.get() + )); + + getIt.registerFactory(() => SendViewModel( + getIt.get().wallet, + getIt.get().settingsStore, + getIt.get(), getIt.get(), _transactionDescriptionBox)); @@ -318,7 +326,8 @@ Future setup( () => SendPage(sendViewModel: getIt.get())); getIt.registerFactory( - () => SendTemplatePage(sendViewModel: getIt.get())); + () => SendTemplatePage( + sendTemplateViewModel: getIt.get())); getIt.registerFactory(() => WalletListViewModel( _walletInfoSource, diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 0a3b10e18..2b429dc62 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -262,6 +262,9 @@ class ExchangeTradeState extends State { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { + final item = widget.exchangeTradeViewModel.sendViewModel + .sendItemList.first; + showPopUp( context: context, builder: (BuildContext context) { @@ -386,8 +389,7 @@ class ExchangeTradeState extends State { ' ' + widget.exchangeTradeViewModel.sendViewModel.fiat.title, recipientTitle: S.of(context).recipient_address, - recipientAddress: - widget.exchangeTradeViewModel.sendViewModel.address); + recipientAddress: item.address); }); }); } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index eb4153676..85c5e6856 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,17 +1,13 @@ import 'dart:ui'; -import 'package:cake_wallet/entities/monero_transaction_priority.dart'; -import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/src/screens/send/widgets/parse_address_from_domain_alert.dart'; +import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; -import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_item.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/core/execution_state.dart'; @@ -21,42 +17,18 @@ import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cake_wallet/src/widgets/address_text_field.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class SendPage extends BasePage { - SendPage({@required this.sendViewModel}) - : _addressController = TextEditingController(), - _cryptoAmountController = TextEditingController(), - _fiatAmountController = TextEditingController(), - _noteController = TextEditingController(), - _formKey = GlobalKey(), - _cryptoAmountFocus = FocusNode(), - _fiatAmountFocus = FocusNode(), - _addressFocusNode = FocusNode() { - _addressFocusNode.addListener(() { - if (!_addressFocusNode.hasFocus && _addressController.text.isNotEmpty) { - getOpenaliasRecord(_addressFocusNode.context); - } - }); - } - - static const prefixIconWidth = 34.0; - static const prefixIconHeight = 34.0; + SendPage({@required this.sendViewModel}) :_formKey = GlobalKey(); final SendViewModel sendViewModel; - final TextEditingController _addressController; - final TextEditingController _cryptoAmountController; - final TextEditingController _fiatAmountController; - final TextEditingController _noteController; final GlobalKey _formKey; - final FocusNode _cryptoAmountFocus; - final FocusNode _fiatAmountFocus; - final FocusNode _addressFocusNode; + final controller = PageController(initialPage: 0); bool _effectsInstalled = false; @@ -76,505 +48,277 @@ class SendPage extends BasePage { AppBarStyle get appBarStyle => AppBarStyle.transparent; @override - Widget trailing(context) => TrailButton( - caption: S.of(context).clear, - onPressed: () { - _formKey.currentState.reset(); - sendViewModel.reset(); - }); + Widget trailing(context) => Observer(builder: (_) { + return sendViewModel.isRemoveButtonShow + ? TrailButton( + caption: S.of(context).remove, + onPressed: () { + var pageToJump = controller.page.round() - 1; + pageToJump = pageToJump > 0 ? pageToJump : 0; + final item = _defineCurrentSendItem(); + sendViewModel.removeSendItem(item); + controller.jumpToPage(pageToJump); + }) + : TrailButton( + caption: S.of(context).clear, + onPressed: () { + final item = _defineCurrentSendItem(); + _formKey.currentState.reset(); + item.reset(); + }); + }); @override Widget body(BuildContext context) { _setEffects(context); - return KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).accentTextTheme.body2 - .backgroundColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], + return Form( + key: _formKey, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Column( + children: [ + Container( + height: 445, + child: Observer( + builder: (_) { + return PageView.builder( + scrollDirection: Axis.horizontal, + controller: controller, + itemCount: sendViewModel.sendItemList.length, + itemBuilder: (context, index) { + final item = sendViewModel.sendItemList[index]; + + return SendCard( + key: item.key, + item: item, + sendViewModel: sendViewModel, + ); + } + ); + }, + ) ), - KeyboardActionsItem( - focusNode: _fiatAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: Container( - height: 0, - color: Theme.of(context).backgroundColor, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: Column( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).primaryTextTheme.subhead.color, - Theme.of(context) - .primaryTextTheme - .subhead - .decorationColor, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ), - child: Form( - key: _formKey, - child: Column(children: [ - Padding( - padding: EdgeInsets.fromLTRB(24, 100, 24, 32), - child: Column( - children: [ - AddressTextField( - focusNode: _addressFocusNode, - controller: _addressController, - onURIScanned: (uri) { - var address = ''; - var amount = ''; + Padding( + padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10), + child: Container( + height: 10, + child: Observer(builder: (_) { + final count = sendViewModel.sendItemList.length; - if (uri != null) { - address = uri.path; - amount = uri.queryParameters['tx_amount'] ?? - uri.queryParameters['amount']; - } else { - address = uri.toString(); - } - - _addressController.text = address; - _cryptoAmountController.text = amount; - }, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - buttonColor: Theme.of(context) + return count > 1 + ? SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) .primaryTextTheme - .display1 - .color, - borderColor: Theme.of(context) + .display2 + .backgroundColor, + activeDotColor: Theme.of(context) .primaryTextTheme - .headline - .color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor), - validator: sendViewModel.addressValidator, - ), - Observer( - builder: (_) => Padding( - padding: const EdgeInsets.only(top: 20), - child: Stack( - children: [ - BaseTextFormField( - focusNode: _cryptoAmountFocus, - controller: _cryptoAmountController, - keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), - prefixIcon: Padding( - padding: EdgeInsets.only(top: 9), - child: Text( - sendViewModel.currency.title + - ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - ), - suffixIcon: SizedBox( - width: prefixIconWidth, - ), - hintText: '0.0000', - borderColor: Theme.of(context) - .primaryTextTheme - .headline - .color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendViewModel.sendAll - ? sendViewModel.allAmountValidator - : sendViewModel - .amountValidator), - Positioned( - top: 2, - right: 0, - child: Container( - width: prefixIconWidth, - height: prefixIconHeight, - child: InkWell( - onTap: () async => - sendViewModel.setSendAll(), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .primaryTextTheme - .display1 - .color, - borderRadius: - BorderRadius.all( - Radius.circular(6))), - child: Center( - child: Text( - S.of(context).all, - textAlign: - TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.bold, - color: - Theme.of(context) - .primaryTextTheme - .display1 - .decorationColor))), - ))))]) - )), - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(top: 10), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - S.of(context).available_balance + - ':', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor), - )), - Text( - sendViewModel.balance, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor), - ) - ], - ), - )), - Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - focusNode: _fiatAmountFocus, - controller: _fiatAmountController, - keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), - prefixIcon: Padding( - padding: EdgeInsets.only(top: 9), - child: - Text(sendViewModel.fiat.title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - ), - hintText: '0.00', - borderColor: Theme.of(context) - .primaryTextTheme - .headline - .color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor, - fontWeight: FontWeight.w500, - fontSize: 14), - )), - Padding( - padding: EdgeInsets.only(top: 20), - child: BaseTextFormField( - controller: _noteController, - keyboardType: TextInputType.multiline, - maxLines: null, - borderColor: Theme.of(context) - .primaryTextTheme - .headline - .color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - hintText: S.of(context).note_optional, - placeholderTextStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor), - ), - ), - Observer( - builder: (_) => GestureDetector( - onTap: () => - _setTransactionPriority(context), - child: Container( - padding: EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S - .of(context) - .send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w500, - //color: Theme.of(context).primaryTextTheme.display2.color, - color: Colors.white)), - Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - sendViewModel - .estimatedFee - .toString() + - ' ' + - sendViewModel - .currency.title, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - //color: Theme.of(context).primaryTextTheme.display2.color, - color: - Colors.white)), - Padding( - padding: - EdgeInsets.only(top: 5), - child: Text( - sendViewModel - .estimatedFeeFiatAmount - + ' ' + - sendViewModel - .fiat.title, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - color: Theme - .of(context) - .primaryTextTheme - .headline - .decorationColor)) - ), - ], - ), - Padding( - padding: EdgeInsets.only( - top: 2, - left: 5), - child: Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, - ), - ) - ], - ), - ) - ], - ), - ), - )) - ], - ), - ) - ]), - ), - ), - Padding( - padding: EdgeInsets.only(top: 30, left: 24, bottom: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - S.of(context).send_templates, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, + .display3 + .backgroundColor + ) + ) + : Offstage(); + }) + ) + ), + Padding( + padding: EdgeInsets.only(left: 24, bottom: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + S.of(context).send_templates, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .display4 + .color), + ) + ], + ), + ), + Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.sendTemplate), + child: Container( + padding: EdgeInsets.only(left: 1, right: 10), + child: DottedBorder( + borderType: BorderType.RRect, + dashPattern: [6, 4], color: Theme.of(context) .primaryTextTheme - .display4 - .color), - ) - ], - ), - ), - Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.sendTemplate), - child: Container( - padding: EdgeInsets.only(left: 1, right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [6, 4], - color: Theme.of(context) - .primaryTextTheme - .display2 - .decorationColor, - strokeWidth: 2, - radius: Radius.circular(20), - child: Container( - height: 34, - width: 75, - padding: - EdgeInsets.only(left: 10, right: 10), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(20)), - color: Colors.transparent, - ), - child: Text( - S.of(context).send_new, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme - .display3 - .color), - ), - )), - ), - ), - Observer(builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; - - return ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - - return TemplateTile( - key: UniqueKey(), - to: template.name, - amount: template.amount, - from: template.cryptoCurrency, - onTap: () { - _addressController.text = - template.address; - _cryptoAmountController.text = - template.amount; - getOpenaliasRecord(context); - }, - onRemove: () { - showPopUp( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: - S.of(context).template, - alertContent: S - .of(context) - .confirm_delete_template, - rightButtonText: - S.of(context).delete, - leftButtonText: - S.of(context).cancel, - actionRightButton: () { - Navigator.of(dialogContext) - .pop(); - sendViewModel.removeTemplate( - template: template); - sendViewModel - .updateTemplate(); - }, - actionLeftButton: () => - Navigator.of(dialogContext) - .pop()); - }); - }, - ); - }); - }) - ], + .display2 + .decorationColor, + strokeWidth: 2, + radius: Radius.circular(20), + child: Container( + height: 34, + width: 75, + padding: + EdgeInsets.only(left: 10, right: 10), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(20)), + color: Colors.transparent, + ), + child: Text( + S.of(context).send_new, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .display3 + .color), + ), + )), + ), ), - ), - ) - ], + Observer(builder: (_) { + final templates = sendViewModel.templates; + final itemCount = templates.length; + + return ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final template = templates[index]; + + return TemplateTile( + key: UniqueKey(), + to: template.name, + amount: template.amount, + from: template.cryptoCurrency, + onTap: () async { + final item = _defineCurrentSendItem(); + item.address = + template.address; + item.setCryptoAmount(template.amount); + final record = + await item.getOpenaliasRecord(); + + if (record != null) { + showAddressAlert( + context, + S.current.openalias_alert_title, + S.current + .openalias_alert_content(record.name)); + } + }, + onRemove: () { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: + S.of(context).template, + alertContent: S + .of(context) + .confirm_delete_template, + rightButtonText: + S.of(context).delete, + leftButtonText: + S.of(context).cancel, + actionRightButton: () { + Navigator.of(dialogContext) + .pop(); + sendViewModel + .sendTemplateViewModel + .removeTemplate( + template: template); + }, + actionLeftButton: () => + Navigator.of(dialogContext) + .pop()); + }); + }, + ); + }); + }) + ], + ), + ), + ) + ], + ), + bottomSectionPadding: + EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column( + children: [ + PrimaryButton( + onPressed: () { + sendViewModel.addSendItem(); + }, + text: 'Add receiver', + color: Colors.green, + textColor: Colors.white, ), - bottomSectionPadding: - EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () async { - if (_formKey.currentState.validate()) { - await sendViewModel.createTransaction(); - } - }, - text: S.of(context).send, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, - isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting, - isDisabled: - false // FIXME !(syncStore.status is SyncedSyncStatus), + Padding( + padding: EdgeInsets.only(top: 12), + child: Observer(builder: (_) { + return LoadingPrimaryButton( + onPressed: () async { + if (_formKey.currentState.validate()) { + //await sendViewModel.createTransaction(); + // FIXME: for test only + sendViewModel.clearSendItemList(); + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).send, + alertContent: S.of(context).send_success( + sendViewModel.currency + .toString()), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } else { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: 'Please, check your receivers forms', + buttonText: S.of(context).ok, + buttonAction: () => + Navigator.of(context).pop()); + }); + } + }, + text: S.of(context).send, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + isLoading: sendViewModel.state is IsExecutingState || + sendViewModel.state is TransactionCommitting, + isDisabled: !sendViewModel.isReadyForSend, ); - })), - )); + }, + )) + ], + )), + ); } void _setEffects(BuildContext context) { @@ -582,78 +326,6 @@ class SendPage extends BasePage { return; } - _cryptoAmountController.addListener(() { - final amount = _cryptoAmountController.text; - - if (sendViewModel.sendAll && amount != S.current.all) { - sendViewModel.sendAll = false; - } - - if (amount != sendViewModel.cryptoAmount) { - sendViewModel.setCryptoAmount(amount); - } - }); - - _fiatAmountController.addListener(() { - final amount = _fiatAmountController.text; - - if (amount != sendViewModel.fiatAmount) { - sendViewModel.sendAll = false; - sendViewModel.setFiatAmount(amount); - } - }); - - _noteController.addListener(() { - final note = _noteController.text ?? ''; - - if (note != sendViewModel.note) { - sendViewModel.note = note; - } - }); - - reaction((_) => sendViewModel.sendAll, (bool all) { - if (all) { - _cryptoAmountController.text = S.current.all; - _fiatAmountController.text = null; - } - }); - - reaction((_) => sendViewModel.fiatAmount, (String amount) { - if (amount != _fiatAmountController.text) { - _fiatAmountController.text = amount; - } - }); - - reaction((_) => sendViewModel.cryptoAmount, (String amount) { - if (sendViewModel.sendAll && amount != S.current.all) { - sendViewModel.sendAll = false; - } - - if (amount != _cryptoAmountController.text) { - _cryptoAmountController.text = amount; - } - }); - - reaction((_) => sendViewModel.address, (String address) { - if (address != _addressController.text) { - _addressController.text = address; - } - }); - - _addressController.addListener(() { - final address = _addressController.text; - - if (sendViewModel.address != address) { - sendViewModel.address = address; - } - }); - - reaction((_) => sendViewModel.note, (String note) { - if (note != _noteController.text) { - _noteController.text = note; - } - }); - reaction((_) => sendViewModel.state, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -678,7 +350,7 @@ class SendPage extends BasePage { alertTitle: S.of(context).confirm_sending, amount: S.of(context).send_amount, amountValue: - sendViewModel.pendingTransaction.amountFormatted, + sendViewModel.pendingTransaction.amountFormatted, fiatAmountValue: sendViewModel.pendingTransactionFiatAmount + ' ' + sendViewModel.fiat.title, fee: S.of(context).send_fee, @@ -686,7 +358,7 @@ class SendPage extends BasePage { feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title, recipientTitle: S.of(context).recipient_address, - recipientAddress: sendViewModel.address, + recipientAddress: '', // FIXME: sendViewModel.address, rightButtonText: S.of(context).ok, leftButtonText: S.of(context).cancel, actionRightButton: () { @@ -724,8 +396,7 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) { - _addressController.text = ''; - _cryptoAmountController.text = ''; + sendViewModel.clearSendItemList(); }); } }); @@ -733,41 +404,8 @@ class SendPage extends BasePage { _effectsInstalled = true; } - Future getOpenaliasRecord(BuildContext context) async { - final record = - await sendViewModel.decodeOpenaliasRecord(_addressController.text); - - if (record != null) { - _addressController.text = record.address; - - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).openalias_alert_title, - alertContent: - S.of(context).openalias_alert_content(record.name), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - } - - Future _setTransactionPriority(BuildContext context) async { - final items = priorityForWalletType(sendViewModel.walletType); - final selectedItem = items.indexOf(sendViewModel.transactionPriority); - final isShowScrollThumb = items.length > 3; - - await showPopUp( - builder: (_) => Picker( - items: items, - displayItem: sendViewModel.displayFeeRate, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (TransactionPriority priority) => - sendViewModel.setTransactionPriority(priority), - ), - context: context); + SendItem _defineCurrentSendItem() { + final itemCount = controller.page.round(); + return sendViewModel.sendItemList[itemCount]; } } diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index c71b60803..baebe435a 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -2,11 +2,10 @@ import 'package:mobx/mobx.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/src/widgets/address_text_field.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; @@ -14,9 +13,11 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; class SendTemplatePage extends BasePage { - SendTemplatePage({@required this.sendViewModel}); + SendTemplatePage({@required this.sendTemplateViewModel}) { + sendTemplateViewModel.sendItem.reset(); + } - final SendViewModel sendViewModel; + final SendTemplateViewModel sendTemplateViewModel; final _addressController = TextEditingController(); final _cryptoAmountController = TextEditingController(); final _fiatAmountController = TextEditingController(); @@ -100,7 +101,7 @@ class SendTemplatePage extends BasePage { .decorationColor, fontWeight: FontWeight.w500, fontSize: 14), - validator: sendViewModel.templateValidator, + validator: sendTemplateViewModel.templateValidator, ), Padding( padding: EdgeInsets.only(top: 20), @@ -145,49 +146,49 @@ class SendTemplatePage extends BasePage { .primaryTextTheme .headline .decorationColor), - validator: sendViewModel.addressValidator, + validator: sendTemplateViewModel.addressValidator, ), ), - Observer(builder: (_) { - return Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - focusNode: _cryptoAmountFocus, - controller: _cryptoAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ ]')) - ], - prefixIcon: Padding( - padding: EdgeInsets.only(top: 9), - child: - Text(sendViewModel.currency.title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - ), - hintText: '0.0000', - borderColor: Theme.of(context) - .primaryTextTheme - .headline - .color, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headline - .decorationColor, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendViewModel.amountValidator)); - }), + Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + focusNode: _cryptoAmountFocus, + controller: _cryptoAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + BlacklistingTextInputFormatter( + RegExp('[\\-|\\ ]')) + ], + prefixIcon: Padding( + padding: EdgeInsets.only(top: 9), + child: + Text(sendTemplateViewModel + .currency.title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + )), + ), + hintText: '0.0000', + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: sendTemplateViewModel + .amountValidator)), Padding( padding: const EdgeInsets.only(top: 20), child: BaseTextFormField( @@ -201,7 +202,8 @@ class SendTemplatePage extends BasePage { ], prefixIcon: Padding( padding: EdgeInsets.only(top: 9), - child: Text(sendViewModel.fiat.title + ':', + child: Text(sendTemplateViewModel + .fiat.title + ':', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -236,12 +238,11 @@ class SendTemplatePage extends BasePage { bottomSection: PrimaryButton( onPressed: () { if (_formKey.currentState.validate()) { - sendViewModel.addTemplate( + sendTemplateViewModel.addTemplate( name: _nameController.text, address: _addressController.text, - cryptoCurrency: sendViewModel.currency.title, + cryptoCurrency: sendTemplateViewModel.currency.title, amount: _cryptoAmountController.text); - sendViewModel.updateTemplate(); Navigator.of(context).pop(); } }, @@ -258,19 +259,21 @@ class SendTemplatePage extends BasePage { return; } - reaction((_) => sendViewModel.fiatAmount, (String amount) { + final item = sendTemplateViewModel.sendItem; + + reaction((_) => item.fiatAmount, (String amount) { if (amount != _fiatAmountController.text) { _fiatAmountController.text = amount; } }); - reaction((_) => sendViewModel.cryptoAmount, (String amount) { + reaction((_) => item.cryptoAmount, (String amount) { if (amount != _cryptoAmountController.text) { _cryptoAmountController.text = amount; } }); - reaction((_) => sendViewModel.address, (String address) { + reaction((_) => item.address, (String address) { if (address != _addressController.text) { _addressController.text = address; } @@ -279,24 +282,24 @@ class SendTemplatePage extends BasePage { _cryptoAmountController.addListener(() { final amount = _cryptoAmountController.text; - if (amount != sendViewModel.cryptoAmount) { - sendViewModel.setCryptoAmount(amount); + if (amount != item.cryptoAmount) { + item.setCryptoAmount(amount); } }); _fiatAmountController.addListener(() { final amount = _fiatAmountController.text; - if (amount != sendViewModel.fiatAmount) { - sendViewModel.setFiatAmount(amount); + if (amount != item.fiatAmount) { + item.setFiatAmount(amount); } }); _addressController.addListener(() { final address = _addressController.text; - if (sendViewModel.address != address) { - sendViewModel.address = address; + if (item.address != address) { + item.address = address; } }); diff --git a/lib/src/screens/send/widgets/parse_address_from_domain_alert.dart b/lib/src/screens/send/widgets/parse_address_from_domain_alert.dart new file mode 100644 index 000000000..760091611 --- /dev/null +++ b/lib/src/screens/send/widgets/parse_address_from_domain_alert.dart @@ -0,0 +1,17 @@ +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +void showAddressAlert(BuildContext context, String title, String content) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + + return AlertWithOneAction( + alertTitle: title, + alertContent: content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); +} \ No newline at end of file diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart new file mode 100644 index 000000000..28212412e --- /dev/null +++ b/lib/src/screens/send/widgets/send_card.dart @@ -0,0 +1,521 @@ +import 'dart:ui'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/src/screens/send/widgets/parse_address_from_domain_alert.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/view_model/send/send_item.dart'; +import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; + +class SendCard extends StatefulWidget { + SendCard({Key key, @required this.item, @required this.sendViewModel}) : super(key: key); + + final SendItem item; + final SendViewModel sendViewModel; + + @override + SendCardState createState() => SendCardState( + item: item, + sendViewModel: sendViewModel + ); +} + +class SendCardState extends State + with AutomaticKeepAliveClientMixin { + SendCardState({@required this.item, @required this.sendViewModel}) + : addressController = TextEditingController(), + cryptoAmountController = TextEditingController(), + fiatAmountController = TextEditingController(), + noteController = TextEditingController(), + cryptoAmountFocus = FocusNode(), + fiatAmountFocus = FocusNode(), + addressFocusNode = FocusNode(); + + static const prefixIconWidth = 34.0; + static const prefixIconHeight = 34.0; + + final SendItem item; + final SendViewModel sendViewModel; + + final TextEditingController addressController; + final TextEditingController cryptoAmountController; + final TextEditingController fiatAmountController; + final TextEditingController noteController; + final FocusNode cryptoAmountFocus; + final FocusNode fiatAmountFocus; + final FocusNode addressFocusNode; + + bool _effectsInstalled = false; + + @override + Widget build(BuildContext context) { + super.build(context); + _setEffects(context); + + return KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2 + .backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + KeyboardActionsItem( + focusNode: fiatAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ]), + child: Container( + height: 445, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context) + .primaryTextTheme + .subhead + .decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(24, 80, 24, 32), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AddressTextField( + focusNode: addressFocusNode, + controller: addressController, + onURIScanned: (uri) { + var address = ''; + var amount = ''; + + if (uri != null) { + address = uri.path; + amount = uri.queryParameters['tx_amount'] ?? + uri.queryParameters['amount']; + } else { + address = uri.toString(); + } + + addressController.text = address; + cryptoAmountController.text = amount; + }, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook + ], + buttonColor: Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + validator: sendViewModel.addressValidator, + ), + Observer( + builder: (_) => Padding( + padding: const EdgeInsets.only(top: 20), + child: Stack( + children: [ + BaseTextFormField( + focusNode: cryptoAmountFocus, + controller: cryptoAmountController, + keyboardType: + TextInputType.numberWithOptions( + signed: false, decimal: true), + prefixIcon: Padding( + padding: EdgeInsets.only(top: 9), + child: Text( + sendViewModel.currency.title + + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + )), + ), + suffixIcon: SizedBox( + width: prefixIconWidth, + ), + hintText: '0.0000', + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: item.sendAll + ? sendViewModel.allAmountValidator + : sendViewModel + .amountValidator), + Positioned( + top: 2, + right: 0, + child: Container( + width: prefixIconWidth, + height: prefixIconHeight, + child: InkWell( + onTap: () async => + item.setSendAll(), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderRadius: + BorderRadius.all( + Radius.circular(6))), + child: Center( + child: Text( + S.of(context).all, + textAlign: + TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.bold, + color: + Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor))), + ))))]) + )), + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + S.of(context).available_balance + + ':', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + )), + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + ) + ], + ), + )), + Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + focusNode: fiatAmountFocus, + controller: fiatAmountController, + keyboardType: + TextInputType.numberWithOptions( + signed: false, decimal: true), + prefixIcon: Padding( + padding: EdgeInsets.only(top: 9), + child: + Text(sendViewModel.fiat.title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + )), + ), + hintText: '0.00', + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + )), + Padding( + padding: EdgeInsets.only(top: 20), + child: BaseTextFormField( + controller: noteController, + keyboardType: TextInputType.multiline, + maxLines: null, + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + hintText: S.of(context).note_optional, + placeholderTextStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + ), + ), + Observer( + builder: (_) => GestureDetector( + onTap: () => + _setTransactionPriority(context), + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S + .of(context) + .send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w500, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white)), + Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + item + .estimatedFee + .toString() + + ' ' + + sendViewModel + .currency.title, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: + Colors.white)), + Padding( + padding: + EdgeInsets.only(top: 5), + child: Text( + item + .estimatedFeeFiatAmount + + ' ' + + sendViewModel + .fiat.title, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + color: Theme + .of(context) + .primaryTextTheme + .headline + .decorationColor)) + ), + ], + ), + Padding( + padding: EdgeInsets.only( + top: 2, + left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ) + ], + ), + ) + ], + ), + ), + )) + ], + ) + ), + ), + )); + } + + void _setEffects(BuildContext context) { + addressController.text = item.address; + cryptoAmountController.text = item.cryptoAmount; + fiatAmountController.text = item.fiatAmount; + noteController.text = item.note; + + if (_effectsInstalled) { + return; + } + + cryptoAmountController.addListener(() { + final amount = cryptoAmountController.text; + + if (item.sendAll && amount != S.current.all) { + item.sendAll = false; + } + + if (amount != item.cryptoAmount) { + item.setCryptoAmount(amount); + } + }); + + fiatAmountController.addListener(() { + final amount = fiatAmountController.text; + + if (amount != item.fiatAmount) { + item.sendAll = false; + item.setFiatAmount(amount); + } + }); + + noteController.addListener(() { + final note = noteController.text ?? ''; + + if (note != item.note) { + item.note = note; + } + }); + + reaction((_) => item.sendAll, (bool all) { + if (all) { + cryptoAmountController.text = S.current.all; + fiatAmountController.text = null; + } + }); + + reaction((_) => item.fiatAmount, (String amount) { + if (amount != fiatAmountController.text) { + fiatAmountController.text = amount; + } + }); + + reaction((_) => item.cryptoAmount, (String amount) { + if (item.sendAll && amount != S.current.all) { + item.sendAll = false; + } + + if (amount != cryptoAmountController.text) { + cryptoAmountController.text = amount; + } + }); + + reaction((_) => item.address, (String address) { + if (address != addressController.text) { + addressController.text = address; + } + }); + + addressController.addListener(() { + final address = addressController.text; + + if (item.address != address) { + item.address = address; + } + }); + + reaction((_) => item.note, (String note) { + if (note != noteController.text) { + noteController.text = note; + } + }); + + addressFocusNode.addListener(() async { + if (!addressFocusNode.hasFocus && addressController.text.isNotEmpty) { + final record = await item.getOpenaliasRecord(); + + if (record != null) { + showAddressAlert( + context, + S.current.openalias_alert_title, + S.current.openalias_alert_content(record.name)); + } + } + }); + + _effectsInstalled = true; + } + + Future _setTransactionPriority(BuildContext context) async { + final items = priorityForWalletType(sendViewModel.walletType); + final selectedItem = items.indexOf(sendViewModel.transactionPriority); + + await showPopUp( + builder: (_) => Picker( + items: items, + displayItem: sendViewModel.displayFeeRate, + selectedAtIndex: selectedItem, + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (TransactionPriority priority) => + sendViewModel.setTransactionPriority(priority), + ), + context: context); + } + + @override + bool get wantKeepAlive => true; +} \ No newline at end of file diff --git a/lib/themes/bright_theme.dart b/lib/themes/bright_theme.dart index 73c33aba3..5adb5d2f1 100644 --- a/lib/themes/bright_theme.dart +++ b/lib/themes/bright_theme.dart @@ -105,10 +105,12 @@ class BrightTheme extends ThemeBase { ), display2: TextStyle( color: Colors.white.withOpacity(0.5), // estimated fee (send page) + backgroundColor: PaletteDark.darkCyanBlue.withOpacity(0.67), // dot color for indicator on send page decorationColor: Palette.shadowWhite // template dotted border (send page) ), display3: TextStyle( color: Palette.darkBlueCraiola, // template new text (send page) + backgroundColor: PaletteDark.darkNightBlue, // active dot color for indicator on send page decorationColor: Palette.shadowWhite // template background color (send page) ), display4: TextStyle( diff --git a/lib/themes/dark_theme.dart b/lib/themes/dark_theme.dart index 428100eba..646afc964 100644 --- a/lib/themes/dark_theme.dart +++ b/lib/themes/dark_theme.dart @@ -104,10 +104,12 @@ class DarkTheme extends ThemeBase { ), display2: TextStyle( color: Colors.white, // estimated fee (send page) + backgroundColor: PaletteDark.cyanBlue, // dot color for indicator on send page decorationColor: PaletteDark.darkCyanBlue // template dotted border (send page) ), display3: TextStyle( color: PaletteDark.darkCyanBlue, // template new text (send page) + backgroundColor: Colors.white, // active dot color for indicator on send page decorationColor: PaletteDark.darkVioletBlue // template background color (send page) ), display4: TextStyle( diff --git a/lib/themes/light_theme.dart b/lib/themes/light_theme.dart index fa12a90b6..763872efe 100644 --- a/lib/themes/light_theme.dart +++ b/lib/themes/light_theme.dart @@ -105,10 +105,12 @@ class LightTheme extends ThemeBase { ), display2: TextStyle( color: Colors.white.withOpacity(0.5), // estimated fee (send page) + backgroundColor: PaletteDark.darkCyanBlue.withOpacity(0.67), // dot color for indicator on send page decorationColor: Palette.moderateLavender // template dotted border (send page) ), display3: TextStyle( color: Palette.darkBlueCraiola, // template new text (send page) + backgroundColor: PaletteDark.darkNightBlue, // active dot color for indicator on send page decorationColor: Palette.blueAlice // template background color (send page) ), display4: TextStyle( diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 7882c3dca..eeb9bfd26 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -79,8 +79,11 @@ abstract class ExchangeTradeViewModelBase with Store { return; } - sendViewModel.address = trade.inputAddress; - sendViewModel.setCryptoAmount(trade.amount); + sendViewModel.clearSendItemList(); + final item = sendViewModel.sendItemList.first; + + item.address = trade.inputAddress; + item.setCryptoAmount(trade.amount); await sendViewModel.createTransaction(); } diff --git a/lib/view_model/send/send_item.dart b/lib/view_model/send/send_item.dart new file mode 100644 index 000000000..aa5d93b02 --- /dev/null +++ b/lib/view_model/send/send_item.dart @@ -0,0 +1,198 @@ +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; +import 'package:cake_wallet/entities/openalias_record.dart'; +import 'package:cake_wallet/monero/monero_amount_format.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +part 'send_item.g.dart'; + +const String cryptoNumberPattern = '0.0'; + +class SendItem = SendItemBase with _$SendItem; + +abstract class SendItemBase with Store { + SendItemBase(this._wallet, this._settingsStore, this._fiatConversationStore) + :_cryptoNumberFormat = NumberFormat(cryptoNumberPattern) { + reset(); + _setCryptoNumMaximumFractionDigits(); + key = UniqueKey(); + } + + Key key; + + @observable + String fiatAmount; + + @observable + String cryptoAmount; + + @observable + String address; + + @observable + String note; + + @observable + bool sendAll; + + @computed + double get estimatedFee { + int amount; + + try { + if (cryptoAmount?.isNotEmpty ?? false) { + final _cryptoAmount = cryptoAmount.replaceAll(',', '.'); + int _amount = 0; + switch (walletType) { + case WalletType.monero: + _amount = moneroParseAmount(amount: _cryptoAmount); + break; + case WalletType.bitcoin: + _amount = stringDoubleToBitcoinAmount(_cryptoAmount); + break; + case WalletType.litecoin: + _amount = stringDoubleToBitcoinAmount(_cryptoAmount); + break; + default: + break; + } + + if (_amount > 0) { + amount = _amount; + } + } + + final fee = _wallet.calculateEstimatedFee( + _settingsStore.priority[_wallet.type], amount); + + if (_wallet is ElectrumWallet) { + return bitcoinAmountToDouble(amount: fee); + } + + if (_wallet is MoneroWallet) { + return moneroAmountToDouble(amount: fee); + } + } catch (e) { + print(e.toString()); + } + + return 0; + } + + @computed + String get estimatedFeeFiatAmount { + try { + final fiat = calculateFiatAmountRaw( + price: _fiatConversationStore.prices[_wallet.currency], + cryptoAmount: estimatedFee); + return fiat; + } catch (_) { + return '0.00'; + } + } + + WalletType get walletType => _wallet.type; + final WalletBase _wallet; + final SettingsStore _settingsStore; + final FiatConversionStore _fiatConversationStore; + final NumberFormat _cryptoNumberFormat; + + @action + void setSendAll() => sendAll = true; + + @action + void reset() { + sendAll = false; + cryptoAmount = ''; + fiatAmount = ''; + address = ''; + note = ''; + } + + @action + void setCryptoAmount(String amount) { + if (amount.toUpperCase() != S.current.all) { + sendAll = false; + } + + cryptoAmount = amount; + _updateFiatAmount(); + } + + @action + void setFiatAmount(String amount) { + fiatAmount = amount; + _updateCryptoAmount(); + } + + @action + void _updateFiatAmount() { + try { + final fiat = calculateFiatAmount( + price: _fiatConversationStore.prices[_wallet.currency], + cryptoAmount: cryptoAmount.replaceAll(',', '.')); + if (fiatAmount != fiat) { + fiatAmount = fiat; + } + } catch (_) { + fiatAmount = ''; + } + } + + @action + void _updateCryptoAmount() { + try { + final crypto = double.parse(fiatAmount.replaceAll(',', '.')) / + _fiatConversationStore.prices[_wallet.currency]; + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); + + if (cryptoAmount != cryptoAmountTmp) { + cryptoAmount = cryptoAmountTmp; + } + } catch (e) { + cryptoAmount = ''; + } + } + + void _setCryptoNumMaximumFractionDigits() { + var maximumFractionDigits = 0; + + switch (_wallet.type) { + case WalletType.monero: + maximumFractionDigits = 12; + break; + case WalletType.bitcoin: + maximumFractionDigits = 8; + break; + case WalletType.litecoin: + maximumFractionDigits = 8; + break; + default: + break; + } + + _cryptoNumberFormat.maximumFractionDigits = maximumFractionDigits; + } + + Future getOpenaliasRecord() async { + final formattedName = OpenaliasRecord.formatDomainName(address); + final record = await OpenaliasRecord.fetchAddressAndName(formattedName); + + if (record == null || record.address.contains(formattedName)) { + return null; + } + + address = record.address; + return record; + } +} \ No newline at end of file diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart new file mode 100644 index 000000000..37848a6ec --- /dev/null +++ b/lib/view_model/send/send_template_view_model.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/view_model/send/send_item.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/template.dart'; +import 'package:cake_wallet/store/templates/send_template_store.dart'; +import 'package:cake_wallet/core/template_validator.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/crypto_currency.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; + +part 'send_template_view_model.g.dart'; + +class SendTemplateViewModel = SendTemplateViewModelBase + with _$SendTemplateViewModel; + +abstract class SendTemplateViewModelBase with Store { + SendTemplateViewModelBase(this._wallet, this._settingsStore, + this._sendTemplateStore, this._fiatConversationStore) { + + sendItem = SendItem(_wallet, _settingsStore, _fiatConversationStore); + } + + SendItem sendItem; + + Validator get amountValidator => AmountValidator(type: _wallet.type); + + Validator get addressValidator => AddressValidator(type: _wallet.currency); + + Validator get templateValidator => TemplateValidator(); + + CryptoCurrency get currency => _wallet.currency; + + FiatCurrency get fiat => _settingsStore.fiatCurrency; + + @computed + ObservableList