Cw 433 support send templates with multiple recipients (#995)

* feat: Support Send templates with multiple recipients

* feat: use only first name for template display, and sum total amount

* fix: amounts being wiped

* feat: make send template card buttons function like send card

* feat: replace amount -> name for template name

* fix: template name
This commit is contained in:
Rafael Saes 2023-08-01 19:19:04 -03:00 committed by GitHub
parent c68baf7244
commit fcf4fbdc14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 716 additions and 439 deletions

View file

@ -4,14 +4,15 @@ part 'template.g.dart';
@HiveType(typeId: Template.typeId) @HiveType(typeId: Template.typeId)
class Template extends HiveObject { class Template extends HiveObject {
Template({ Template(
required this.nameRaw, {required this.nameRaw,
required this.isCurrencySelectedRaw, required this.isCurrencySelectedRaw,
required this.addressRaw, required this.addressRaw,
required this.cryptoCurrencyRaw, required this.cryptoCurrencyRaw,
required this.amountRaw, required this.amountRaw,
required this.fiatCurrencyRaw, required this.fiatCurrencyRaw,
required this.amountFiatRaw}); required this.amountFiatRaw,
this.additionalRecipientsRaw});
static const typeId = 6; static const typeId = 6;
static const boxName = 'Template'; static const boxName = 'Template';
@ -37,6 +38,9 @@ class Template extends HiveObject {
@HiveField(6) @HiveField(6)
String? amountFiatRaw; String? amountFiatRaw;
@HiveField(7)
List<Template>? additionalRecipientsRaw;
bool get isCurrencySelected => isCurrencySelectedRaw ?? false; bool get isCurrencySelected => isCurrencySelectedRaw ?? false;
String get fiatCurrency => fiatCurrencyRaw ?? ''; String get fiatCurrency => fiatCurrencyRaw ?? '';
@ -50,5 +54,6 @@ class Template extends HiveObject {
String get cryptoCurrency => cryptoCurrencyRaw ?? ''; String get cryptoCurrency => cryptoCurrencyRaw ?? '';
String get amount => amountRaw ?? ''; String get amount => amountRaw ?? '';
}
List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
}

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart';
import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart';
@ -241,6 +242,11 @@ class SendPage extends BasePage {
return TemplateTile( return TemplateTile(
key: UniqueKey(), key: UniqueKey(),
to: template.name, to: template.name,
hasMultipleRecipients:
template.additionalRecipients !=
null &&
template.additionalRecipients!
.length > 1,
amount: template.isCurrencySelected amount: template.isCurrencySelected
? template.amount ? template.amount
: template.amountFiat, : template.amountFiat,
@ -248,25 +254,36 @@ class SendPage extends BasePage {
? template.cryptoCurrency ? template.cryptoCurrency
: template.fiatCurrency, : template.fiatCurrency,
onTap: () async { onTap: () async {
final fiatFromTemplate = FiatCurrency if (template.additionalRecipients !=
.all null) {
.singleWhere((element) => sendViewModel.clearOutputs();
element.title ==
template.fiatCurrency); template.additionalRecipients!
final output = _defineCurrentOutput(); .forEach((currentElement) async {
output.address = template.address; int i = template
if (template.isCurrencySelected) { .additionalRecipients!
output .indexOf(currentElement);
.setCryptoAmount(template.amount);
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
await _setInputsFromTemplate(
context,
output: output,
template: currentElement);
});
} else { } else {
sendViewModel.setFiatCurrency( final output = _defineCurrentOutput();
fiatFromTemplate); await _setInputsFromTemplate(
output.setFiatAmount( context,
template.amountFiat); output: output,
template: template);
} }
output.resetParsedAddress();
await output
.fetchParsedAddress(context);
}, },
onRemove: () { onRemove: () {
showPopUp<void>( showPopUp<void>(
@ -477,6 +494,24 @@ class SendPage extends BasePage {
_effectsInstalled = true; _effectsInstalled = true;
} }
Future<void> _setInputsFromTemplate(BuildContext context,
{required Output output, required Template template}) async {
final fiatFromTemplate = FiatCurrency.all
.singleWhere((element) => element.title == template.fiatCurrency);
output.address = template.address;
if (template.isCurrencySelected) {
output.setCryptoAmount(template.amount);
} else {
sendViewModel.setFiatCurrency(fiatFromTemplate);
output.setFiatAmount(template.amountFiat);
}
output.resetParsedAddress();
await output.fetchParsedAddress(context);
}
Output _defineCurrentOutput() { Output _defineCurrentOutput() {
if (controller.page == null) { if (controller.page == null) {
throw Exception('Controller page is null'); throw Exception('Controller page is null');

View file

@ -1,35 +1,21 @@
import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/send/send_template_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';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_template_card.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
class SendTemplatePage extends BasePage { class SendTemplatePage extends BasePage {
SendTemplatePage({required this.sendTemplateViewModel}) { SendTemplatePage({required this.sendTemplateViewModel});
sendTemplateViewModel.output.reset();
}
final SendTemplateViewModel sendTemplateViewModel; final SendTemplateViewModel sendTemplateViewModel;
final _addressController = TextEditingController();
final _cryptoAmountController = TextEditingController();
final _fiatAmountController = TextEditingController();
final _nameController = TextEditingController();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final FocusNode _cryptoAmountFocus = FocusNode(); final controller = PageController(initialPage: 0);
final FocusNode _fiatAmountFocus = FocusNode();
bool _effectsInstalled = false;
@override @override
String get title => S.current.exchange_new_template; String get title => S.current.exchange_new_template;
@ -44,273 +30,146 @@ class SendTemplatePage extends BasePage {
AppBarStyle get appBarStyle => AppBarStyle.transparent; AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override @override
Widget body(BuildContext context) { Widget trailing(context) => Observer(builder: (_) {
_setEffects(context); return sendTemplateViewModel.recipients.length > 1
? TrailButton(
caption: S.of(context).remove,
onPressed: () {
int pageToJump = (controller.page?.round() ?? 0) - 1;
pageToJump = pageToJump > 0 ? pageToJump : 0;
final recipient = _defineCurrentRecipient();
sendTemplateViewModel.removeRecipient(recipient);
controller.jumpToPage(pageToJump);
})
: TrailButton(
caption: S.of(context).clear,
onPressed: () {
final recipient = _defineCurrentRecipient();
_formKey.currentState?.reset();
recipient.reset();
});
});
return KeyboardActions( @override
config: KeyboardActionsConfig( Widget body(BuildContext context) {
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, return Form(
keyboardBarColor: Theme.of(context) key: _formKey,
.accentTextTheme! child: ScrollableWithBottomSection(
.bodyLarge!
.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
KeyboardActionsItem(
focusNode: _fiatAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
)
]),
child: Container(
height: 0,
color: Theme.of(context).colorScheme.background,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24), contentPadding: EdgeInsets.only(bottom: 24),
content: Container( content: FocusTraversalGroup(
decoration: BoxDecoration( policy: OrderedTraversalPolicy(),
borderRadius: BorderRadius.only( child: Column(children: [
bottomLeft: Radius.circular(24), Container(
bottomRight: Radius.circular(24), height: 460,
), child: Observer(builder: (_) {
gradient: LinearGradient(colors: [ return PageView.builder(
Theme.of(context).primaryTextTheme!.titleMedium!.color!, scrollDirection: Axis.horizontal,
Theme.of(context) controller: controller,
.primaryTextTheme! itemCount: sendTemplateViewModel.recipients.length,
.titleMedium! itemBuilder: (_, index) {
.decorationColor!, final template =
], begin: Alignment.topLeft, end: Alignment.bottomRight), sendTemplateViewModel.recipients[index];
), return SendTemplateCard(
child: Form( template: template,
key: _formKey, index: index,
child: Column( sendTemplateViewModel: sendTemplateViewModel);
children: <Widget>[ });
Padding( })),
padding: EdgeInsets.fromLTRB(24, 90, 24, 32), Padding(
child: Column( padding: EdgeInsets.only(
children: <Widget>[ top: 10, left: 24, right: 24, bottom: 10),
BaseTextFormField( child: Container(
controller: _nameController, height: 10,
hintText: S.of(context).send_name, child: Observer(
borderColor: Theme.of(context) builder: (_) {
.primaryTextTheme! final count = sendTemplateViewModel.recipients.length;
.headlineSmall!
.color!, return count > 1
textStyle: TextStyle( ? SmoothPageIndicator(
fontSize: 14, controller: controller,
fontWeight: FontWeight.w500, count: count,
color: Colors.white), effect: ScrollingDotsEffect(
placeholderTextStyle: TextStyle( spacing: 6.0,
color: Theme.of(context) radius: 6.0,
.primaryTextTheme! dotWidth: 6.0,
.headlineSmall! dotHeight: 6.0,
.decorationColor!, dotColor: Theme.of(context)
fontWeight: FontWeight.w500, .primaryTextTheme
fontSize: 14), .displaySmall!
validator: sendTemplateViewModel.templateValidator, .backgroundColor!,
), activeDotColor: Theme.of(context)
Padding( .primaryTextTheme
padding: EdgeInsets.only(top: 20), .displayMedium!
child: AddressTextField( .backgroundColor!))
controller: _addressController, : Offstage();
onURIScanned: (uri) { },
final paymentRequest = PaymentRequest.fromUri(uri);
_addressController.text = paymentRequest.address;
_cryptoAmountController.text = paymentRequest.amount;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor: Theme.of(context)
.primaryTextTheme!
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!),
),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
sendTemplateViewModel.selectCurrency();
}
},
child: BaseTextFormField(
focusNode: _cryptoAmountFocus,
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.currency.title,
isSelected:
sendTemplateViewModel
.isCurrencySelected,
)),
hintText: '0.0000',
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator:
sendTemplateViewModel.amountValidator))),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
sendTemplateViewModel.selectFiat();
}
},
child: BaseTextFormField(
focusNode: _fiatAmountFocus,
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.fiat.title,
isSelected: sendTemplateViewModel
.isFiatSelected,
)),
hintText: '0.00',
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
))),
],
), ),
) ),
], ),
), ])),
),
),
bottomSectionPadding: bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24), EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: PrimaryButton( bottomSection: Column(children: [
onPressed: () { // if (sendViewModel.hasMultiRecipient)
if (_formKey.currentState != null && _formKey.currentState!.validate()) { Padding(
sendTemplateViewModel.addTemplate( padding: EdgeInsets.only(bottom: 12),
isCurrencySelected: sendTemplateViewModel.isCurrencySelected, child: PrimaryButton(
name: _nameController.text, onPressed: () {
address: _addressController.text, sendTemplateViewModel.addRecipient();
cryptoCurrency:sendTemplateViewModel.currency.title, Future.delayed(const Duration(milliseconds: 250), () {
fiatCurrency: sendTemplateViewModel.fiat.title, controller.jumpToPage(
amount: _cryptoAmountController.text, sendTemplateViewModel.recipients.length - 1);
amountFiat: _fiatAmountController.text); });
Navigator.of(context).pop(); },
} text: S.of(context).add_receiver,
}, color: Colors.transparent,
text: S.of(context).save, textColor: Theme.of(context)
color: Colors.green, .accentTextTheme
textColor: Colors.white, .displaySmall!
), .decorationColor!,
), isDottedBorder: true,
)); borderColor: Theme.of(context)
.primaryTextTheme
.displaySmall!
.decorationColor!)),
PrimaryButton(
onPressed: () {
if (_formKey.currentState != null &&
_formKey.currentState!.validate()) {
final mainTemplate = sendTemplateViewModel.recipients[0];
print(sendTemplateViewModel.recipients.map((element) =>
element.toTemplate(
cryptoCurrency:
sendTemplateViewModel.cryptoCurrency.title,
fiatCurrency:
sendTemplateViewModel.fiatCurrency)));
sendTemplateViewModel.addTemplate(
isCurrencySelected: mainTemplate.isCurrencySelected,
name: mainTemplate.name,
address: mainTemplate.address,
amount: mainTemplate.output.cryptoAmount,
amountFiat: mainTemplate.output.fiatAmount,
additionalRecipients: sendTemplateViewModel.recipients
.map((element) => element.toTemplate(
cryptoCurrency: sendTemplateViewModel
.cryptoCurrency.title,
fiatCurrency:
sendTemplateViewModel.fiatCurrency))
.toList());
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Colors.green,
textColor: Colors.white)
])));
} }
void _setEffects(BuildContext context) { TemplateViewModel _defineCurrentRecipient() {
if (_effectsInstalled) { if (controller.page == null) {
return; throw Exception('Controller page is null');
} }
final itemCount = controller.page!.round();
final output = sendTemplateViewModel.output; return sendTemplateViewModel.recipients[itemCount];
reaction((_) => output.fiatAmount, (String amount) {
if (amount != _fiatAmountController.text) {
_fiatAmountController.text = amount;
}
});
reaction((_) => output.cryptoAmount, (String amount) {
if (amount != _cryptoAmountController.text) {
_cryptoAmountController.text = amount;
}
});
reaction((_) => output.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
_cryptoAmountController.addListener(() {
final amount = _cryptoAmountController.text;
if (amount != output.cryptoAmount) {
output.setCryptoAmount(amount);
}
});
_fiatAmountController.addListener(() {
final amount = _fiatAmountController.text;
if (amount != output.fiatAmount) {
output.setFiatAmount(amount);
}
});
_addressController.addListener(() {
final address = _addressController.text;
if (output.address != address) {
output.address = address;
}
});
_effectsInstalled = true;
} }
} }

View file

@ -0,0 +1,267 @@
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.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:mobx/mobx.dart';
class SendTemplateCard extends StatelessWidget {
SendTemplateCard(
{super.key,
required this.template,
required this.index,
required this.sendTemplateViewModel});
final TemplateViewModel template;
final int index;
final SendTemplateViewModel sendTemplateViewModel;
final _addressController = TextEditingController();
final _cryptoAmountController = TextEditingController();
final _fiatAmountController = TextEditingController();
final _nameController = TextEditingController();
final FocusNode _cryptoAmountFocus = FocusNode();
final FocusNode _fiatAmountFocus = FocusNode();
bool _effectsInstalled = false;
@override
Widget build(BuildContext context) {
_setEffects(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
], begin: Alignment.topLeft, end: Alignment.bottomRight)),
child: Column(children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
child: Column(children: <Widget>[
if (index == 0)
BaseTextFormField(
controller: _nameController,
hintText: sendTemplateViewModel.recipients.length > 1
? S.of(context).template_name
: S.of(context).send_name,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.templateValidator),
Padding(
padding: EdgeInsets.only(top: 20),
child: AddressTextField(
selectedCurrency: sendTemplateViewModel.cryptoCurrency,
controller: _addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
_addressController.text = paymentRequest.address;
_cryptoAmountController.text = paymentRequest.amount;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
onPushPasteButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
buttonColor: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
validator: sendTemplateViewModel.addressValidator)),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectCurrency();
}
},
child: BaseTextFormField(
focusNode: _cryptoAmountFocus,
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.cryptoCurrency.title,
isSelected: template.isCurrencySelected)),
hintText: '0.0000',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.amountValidator))),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectFiat();
}
},
child: BaseTextFormField(
focusNode: _fiatAmountFocus,
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel.fiatCurrency,
isSelected: template.isFiatSelected)),
hintText: '0.00',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14))))
]))
]));
}
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
final output = template.output;
if (template.address.isNotEmpty) {
_addressController.text = template.address;
}
if (template.name.isNotEmpty) {
_nameController.text = template.name;
}
if (template.output.cryptoAmount.isNotEmpty) {
_cryptoAmountController.text = template.output.cryptoAmount;
}
if (template.output.fiatAmount.isNotEmpty) {
_fiatAmountController.text = template.output.fiatAmount;
}
_addressController.addListener(() {
final address = _addressController.text;
if (template.address != address) {
template.address = address;
}
});
_cryptoAmountController.addListener(() {
final amount = _cryptoAmountController.text;
if (amount != output.cryptoAmount) {
output.setCryptoAmount(amount);
}
});
_fiatAmountController.addListener(() {
final amount = _fiatAmountController.text;
if (amount != output.fiatAmount) {
output.setFiatAmount(amount);
}
});
_nameController.addListener(() {
final name = _nameController.text;
if (name != template.name) {
template.name = name;
}
});
reaction((_) => template.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
reaction((_) => output.cryptoAmount, (String amount) {
if (amount != _cryptoAmountController.text) {
_cryptoAmountController.text = amount;
}
});
reaction((_) => output.fiatAmount, (String amount) {
if (amount != _fiatAmountController.text) {
_fiatAmountController.text = amount;
}
});
reaction((_) => template.name, (String name) {
if (name != _nameController.text) {
_nameController.text = name;
}
});
_effectsInstalled = true;
}
}

