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

This commit is contained in:
OleksandrSobol 2020-12-22 20:42:30 +02:00
parent 782de5d1d8
commit 023336d460
11 changed files with 312 additions and 95 deletions

View file

@ -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<WalletRestorePage, WalletType, void>((type, _) =>
WalletRestorePage(getIt.get<WalletRestoreViewModel>(param1: type)));
getIt.registerFactoryParam<TransactionDetailsViewModel, TransactionInfo, void>
((TransactionInfo transactionInfo, _) => TransactionDetailsViewModel(
transactionInfo: transactionInfo,
transactionDescriptionBox: transactionDescriptionBox,
settingsStore: getIt.get<SettingsStore>()
));
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
transactionInfo,
getIt.get<SettingsStore>().shouldSaveRecipientAddress,
transactionDescriptionBox));
transactionDetailsViewModel: getIt
.get<TransactionDetailsViewModel>(param1: transactionInfo)));
getIt.registerFactoryParam<NewWalletTypePage,
void Function(BuildContext, WalletType), bool>(

View file

@ -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 ?? '';
}

View file

@ -31,6 +31,7 @@ class SendPage extends BasePage {
: _addressController = TextEditingController(),
_cryptoAmountController = TextEditingController(),
_fiatAmountController = TextEditingController(),
_noteController = TextEditingController(),
_formKey = GlobalKey<FormState>(),
_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<FormState> _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((_) {

View file

@ -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);
}

View file

@ -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;
}

View file

@ -0,0 +1,6 @@
abstract class TransactionDetailsListItem {
TransactionDetailsListItem({this.title, this.value});
final String title;
final String value;
}

View file

@ -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<TransactionDescription> 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<StandartListItem> _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<void>(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<void>(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;
}),
);
}

View file

@ -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: <Widget>[
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: <Widget>[
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(),
],
);
}
}

View file

@ -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) {

View file

@ -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();

View file

@ -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<TransactionDescription> transactionDescriptionBox;
final SettingsStore settingsStore;
final List<TransactionDetailsListItem> items;
bool showRecipientAddress;
}