From 023336d46026d88db5add907797eba02038eef66 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Tue, 22 Dec 2020 20:42:30 +0200 Subject: [PATCH] CAKE-198 | applied ability to make a note with some details about transaction; added transactionNote property and note getter to transaction_description.dart; added multiline textfield for notes (send_page.dart); added transaction_details_list_item.dart, textfield_list_item.dart, textfield_list_row.dart, transaction_details_view_model.dart to the app --- lib/di.dart | 13 +- lib/entities/transaction_description.dart | 7 +- lib/src/screens/send/send_page.dart | 40 ++++++ .../standart_list_item.dart | 8 +- .../textfield_list_item.dart | 8 ++ .../transaction_details_list_item.dart | 6 + .../transaction_details_page.dart | 119 +++++------------- .../widgets/textfield_list_row.dart | 91 ++++++++++++++ lib/src/widgets/base_text_form_field.dart | 2 +- lib/view_model/send/send_view_model.dart | 8 +- .../transaction_details_view_model.dart | 105 ++++++++++++++++ 11 files changed, 312 insertions(+), 95 deletions(-) create mode 100644 lib/src/screens/transaction_details/textfield_list_item.dart create mode 100644 lib/src/screens/transaction_details/transaction_details_list_item.dart create mode 100644 lib/src/screens/transaction_details/widgets/textfield_list_row.dart create mode 100644 lib/view_model/transaction_details_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index 12828300d..e08748171 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -55,6 +55,7 @@ import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; +import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -415,11 +416,17 @@ Future setup( getIt.registerFactoryParam((type, _) => WalletRestorePage(getIt.get(param1: type))); + getIt.registerFactoryParam + ((TransactionInfo transactionInfo, _) => TransactionDetailsViewModel( + transactionInfo: transactionInfo, + transactionDescriptionBox: transactionDescriptionBox, + settingsStore: getIt.get() + )); + getIt.registerFactoryParam( (TransactionInfo transactionInfo, _) => TransactionDetailsPage( - transactionInfo, - getIt.get().shouldSaveRecipientAddress, - transactionDescriptionBox)); + transactionDetailsViewModel: getIt + .get(param1: transactionInfo))); getIt.registerFactoryParam( diff --git a/lib/entities/transaction_description.dart b/lib/entities/transaction_description.dart index 3f817fe4f..65f9d4263 100644 --- a/lib/entities/transaction_description.dart +++ b/lib/entities/transaction_description.dart @@ -4,7 +4,7 @@ part 'transaction_description.g.dart'; @HiveType(typeId: 2) class TransactionDescription extends HiveObject { - TransactionDescription({this.id, this.recipientAddress}); + TransactionDescription({this.id, this.recipientAddress, this.transactionNote}); static const boxName = 'TransactionDescriptions'; static const boxKey = 'transactionDescriptionsBoxKey'; @@ -14,4 +14,9 @@ class TransactionDescription extends HiveObject { @HiveField(1) String recipientAddress; + + @HiveField(2) + String transactionNote; + + String get note => transactionNote ?? ''; } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 1a18b5ba6..db943148d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -31,6 +31,7 @@ class SendPage extends BasePage { : _addressController = TextEditingController(), _cryptoAmountController = TextEditingController(), _fiatAmountController = TextEditingController(), + _noteController = TextEditingController(), _formKey = GlobalKey(), _cryptoAmountFocus = FocusNode(), _fiatAmountFocus = FocusNode(), @@ -46,6 +47,7 @@ class SendPage extends BasePage { final TextEditingController _addressController; final TextEditingController _cryptoAmountController; final TextEditingController _fiatAmountController; + final TextEditingController _noteController; final GlobalKey _formKey; final FocusNode _cryptoAmountFocus; final FocusNode _fiatAmountFocus; @@ -304,6 +306,30 @@ class SendPage extends BasePage { 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: 'Note (optional)', + placeholderTextStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + ), + ), Observer( builder: (_) => GestureDetector( onTap: () => @@ -534,6 +560,14 @@ class SendPage extends BasePage { } }); + _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; @@ -571,6 +605,12 @@ class SendPage extends BasePage { } }); + reaction((_) => sendViewModel.note, (String note) { + if (note != _noteController.text) { + _noteController.text = note; + } + }); + reaction((_) => sendViewModel.state, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/screens/transaction_details/standart_list_item.dart b/lib/src/screens/transaction_details/standart_list_item.dart index 9cf23eeb5..728ec5cc0 100644 --- a/lib/src/screens/transaction_details/standart_list_item.dart +++ b/lib/src/screens/transaction_details/standart_list_item.dart @@ -1,6 +1,6 @@ -class StandartListItem { - StandartListItem({this.title, this.value}); +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; - final String title; - final String value; +class StandartListItem extends TransactionDetailsListItem { + StandartListItem({String title, String value}) + : super(title: title, value: value); } diff --git a/lib/src/screens/transaction_details/textfield_list_item.dart b/lib/src/screens/transaction_details/textfield_list_item.dart new file mode 100644 index 000000000..2bc6010c0 --- /dev/null +++ b/lib/src/screens/transaction_details/textfield_list_item.dart @@ -0,0 +1,8 @@ +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; + +class TextFieldListItem extends TransactionDetailsListItem { + TextFieldListItem({String title, String value, this.onSubmitted}) + : super(title: title, value: value); + + final Function(String value) onSubmitted; +} \ No newline at end of file diff --git a/lib/src/screens/transaction_details/transaction_details_list_item.dart b/lib/src/screens/transaction_details/transaction_details_list_item.dart new file mode 100644 index 000000000..3f666ea34 --- /dev/null +++ b/lib/src/screens/transaction_details/transaction_details_list_item.dart @@ -0,0 +1,6 @@ +abstract class TransactionDetailsListItem { + TransactionDetailsListItem({this.title, this.value}); + + final String title; + final String value; +} \ No newline at end of file diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 3a7c7e591..a0e1b3c7a 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -1,87 +1,21 @@ -import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; -import 'package:cake_wallet/monero/monero_transaction_info.dart'; -import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/src/widgets/standart_list_row.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/utils/date_formatter.dart'; -import 'package:hive/hive.dart'; class TransactionDetailsPage extends BasePage { - TransactionDetailsPage(this.transactionInfo, bool showRecipientAddress, - Box transactionDescriptionBox) - : _items = [] { - final dateFormat = DateFormatter.withCurrentLocal(); - final tx = transactionInfo; - - if (tx is MoneroTransactionInfo) { - final items = [ - StandartListItem( - title: S.current.transaction_details_transaction_id, value: tx.id), - StandartListItem( - title: S.current.transaction_details_date, - value: dateFormat.format(tx.date)), - StandartListItem( - title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem( - title: S.current.transaction_details_amount, - value: tx.amountFormatted()), - StandartListItem(title: S.current.send_fee, value: tx.feeFormatted()) - ]; - - if (tx.key?.isNotEmpty ?? null) { - // FIXME: add translation - items.add(StandartListItem(title: 'Transaction Key', value: tx.key)); - } - - _items.addAll(items); - } - - if (tx is BitcoinTransactionInfo) { - final items = [ - StandartListItem( - title: S.current.transaction_details_transaction_id, value: tx.id), - StandartListItem( - title: S.current.transaction_details_date, - value: dateFormat.format(tx.date)), - StandartListItem( - title: 'Confirmations', value: tx.confirmations?.toString()), - StandartListItem( - title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem( - title: S.current.transaction_details_amount, - value: tx.amountFormatted()), - if (tx.feeFormatted()?.isNotEmpty) - StandartListItem(title: S.current.send_fee, value: tx.feeFormatted()) - ]; - - _items.addAll(items); - } - - if (showRecipientAddress) { - final recipientAddress = transactionDescriptionBox.values - .firstWhere((val) => val.id == transactionInfo.id, orElse: () => null) - ?.recipientAddress; - - if (recipientAddress?.isNotEmpty ?? false) { - _items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: recipientAddress)); - } - } - } + TransactionDetailsPage({this.transactionDetailsViewModel}); @override String get title => S.current.transaction_details_title; - final TransactionInfo transactionInfo; - - final List _items; + final TransactionDetailsViewModel transactionDetailsViewModel; @override Widget body(BuildContext context) { @@ -97,22 +31,37 @@ class TransactionDetailsPage extends BasePage { Theme.of(context).primaryTextTheme.title.backgroundColor, ), ), - itemCount: _items.length, + itemCount: transactionDetailsViewModel.items.length, itemBuilder: (context, index) { - final item = _items[index]; - final isDrawBottom = index == _items.length - 1 ? true : false; + final item = transactionDetailsViewModel.items[index]; + final isDrawBottom = + index == transactionDetailsViewModel.items.length - 1 + ? true : false; - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: item.value)); - showBar(context, - S.of(context).transaction_details_copied(item.title)); - }, - child: StandartListRow( - title: '${item.title}:', - value: item.value, - isDrawBottom: isDrawBottom), - ); + if (item is StandartListItem) { + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: item.value)); + showBar(context, + S.of(context).transaction_details_copied(item.title)); + }, + child: StandartListRow( + title: '${item.title}:', + value: item.value, + isDrawBottom: isDrawBottom), + ); + } + + if (item is TextFieldListItem) { + return TextFieldListRow( + title: item.title, + value: item.value, + onSubmitted: item.onSubmitted, + isDrawBottom: isDrawBottom, + ); + } + + return null; }), ); } diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart new file mode 100644 index 000000000..3f2f2d877 --- /dev/null +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -0,0 +1,91 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class TextFieldListRow extends StatelessWidget { + TextFieldListRow( + {this.title, + this.value, + this.titleFontSize = 14, + this.valueFontSize = 16, + this.onSubmitted, + this.isDrawBottom = false}) { + + _textController = TextEditingController(); + _textController.text = value; + } + + final String title; + final String value; + final double titleFontSize; + final double valueFontSize; + final Function(String value) onSubmitted; + final bool isDrawBottom; + + TextEditingController _textController; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: double.infinity, + color: Theme.of(context).backgroundColor, + child: Padding( + padding: + const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: titleFontSize, + fontWeight: FontWeight.w500, + color: + Theme.of(context).primaryTextTheme.overline.color), + textAlign: TextAlign.left), + TextField( + controller: _textController, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.done, + maxLines: null, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.only(top: 12, bottom: 0), + hintText: 'Note', + hintStyle: TextStyle( + fontSize: valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + border: InputBorder.none + ), + onSubmitted: (value) => onSubmitted.call(value), + ) + ]), + ), + ), + isDrawBottom + ? Container( + height: 1, + padding: EdgeInsets.only(left: 24), + color: Theme.of(context).backgroundColor, + child: Container( + height: 1, + color: Theme.of(context).primaryTextTheme.title.backgroundColor, + ), + ) + : Offstage(), + ], + ); + } +} diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 2c296df25..b9148bf2c 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -51,7 +51,7 @@ class BaseTextFormField extends StatelessWidget { final FocusNode focusNode; final bool readOnly; final bool enableInteractiveSelection; - String initialValue; + final String initialValue; @override Widget build(BuildContext context) { diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index f3d46ccb3..c56230c59 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -37,6 +37,7 @@ abstract class SendViewModelBase with Store { this._fiatConversationStore, this.transactionDescriptionBox) : state = InitialExecutionState(), _cryptoNumberFormat = NumberFormat(), + note = '', sendAll = false { _setCryptoNumMaximumFractionDigits(); } @@ -53,6 +54,9 @@ abstract class SendViewModelBase with Store { @observable String address; + @observable + String note; + @observable bool sendAll; @@ -105,6 +109,7 @@ abstract class SendViewModelBase with Store { cryptoAmount = ''; fiatAmount = ''; address = ''; + note = ''; } @action @@ -127,7 +132,8 @@ abstract class SendViewModelBase with Store { if (_settingsStore.shouldSaveRecipientAddress && (pendingTransaction.id?.isNotEmpty ?? false)) { await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction.id, recipientAddress: address)); + id: pendingTransaction.id, recipientAddress: address, + transactionNote: note)); } state = TransactionCommitted(); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart new file mode 100644 index 000000000..e3f57c9a3 --- /dev/null +++ b/lib/view_model/transaction_details_view_model.dart @@ -0,0 +1,105 @@ +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/monero/monero_transaction_info.dart'; +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +part 'transaction_details_view_model.g.dart'; + +class TransactionDetailsViewModel = TransactionDetailsViewModelBase with _$TransactionDetailsViewModel; + +abstract class TransactionDetailsViewModelBase with Store { + TransactionDetailsViewModelBase({ + this.transactionInfo, + this.transactionDescriptionBox, + this.settingsStore}) : items = [] { + + showRecipientAddress = settingsStore?.shouldSaveRecipientAddress ?? false; + + final dateFormat = DateFormatter.withCurrentLocal(); + final tx = transactionInfo; + + if (tx is MoneroTransactionInfo) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date)), + StandartListItem( + title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted()), + StandartListItem(title: S.current.send_fee, value: tx.feeFormatted()), + ]; + + if (tx.key?.isNotEmpty ?? null) { + // FIXME: add translation + _items.add(StandartListItem(title: 'Transaction Key', value: tx.key)); + } + + items.addAll(_items); + } + + if (tx is BitcoinTransactionInfo) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date)), + // FIXME: add translation + StandartListItem( + title: 'Confirmations', value: tx.confirmations?.toString()), + StandartListItem( + title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty) + StandartListItem(title: S.current.send_fee, value: tx.feeFormatted()) + ]; + + items.addAll(_items); + } + + if (showRecipientAddress) { + final recipientAddress = transactionDescriptionBox.values + .firstWhere((val) => val.id == transactionInfo.id, orElse: () => null) + ?.recipientAddress; + + if (recipientAddress?.isNotEmpty ?? false) { + items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: recipientAddress)); + } + } + + final description = transactionDescriptionBox.values.firstWhere( + (val) => val.id == transactionInfo.id, orElse: () => null); + + if (description != null) { + // FIXME: add translation + items.add(TextFieldListItem(title: 'Note (tap to change)', + value: description.note, onSubmitted: (value) { + description.transactionNote = value; + description.save(); + })); + } + } + + final TransactionInfo transactionInfo; + final Box transactionDescriptionBox; + final SettingsStore settingsStore; + + final List items; + bool showRecipientAddress; +} \ No newline at end of file