View file

@ -8,7 +8,8 @@ class TemplateTile extends StatefulWidget {
required this.amount, required this.amount,
required this.from, required this.from,
required this.onTap, required this.onTap,
required this.onRemove required this.onRemove,
this.hasMultipleRecipients,
}) : super(key: key); }) : super(key: key);
final String to; final String to;
@ -16,6 +17,7 @@ class TemplateTile extends StatefulWidget {
final String from; final String from;
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback onRemove; final VoidCallback onRemove;
final bool? hasMultipleRecipients;
@override @override
TemplateTileState createState() => TemplateTileState( TemplateTileState createState() => TemplateTileState(
@ -51,45 +53,47 @@ class TemplateTileState extends State<TemplateTile> {
final toIcon = Image.asset('assets/images/to_icon.png', color: color); final toIcon = Image.asset('assets/images/to_icon.png', color: color);
final content = Row( final content = Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: widget.hasMultipleRecipients ?? false
Text( ? [
amount, Text(
style: TextStyle( to,
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16, fontWeight: FontWeight.w600, color: color),
color: color ),
), ]
), : [
Padding( Text(
padding: EdgeInsets.only(left: 5), amount,
child: Text( style: TextStyle(
from, fontSize: 16, fontWeight: FontWeight.w600, color: color),
style: TextStyle( ),
fontSize: 16, Padding(
fontWeight: FontWeight.w600, padding: EdgeInsets.only(left: 5),
color: color child: Text(
), from,
), style: TextStyle(
), fontSize: 16,
Padding( fontWeight: FontWeight.w600,
padding: EdgeInsets.only(left: 5), color: color),
child: toIcon, ),
), ),
Padding( Padding(
padding: EdgeInsets.only(left: 5), padding: EdgeInsets.only(left: 5),
child: Text( child: toIcon,
to, ),
style: TextStyle( Padding(
fontSize: 16, padding: EdgeInsets.only(left: 5),
fontWeight: FontWeight.w600, child: Text(
color: color to,
), style: TextStyle(
), fontSize: 16,
), fontWeight: FontWeight.w600,
], color: color),
); ),
),
]);
final tile = Container( final tile = Container(
padding: EdgeInsets.only(right: 10), padding: EdgeInsets.only(right: 10),

View file

@ -23,25 +23,27 @@ abstract class SendTemplateBase with Store {
templates.replaceRange(0, templates.length, templateSource.values.toList()); templates.replaceRange(0, templates.length, templateSource.values.toList());
@action @action
Future<void> addTemplate({ Future<void> addTemplate(
required String name, {required String name,
required bool isCurrencySelected, required bool isCurrencySelected,
required String address, required String address,
required String cryptoCurrency, required String cryptoCurrency,
required String fiatCurrency, required String fiatCurrency,
required String amount, required String amount,
required String amountFiat}) async { required String amountFiat,
required List<Template> additionalRecipients}) async {
final template = Template( final template = Template(
nameRaw: name, nameRaw: name,
isCurrencySelectedRaw: isCurrencySelected, isCurrencySelectedRaw: isCurrencySelected,
addressRaw: address, addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency, cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency, fiatCurrencyRaw: fiatCurrency,
amountRaw: amount, amountRaw: amount,
amountFiatRaw: amountFiat); amountFiatRaw: amountFiat,
additionalRecipientsRaw: additionalRecipients);
await templateSource.add(template); await templateSource.add(template);
} }
@action @action
Future<void> remove({required Template template}) async => await template.delete(); Future<void> remove({required Template template}) async => await template.delete();
} }

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/template.dart';
@ -6,10 +7,7 @@ import 'package:cake_wallet/store/templates/send_template_store.dart';
import 'package:cake_wallet/core/template_validator.dart'; import 'package:cake_wallet/core/template_validator.dart';
import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/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/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
@ -19,72 +17,74 @@ class SendTemplateViewModel = SendTemplateViewModelBase
with _$SendTemplateViewModel; with _$SendTemplateViewModel;
abstract class SendTemplateViewModelBase with Store { abstract class SendTemplateViewModelBase with Store {
SendTemplateViewModelBase(this._wallet, this._settingsStore,
this._sendTemplateStore, this._fiatConversationStore)
: output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _wallet.currency) {
output = Output(_wallet, _settingsStore, _fiatConversationStore, () => currency);
}
Output output;
Validator get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
Validator get addressValidator => AddressValidator(type: _wallet.currency);
Validator get templateValidator => TemplateValidator();
CryptoCurrency get currency => _wallet.currency;
FiatCurrency get fiat => _settingsStore.fiatCurrency;
@observable
bool isCurrencySelected = true;
@observable
bool isFiatSelected = false;
@action
void selectCurrency () {
isCurrencySelected = true;
isFiatSelected = false;
}
@action
void selectFiat () {
isFiatSelected = true;
isCurrencySelected = false;
}
@computed
ObservableList<Template> get templates => _sendTemplateStore.templates;
final WalletBase _wallet; final WalletBase _wallet;
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
final SendTemplateStore _sendTemplateStore; final SendTemplateStore _sendTemplateStore;
final FiatConversionStore _fiatConversationStore; final FiatConversionStore _fiatConversationStore;
SendTemplateViewModelBase(this._wallet, this._settingsStore,
this._sendTemplateStore, this._fiatConversationStore)
: recipients = ObservableList<TemplateViewModel>() {
addRecipient();
}
ObservableList<TemplateViewModel> recipients;
@action
void addRecipient() {
recipients.add(TemplateViewModel(
cryptoCurrency: cryptoCurrency,
wallet: _wallet,
settingsStore: _settingsStore,
fiatConversationStore: _fiatConversationStore));
}
@action
void removeRecipient(TemplateViewModel recipient) {
recipients.remove(recipient);
}
AmountValidator get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
AddressValidator get addressValidator =>
AddressValidator(type: _wallet.currency);
TemplateValidator get templateValidator => TemplateValidator();
@computed
CryptoCurrency get cryptoCurrency => _wallet.currency;
@computed
String get fiatCurrency => _settingsStore.fiatCurrency.title;
@computed
ObservableList<Template> get templates => _sendTemplateStore.templates;
@action
void updateTemplate() => _sendTemplateStore.update(); void updateTemplate() => _sendTemplateStore.update();
@action
void addTemplate( void addTemplate(
{required String name, {required String name,
required bool isCurrencySelected, required bool isCurrencySelected,
required String address, required String address,
required String cryptoCurrency, required String amount,
required String fiatCurrency, required String amountFiat,
required String amount, required List<Template> additionalRecipients}) {
required String amountFiat}) {
_sendTemplateStore.addTemplate( _sendTemplateStore.addTemplate(
name: name, name: name,
isCurrencySelected: isCurrencySelected, isCurrencySelected: isCurrencySelected,
address: address, address: address,
cryptoCurrency: cryptoCurrency, cryptoCurrency: cryptoCurrency.title,
fiatCurrency: fiatCurrency, fiatCurrency: fiatCurrency,
amount: amount, amount: amount,
amountFiat: amountFiat); amountFiat: amountFiat,
additionalRecipients: additionalRecipients);
updateTemplate(); updateTemplate();
} }
@action
void removeTemplate({required Template template}) { void removeTemplate({required Template template}) {
_sendTemplateStore.remove(template: template); _sendTemplateStore.remove(template: template);
updateTemplate(); updateTemplate();

View file

@ -0,0 +1,80 @@
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
part 'template_view_model.g.dart';
class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
abstract class TemplateViewModelBase with Store {
final CryptoCurrency cryptoCurrency;
final WalletBase _wallet;
final SettingsStore _settingsStore;
final FiatConversionStore _fiatConversationStore;
TemplateViewModelBase(
{required this.cryptoCurrency,
required WalletBase wallet,
required SettingsStore settingsStore,
required FiatConversionStore fiatConversationStore})
: _wallet = wallet,
_settingsStore = settingsStore,
_fiatConversationStore = fiatConversationStore,
output = Output(wallet, settingsStore, fiatConversationStore,
() => wallet.currency) {
output = Output(
_wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency);
}
@observable
Output output;
@observable
String name = '';
@observable
String address = '';
@observable
bool isCurrencySelected = true;
@observable
bool isFiatSelected = false;
@action
void selectCurrency() {
isCurrencySelected = true;
isFiatSelected = false;
}
@action
void selectFiat() {
isFiatSelected = true;
isCurrencySelected = false;
}
@action
void reset() {
name = '';
address = '';
isCurrencySelected = true;
isFiatSelected = false;
output.reset();
}
Template toTemplate(
{required String cryptoCurrency, required String fiatCurrency}) {
return Template(
isCurrencySelectedRaw: isCurrencySelected,
nameRaw: name,
addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency,
amountRaw: output.cryptoAmount,
amountFiatRaw: output.fiatAmount);
}
}

View file

@ -635,5 +635,6 @@
"generate_name": "توليد الاسم", "generate_name": "توليد الاسم",
"balance_page": "صفحة التوازن", "balance_page": "صفحة التوازن",
"share": "يشارك", "share": "يشارك",
"slidable": "قابل للانزلاق" "slidable": "قابل للانزلاق",
"template_name": "اسم القالب"
} }

View file

@ -631,5 +631,6 @@
"generate_name": "Генериране на име", "generate_name": "Генериране на име",
"balance_page": "Страница за баланс", "balance_page": "Страница за баланс",
"share": "Дял", "share": "Дял",
"slidable": "Плъзгащ се" "slidable": "Плъзгащ се",
"template_name": "Име на шаблон"
} }

View file

@ -631,5 +631,6 @@
"generate_name": "Generovat jméno", "generate_name": "Generovat jméno",
"balance_page": "Stránka zůstatku", "balance_page": "Stránka zůstatku",
"share": "Podíl", "share": "Podíl",
"slidable": "Posuvné" "slidable": "Posuvné",
"template_name": "Název šablony"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Namen generieren", "generate_name": "Namen generieren",
"balance_page": "Balance-Seite", "balance_page": "Balance-Seite",
"share": "Aktie", "share": "Aktie",
"slidable": "Verschiebbar" "slidable": "Verschiebbar",
"template_name": "Vorlagenname"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Generate Name", "generate_name": "Generate Name",
"balance_page": "Balance Page", "balance_page": "Balance Page",
"share": "Share", "share": "Share",
"slidable": "Slidable" "slidable": "Slidable",
"template_name": "Template Name"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Generar nombre", "generate_name": "Generar nombre",
"balance_page": "Página de saldo", "balance_page": "Página de saldo",
"share": "Compartir", "share": "Compartir",
"slidable": "deslizable" "slidable": "deslizable",
"template_name": "Nombre de la plantilla"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Générer un nom", "generate_name": "Générer un nom",
"balance_page": "Page Solde", "balance_page": "Page Solde",
"share": "Partager", "share": "Partager",
"slidable": "Glissable" "slidable": "Glissable",
"template_name": "Nom du modèle"
} }

