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)
class Template extends HiveObject {
Template({
required this.nameRaw,
required this.isCurrencySelectedRaw,
required this.addressRaw,
required this.cryptoCurrencyRaw,
required this.amountRaw,
required this.fiatCurrencyRaw,
required this.amountFiatRaw});
Template(
{required this.nameRaw,
required this.isCurrencySelectedRaw,
required this.addressRaw,
required this.cryptoCurrencyRaw,
required this.amountRaw,
required this.fiatCurrencyRaw,
required this.amountFiatRaw,
this.additionalRecipientsRaw});
static const typeId = 6;
static const boxName = 'Template';
@ -37,6 +38,9 @@ class Template extends HiveObject {
@HiveField(6)
String? amountFiatRaw;
@HiveField(7)
List<Template>? additionalRecipientsRaw;
bool get isCurrencySelected => isCurrencySelectedRaw ?? false;
String get fiatCurrency => fiatCurrencyRaw ?? '';
@ -50,5 +54,6 @@ class Template extends HiveObject {
String get cryptoCurrency => cryptoCurrencyRaw ?? '';
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/template.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/widgets/add_template_button.dart';
@ -241,6 +242,11 @@ class SendPage extends BasePage {
return TemplateTile(
key: UniqueKey(),
to: template.name,
hasMultipleRecipients:
template.additionalRecipients !=
null &&
template.additionalRecipients!
.length > 1,
amount: template.isCurrencySelected
? template.amount
: template.amountFiat,
@ -248,25 +254,36 @@ class SendPage extends BasePage {
? template.cryptoCurrency
: template.fiatCurrency,
onTap: () async {
final fiatFromTemplate = FiatCurrency
.all
.singleWhere((element) =>
element.title ==
template.fiatCurrency);
final output = _defineCurrentOutput();
output.address = template.address;
if (template.isCurrencySelected) {
output
.setCryptoAmount(template.amount);
if (template.additionalRecipients !=
null) {
sendViewModel.clearOutputs();
template.additionalRecipients!
.forEach((currentElement) async {
int i = template
.additionalRecipients!
.indexOf(currentElement);
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
await _setInputsFromTemplate(
context,
output: output,
template: currentElement);
});
} else {
sendViewModel.setFiatCurrency(
fiatFromTemplate);
output.setFiatAmount(
template.amountFiat);
final output = _defineCurrentOutput();
await _setInputsFromTemplate(
context,
output: output,
template: template);
}
output.resetParsedAddress();
await output
.fetchParsedAddress(context);
},
onRemove: () {
showPopUp<void>(
@ -477,6 +494,24 @@ class SendPage extends BasePage {
_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() {
if (controller.page == 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:mobx/mobx.dart';
import 'package:flutter/cupertino.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/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:cake_wallet/src/widgets/keyboard_done_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/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 {
SendTemplatePage({required this.sendTemplateViewModel}) {
sendTemplateViewModel.output.reset();
}
SendTemplatePage({required this.sendTemplateViewModel});
final SendTemplateViewModel sendTemplateViewModel;
final _addressController = TextEditingController();
final _cryptoAmountController = TextEditingController();
final _fiatAmountController = TextEditingController();
final _nameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final FocusNode _cryptoAmountFocus = FocusNode();
final FocusNode _fiatAmountFocus = FocusNode();
bool _effectsInstalled = false;
final controller = PageController(initialPage: 0);
@override
String get title => S.current.exchange_new_template;
@ -44,273 +30,146 @@ class SendTemplatePage extends BasePage {
AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override
Widget body(BuildContext context) {
_setEffects(context);
Widget trailing(context) => Observer(builder: (_) {
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(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context)
.accentTextTheme!
.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(
@override
Widget body(BuildContext context) {
return Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: 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: Form(
key: _formKey,
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
child: Column(
children: <Widget>[
BaseTextFormField(
controller: _nameController,
hintText: 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(
controller: _addressController,
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),
))),
],
content: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(children: [
Container(
height: 460,
child: Observer(builder: (_) {
return PageView.builder(
scrollDirection: Axis.horizontal,
controller: controller,
itemCount: sendTemplateViewModel.recipients.length,
itemBuilder: (_, index) {
final template =
sendTemplateViewModel.recipients[index];
return SendTemplateCard(
template: template,
index: index,
sendTemplateViewModel: sendTemplateViewModel);
});
})),
Padding(
padding: EdgeInsets.only(
top: 10, left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
builder: (_) {
final count = sendTemplateViewModel.recipients.length;
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
.displaySmall!
.backgroundColor!,
activeDotColor: Theme.of(context)
.primaryTextTheme
.displayMedium!
.backgroundColor!))
: Offstage();
},
),
)
],
),
),
),
),
),
])),
bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: PrimaryButton(
onPressed: () {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
sendTemplateViewModel.addTemplate(
isCurrencySelected: sendTemplateViewModel.isCurrencySelected,
name: _nameController.text,
address: _addressController.text,
cryptoCurrency:sendTemplateViewModel.currency.title,
fiatCurrency: sendTemplateViewModel.fiat.title,
amount: _cryptoAmountController.text,
amountFiat: _fiatAmountController.text);
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Colors.green,
textColor: Colors.white,
),
),
));
bottomSection: Column(children: [
// if (sendViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () {
sendTemplateViewModel.addRecipient();
Future.delayed(const Duration(milliseconds: 250), () {
controller.jumpToPage(
sendTemplateViewModel.recipients.length - 1);
});
},
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor: Theme.of(context)
.accentTextTheme
.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) {
if (_effectsInstalled) {
return;
TemplateViewModel _defineCurrentRecipient() {
if (controller.page == null) {
throw Exception('Controller page is null');
}
final output = sendTemplateViewModel.output;
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;
final itemCount = controller.page!.round();
return sendTemplateViewModel.recipients[itemCount];
}
}

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

View file

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

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:mobx/mobx.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/address_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/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';
@ -19,72 +17,74 @@ class SendTemplateViewModel = SendTemplateViewModelBase
with _$SendTemplateViewModel;
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 SettingsStore _settingsStore;
final SendTemplateStore _sendTemplateStore;
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();
@action
void addTemplate(
{required String name,
required bool isCurrencySelected,
required String address,
required String cryptoCurrency,
required String fiatCurrency,
required String amount,
required String amountFiat}) {
required bool isCurrencySelected,
required String address,
required String amount,
required String amountFiat,
required List<Template> additionalRecipients}) {
_sendTemplateStore.addTemplate(
name: name,
isCurrencySelected: isCurrencySelected,
address: address,
cryptoCurrency: cryptoCurrency,
cryptoCurrency: cryptoCurrency.title,
fiatCurrency: fiatCurrency,
amount: amount,
amountFiat: amountFiat);
amountFiat: amountFiat,
additionalRecipients: additionalRecipients);
updateTemplate();
}
@action
void removeTemplate({required Template template}) {
_sendTemplateStore.remove(template: template);
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": "توليد الاسم",
"balance_page": "صفحة التوازن",
"share": "يشارك",
"slidable": "قابل للانزلاق"
"slidable": "قابل للانزلاق",
"template_name": "اسم القالب"
}

View file

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

View file

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

View file

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

View file

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

View file

@ -637,5 +637,6 @@
"generate_name": "Generar nombre",
"balance_page": "Página de saldo",
"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",
"balance_page": "Page Solde",
"share": "Partager",
"slidable": "Glissable"
"slidable": "Glissable",
"template_name": "Nom du modèle"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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