View file

@ -617,6 +617,7 @@
"generate_name": "Ƙirƙirar Suna", "generate_name": "Ƙirƙirar Suna",
"balance_page": "Ma'auni Page", "balance_page": "Ma'auni Page",
"share": "Raba", "share": "Raba",
"slidable": "Mai iya zamewa" "slidable": "Mai iya zamewa",
"template_name": "Sunan Samfura"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "नाम जनरेट करें", "generate_name": "नाम जनरेट करें",
"balance_page": "बैलेंस पेज", "balance_page": "बैलेंस पेज",
"share": "शेयर करना", "share": "शेयर करना",
"slidable": "फिसलने लायक" "slidable": "फिसलने लायक",
"template_name": "टेम्पलेट नाम"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Generiraj ime", "generate_name": "Generiraj ime",
"balance_page": "Stranica sa stanjem", "balance_page": "Stranica sa stanjem",
"share": "Udio", "share": "Udio",
"slidable": "Klizna" "slidable": "Klizna",
"template_name": "Naziv predloška"
} }

View file

@ -627,5 +627,6 @@
"generate_name": "Hasilkan Nama", "generate_name": "Hasilkan Nama",
"balance_page": "Halaman Saldo", "balance_page": "Halaman Saldo",
"share": "Membagikan", "share": "Membagikan",
"slidable": "Dapat digeser" "slidable": "Dapat digeser",
"template_name": "Nama Templat"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Genera nome", "generate_name": "Genera nome",
"balance_page": "Pagina di equilibrio", "balance_page": "Pagina di equilibrio",
"share": "Condividere", "share": "Condividere",
"slidable": "Scorrevole" "slidable": "Scorrevole",
"template_name": "Nome modello"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "名前の生成", "generate_name": "名前の生成",
"balance_page": "残高ページ", "balance_page": "残高ページ",
"share": "共有", "share": "共有",
"slidable": "スライド可能" "slidable": "スライド可能",
"template_name": "テンプレート名"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "이름 생성", "generate_name": "이름 생성",
"balance_page": "잔액 페이지", "balance_page": "잔액 페이지",
"share": "공유하다", "share": "공유하다",
"slidable": "슬라이딩 가능" "slidable": "슬라이딩 가능",
"template_name": "템플릿 이름"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "အမည်ဖန်တီးပါ။", "generate_name": "အမည်ဖန်တီးပါ။",
"balance_page": "လက်ကျန်စာမျက်နှာ", "balance_page": "လက်ကျန်စာမျက်နှာ",
"share": "မျှဝေပါ။", "share": "မျှဝေပါ။",
"slidable": "လျှောချနိုင်သည်။" "slidable": "လျှောချနိုင်သည်။",
"template_name": "နမူနာပုံစံ"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Naam genereren", "generate_name": "Naam genereren",
"balance_page": "Saldo pagina", "balance_page": "Saldo pagina",
"share": "Deel", "share": "Deel",
"slidable": "Verschuifbaar" "slidable": "Verschuifbaar",
"template_name": "Sjabloonnaam"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Wygeneruj nazwę", "generate_name": "Wygeneruj nazwę",
"balance_page": "Strona salda", "balance_page": "Strona salda",
"share": "Udział", "share": "Udział",
"slidable": "Przesuwne" "slidable": "Przesuwne",
"template_name": "Nazwa szablonu"
} }

View file

@ -636,5 +636,6 @@
"generate_name": "Gerar nome", "generate_name": "Gerar nome",
"balance_page": "Página de saldo", "balance_page": "Página de saldo",
"share": "Compartilhar", "share": "Compartilhar",
"slidable": "Deslizável" "slidable": "Deslizável",
"template_name": "Nome do modelo"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Создать имя", "generate_name": "Создать имя",
"balance_page": "Страница баланса", "balance_page": "Страница баланса",
"share": "Делиться", "share": "Делиться",
"slidable": "Скользящий" "slidable": "Скользящий",
"template_name": "Имя Шаблона"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "สร้างชื่อ", "generate_name": "สร้างชื่อ",
"balance_page": "หน้ายอดคงเหลือ", "balance_page": "หน้ายอดคงเหลือ",
"share": "แบ่งปัน", "share": "แบ่งปัน",
"slidable": "เลื่อนได้" "slidable": "เลื่อนได้",
"template_name": "ชื่อแม่แบบ"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "İsim Oluştur", "generate_name": "İsim Oluştur",
"balance_page": "Bakiye Sayfası", "balance_page": "Bakiye Sayfası",
"share": "Paylaşmak", "share": "Paylaşmak",
"slidable": "kaydırılabilir" "slidable": "kaydırılabilir",
"template_name": "şablon adı"
} }

View file

@ -637,5 +637,6 @@
"generate_name": "Згенерувати назву", "generate_name": "Згенерувати назву",
"balance_page": "Сторінка балансу", "balance_page": "Сторінка балансу",
"share": "Поділіться", "share": "Поділіться",
"slidable": "Розсувний" "slidable": "Розсувний",
"template_name": "Назва шаблону"
} }

View file

@ -631,5 +631,6 @@
"generate_name": "نام پیدا کریں۔", "generate_name": "نام پیدا کریں۔",
"balance_page": "بیلنس صفحہ", "balance_page": "بیلنس صفحہ",
"share": "بانٹیں", "share": "بانٹیں",
"slidable": "سلائیڈ ایبل" "slidable": "سلائیڈ ایبل",
"template_name": "ٹیمپلیٹ کا نام"
} }

View file

@ -633,5 +633,6 @@
"generate_name": "Ṣẹda Orukọ", "generate_name": "Ṣẹda Orukọ",
"balance_page": "Oju-iwe iwọntunwọnsi", "balance_page": "Oju-iwe iwọntunwọnsi",
"share": "Pinpin", "share": "Pinpin",
"slidable": "Slidable" "slidable": "Slidable",
"template_name": "Orukọ Awoṣe"
} }

View file

@ -636,5 +636,6 @@
"generate_name": "生成名称", "generate_name": "生成名称",
"balance_page": "余额页", "balance_page": "余额页",
"share": "分享", "share": "分享",
"slidable": "可滑动" "slidable": "可滑动",
"template_name": "模板名称"
} }