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

Rafael Saes committed Aug 1, 2023 at 19:19 UTC fcf4fbdc14d2aab9e8c9eda6662dde91c1d3ae7f
33 files changed +706 -429
lib/entities/template.dart
+14 -9
@@ -4,14 +4,15 @@ part 'template.g.dart';
4
5 @HiveType(typeId: Template.typeId)
6 class Template extends HiveObject {
7 - Template({
8 - required this.nameRaw,
9 - required this.isCurrencySelectedRaw,
10 - required this.addressRaw,
11 - required this.cryptoCurrencyRaw,
12 - required this.amountRaw,
13 - required this.fiatCurrencyRaw,
14 - required this.amountFiatRaw});
7 + Template(
8 + {required this.nameRaw,
9 + required this.isCurrencySelectedRaw,
10 + required this.addressRaw,
11 + required this.cryptoCurrencyRaw,
12 + required this.amountRaw,
13 + required this.fiatCurrencyRaw,
14 + required this.amountFiatRaw,
15 + this.additionalRecipientsRaw});
16
17 static const typeId = 6;
18 static const boxName = 'Template';
@@ -37,6 +38,9 @@ class Template extends HiveObject {
38 @HiveField(6)
39 String? amountFiatRaw;
40
41 + @HiveField(7)
42 + List<Template>? additionalRecipientsRaw;
43 +
44 bool get isCurrencySelected => isCurrencySelectedRaw ?? false;
45
46 String get fiatCurrency => fiatCurrencyRaw ?? '';
@@ -50,5 +54,6 @@ class Template extends HiveObject {
54 String get cryptoCurrency => cryptoCurrencyRaw ?? '';
55
56 String get amount => amountRaw ?? '';
53 -}
57
58 + List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
59 +}
lib/src/screens/send/send_page.dart
+52 -17
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/entities/fiat_currency.dart';
2 +import 'package:cake_wallet/entities/template.dart';
3 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
4 import 'package:cake_wallet/src/screens/send/widgets/send_card.dart';
5 import 'package:cake_wallet/src/widgets/add_template_button.dart';
@@ -241,6 +242,11 @@ class SendPage extends BasePage {
242 return TemplateTile(
243 key: UniqueKey(),
244 to: template.name,
245 + hasMultipleRecipients:
246 + template.additionalRecipients !=
247 + null &&
248 + template.additionalRecipients!
249 + .length > 1,
250 amount: template.isCurrencySelected
251 ? template.amount
252 : template.amountFiat,
@@ -248,25 +254,36 @@ class SendPage extends BasePage {
254 ? template.cryptoCurrency
255 : template.fiatCurrency,
256 onTap: () async {
251 - final fiatFromTemplate = FiatCurrency
252 - .all
253 - .singleWhere((element) =>
254 - element.title ==
255 - template.fiatCurrency);
256 - final output = _defineCurrentOutput();
257 - output.address = template.address;
258 - if (template.isCurrencySelected) {
259 - output
260 - .setCryptoAmount(template.amount);
257 + if (template.additionalRecipients !=
258 + null) {
259 + sendViewModel.clearOutputs();
260 +
261 + template.additionalRecipients!
262 + .forEach((currentElement) async {
263 + int i = template
264 + .additionalRecipients!
265 + .indexOf(currentElement);
266 +
267 + Output output;
268 + try {
269 + output = sendViewModel.outputs[i];
270 + } catch (e) {
271 + sendViewModel.addOutput();
272 + output = sendViewModel.outputs[i];
273 + }
274 +
275 + await _setInputsFromTemplate(
276 + context,
277 + output: output,
278 + template: currentElement);
279 + });
280 } else {
262 - sendViewModel.setFiatCurrency(
263 - fiatFromTemplate);
264 - output.setFiatAmount(
265 - template.amountFiat);
281 + final output = _defineCurrentOutput();
282 + await _setInputsFromTemplate(
283 + context,
284 + output: output,
285 + template: template);
286 }
267 - output.resetParsedAddress();
268 - await output
269 - .fetchParsedAddress(context);
287 },
288 onRemove: () {
289 showPopUp<void>(
@@ -477,6 +494,24 @@ class SendPage extends BasePage {
494 _effectsInstalled = true;
495 }
496
497 + Future<void> _setInputsFromTemplate(BuildContext context,
498 + {required Output output, required Template template}) async {
499 + final fiatFromTemplate = FiatCurrency.all
500 + .singleWhere((element) => element.title == template.fiatCurrency);
501 +
502 + output.address = template.address;
503 +
504 + if (template.isCurrencySelected) {
505 + output.setCryptoAmount(template.amount);
506 + } else {
507 + sendViewModel.setFiatCurrency(fiatFromTemplate);
508 + output.setFiatAmount(template.amountFiat);
509 + }
510 +
511 + output.resetParsedAddress();
512 + await output.fetchParsedAddress(context);
513 + }
514 +
515 Output _defineCurrentOutput() {
516 if (controller.page == null) {
517 throw Exception('Controller page is null');
lib/src/screens/send/send_template_page.dart
+139 -280
@@ -1,35 +1,21 @@
1 -import 'package:cake_wallet/utils/payment_request.dart';
1 +import 'package:cake_wallet/src/widgets/trail_button.dart';
2 +import 'package:cake_wallet/view_model/send/template_view_model.dart';
3 import 'package:flutter_mobx/flutter_mobx.dart';
3 -import 'package:mobx/mobx.dart';
4 -import 'package:flutter/cupertino.dart';
4 import 'package:flutter/material.dart';
6 -import 'package:flutter/services.dart';
7 -import 'package:keyboard_actions/keyboard_actions.dart';
5 import 'package:cake_wallet/src/screens/base_page.dart';
6 import 'package:cake_wallet/generated/i18n.dart';
7 import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
11 -import 'package:cake_wallet/src/widgets/address_text_field.dart';
12 -import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
13 -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
8 import 'package:cake_wallet/src/widgets/primary_button.dart';
9 import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
16 -import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
10 +import 'package:cake_wallet/src/screens/send/widgets/send_template_card.dart';
11 +import 'package:smooth_page_indicator/smooth_page_indicator.dart';
12
13 class SendTemplatePage extends BasePage {
19 - SendTemplatePage({required this.sendTemplateViewModel}) {
20 - sendTemplateViewModel.output.reset();
21 - }
14 + SendTemplatePage({required this.sendTemplateViewModel});
15
16 final SendTemplateViewModel sendTemplateViewModel;
24 - final _addressController = TextEditingController();
25 - final _cryptoAmountController = TextEditingController();
26 - final _fiatAmountController = TextEditingController();
27 - final _nameController = TextEditingController();
17 final _formKey = GlobalKey<FormState>();
29 - final FocusNode _cryptoAmountFocus = FocusNode();
30 - final FocusNode _fiatAmountFocus = FocusNode();
31 -
32 - bool _effectsInstalled = false;
18 + final controller = PageController(initialPage: 0);
19
20 @override
21 String get title => S.current.exchange_new_template;
@@ -44,273 +30,146 @@ class SendTemplatePage extends BasePage {
30 AppBarStyle get appBarStyle => AppBarStyle.transparent;
31
32 @override
47 - Widget body(BuildContext context) {
48 - _setEffects(context);
33 + Widget trailing(context) => Observer(builder: (_) {
34 + return sendTemplateViewModel.recipients.length > 1
35 + ? TrailButton(
36 + caption: S.of(context).remove,
37 + onPressed: () {
38 + int pageToJump = (controller.page?.round() ?? 0) - 1;
39 + pageToJump = pageToJump > 0 ? pageToJump : 0;
40 + final recipient = _defineCurrentRecipient();
41 + sendTemplateViewModel.removeRecipient(recipient);
42 + controller.jumpToPage(pageToJump);
43 + })
44 + : TrailButton(
45 + caption: S.of(context).clear,
46 + onPressed: () {
47 + final recipient = _defineCurrentRecipient();
48 + _formKey.currentState?.reset();
49 + recipient.reset();
50 + });
51 + });
52
50 - return KeyboardActions(
51 - config: KeyboardActionsConfig(
52 - keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
53 - keyboardBarColor: Theme.of(context)
54 - .accentTextTheme!
55 - .bodyLarge!
56 - .backgroundColor!,
57 - nextFocus: false,
58 - actions: [
59 - KeyboardActionsItem(
60 - focusNode: _cryptoAmountFocus,
61 - toolbarButtons: [(_) => KeyboardDoneButton()],
62 - ),
63 - KeyboardActionsItem(
64 - focusNode: _fiatAmountFocus,
65 - toolbarButtons: [(_) => KeyboardDoneButton()],
66 - )
67 - ]),
68 - child: Container(
69 - height: 0,
70 - color: Theme.of(context).colorScheme.background,
71 - child: ScrollableWithBottomSection(
53 + @override
54 + Widget body(BuildContext context) {
55 + return Form(
56 + key: _formKey,
57 + child: ScrollableWithBottomSection(
58 contentPadding: EdgeInsets.only(bottom: 24),
73 - content: Container(
74 - decoration: BoxDecoration(
75 - borderRadius: BorderRadius.only(
76 - bottomLeft: Radius.circular(24),
77 - bottomRight: Radius.circular(24),
78 - ),
79 - gradient: LinearGradient(colors: [
80 - Theme.of(context).primaryTextTheme!.titleMedium!.color!,
81 - Theme.of(context)
82 - .primaryTextTheme!
83 - .titleMedium!
84 - .decorationColor!,
85 - ], begin: Alignment.topLeft, end: Alignment.bottomRight),
86 - ),
87 - child: Form(
88 - key: _formKey,
89 - child: Column(
90 - children: <Widget>[
91 - Padding(
92 - padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
93 - child: Column(
94 - children: <Widget>[
95 - BaseTextFormField(
96 - controller: _nameController,
97 - hintText: S.of(context).send_name,
98 - borderColor: Theme.of(context)
99 - .primaryTextTheme!
100 - .headlineSmall!
101 - .color!,
102 - textStyle: TextStyle(
103 - fontSize: 14,
104 - fontWeight: FontWeight.w500,
105 - color: Colors.white),
106 - placeholderTextStyle: TextStyle(
107 - color: Theme.of(context)
108 - .primaryTextTheme!
109 - .headlineSmall!
110 - .decorationColor!,
111 - fontWeight: FontWeight.w500,
112 - fontSize: 14),
113 - validator: sendTemplateViewModel.templateValidator,
114 - ),
115 - Padding(
116 - padding: EdgeInsets.only(top: 20),
117 - child: AddressTextField(
118 - controller: _addressController,
119 - onURIScanned: (uri) {
120 - final paymentRequest = PaymentRequest.fromUri(uri);
121 - _addressController.text = paymentRequest.address;
122 - _cryptoAmountController.text = paymentRequest.amount;
123 - },
124 - options: [
125 - AddressTextFieldOption.paste,
126 - AddressTextFieldOption.qrCode,
127 - AddressTextFieldOption.addressBook
128 - ],
129 - buttonColor: Theme.of(context)
130 - .primaryTextTheme!
131 - .headlineMedium!
132 - .color!,
133 - borderColor: Theme.of(context)
134 - .primaryTextTheme!
135 - .headlineSmall!
136 - .color!,
137 - textStyle: TextStyle(
138 - fontSize: 14,
139 - fontWeight: FontWeight.w500,
140 - color: Colors.white),
141 - hintStyle: TextStyle(
142 - fontSize: 14,
143 - fontWeight: FontWeight.w500,
144 - color: Theme.of(context)
145 - .primaryTextTheme!
146 - .headlineSmall!
147 - .decorationColor!),
148 - ),
149 - ),
150 - Padding(
151 - padding: const EdgeInsets.only(top: 20),
152 - child: Focus(
153 - onFocusChange: (hasFocus) {
154 - if (hasFocus) {
155 - sendTemplateViewModel.selectCurrency();
156 - }
157 - },
158 - child: BaseTextFormField(
159 - focusNode: _cryptoAmountFocus,
160 - controller: _cryptoAmountController,
161 - keyboardType: TextInputType.numberWithOptions(
162 - signed: false, decimal: true),
163 - inputFormatters: [
164 - FilteringTextInputFormatter.deny(
165 - RegExp('[\\-|\\ ]'))
166 - ],
167 - prefixIcon: Observer(
168 - builder: (_) => PrefixCurrencyIcon(
169 - title: sendTemplateViewModel
170 - .currency.title,
171 - isSelected:
172 - sendTemplateViewModel
173 - .isCurrencySelected,
174 - )),
175 - hintText: '0.0000',
176 - borderColor: Theme.of(context)
177 - .primaryTextTheme!
178 - .headlineSmall!
179 - .color!,
180 - textStyle: TextStyle(
181 - fontSize: 14,
182 - fontWeight: FontWeight.w500,
183 - color: Colors.white),
184 - placeholderTextStyle: TextStyle(
185 - color: Theme.of(context)
186 - .primaryTextTheme!
187 - .headlineSmall!
188 - .decorationColor!,
189 - fontWeight: FontWeight.w500,
190 - fontSize: 14),
191 - validator:
192 - sendTemplateViewModel.amountValidator))),
193 - Padding(
194 - padding: const EdgeInsets.only(top: 20),
195 - child: Focus(
196 - onFocusChange: (hasFocus) {
197 - if (hasFocus) {
198 - sendTemplateViewModel.selectFiat();
199 - }
200 - },
201 - child: BaseTextFormField(
202 - focusNode: _fiatAmountFocus,
203 - controller: _fiatAmountController,
204 - keyboardType: TextInputType.numberWithOptions(
205 - signed: false, decimal: true),
206 - inputFormatters: [
207 - FilteringTextInputFormatter.deny(
208 - RegExp('[\\-|\\ ]'))
209 - ],
210 - prefixIcon: Observer(
211 - builder: (_) => PrefixCurrencyIcon(
212 - title: sendTemplateViewModel
213 - .fiat.title,
214 - isSelected: sendTemplateViewModel
215 - .isFiatSelected,
216 - )),
217 - hintText: '0.00',
218 - borderColor: Theme.of(context)
219 - .primaryTextTheme!
220 - .headlineSmall!
221 - .color!,
222 - textStyle: TextStyle(
223 - fontSize: 14,
224 - fontWeight: FontWeight.w500,
225 - color: Colors.white),
226 - placeholderTextStyle: TextStyle(
227 - color: Theme.of(context)
228 - .primaryTextTheme!
229 - .headlineSmall!
230 - .decorationColor!,
231 - fontWeight: FontWeight.w500,
232 - fontSize: 14),
233 - ))),
234 - ],
59 + content: FocusTraversalGroup(
60 + policy: OrderedTraversalPolicy(),
61 + child: Column(children: [
62 + Container(
63 + height: 460,
64 + child: Observer(builder: (_) {
65 + return PageView.builder(
66 + scrollDirection: Axis.horizontal,
67 + controller: controller,
68 + itemCount: sendTemplateViewModel.recipients.length,
69 + itemBuilder: (_, index) {
70 + final template =
71 + sendTemplateViewModel.recipients[index];
72 + return SendTemplateCard(
73 + template: template,
74 + index: index,
75 + sendTemplateViewModel: sendTemplateViewModel);
76 + });
77 + })),
78 + Padding(
79 + padding: EdgeInsets.only(
80 + top: 10, left: 24, right: 24, bottom: 10),
81 + child: Container(
82 + height: 10,
83 + child: Observer(
84 + builder: (_) {
85 + final count = sendTemplateViewModel.recipients.length;
86 +
87 + return count > 1
88 + ? SmoothPageIndicator(
89 + controller: controller,
90 + count: count,
91 + effect: ScrollingDotsEffect(
92 + spacing: 6.0,
93 + radius: 6.0,
94 + dotWidth: 6.0,
95 + dotHeight: 6.0,
96 + dotColor: Theme.of(context)
97 + .primaryTextTheme
98 + .displaySmall!
99 + .backgroundColor!,
100 + activeDotColor: Theme.of(context)
101 + .primaryTextTheme
102 + .displayMedium!
103 + .backgroundColor!))
104 + : Offstage();
105 + },
106 ),
236 - )
237 - ],
238 - ),
239 - ),
240 - ),
107 + ),
108 + ),
109 + ])),
110 bottomSectionPadding:
111 EdgeInsets.only(left: 24, right: 24, bottom: 24),
243 - bottomSection: PrimaryButton(
244 - onPressed: () {
245 - if (_formKey.currentState != null && _formKey.currentState!.validate()) {
246 - sendTemplateViewModel.addTemplate(
247 - isCurrencySelected: sendTemplateViewModel.isCurrencySelected,
248 - name: _nameController.text,
249 - address: _addressController.text,
250 - cryptoCurrency:sendTemplateViewModel.currency.title,
251 - fiatCurrency: sendTemplateViewModel.fiat.title,
252 - amount: _cryptoAmountController.text,
253 - amountFiat: _fiatAmountController.text);
254 - Navigator.of(context).pop();
255 - }
256 - },
257 - text: S.of(context).save,
258 - color: Colors.green,
259 - textColor: Colors.white,
260 - ),
261 - ),
262 - ));
112 + bottomSection: Column(children: [
113 + // if (sendViewModel.hasMultiRecipient)
114 + Padding(
115 + padding: EdgeInsets.only(bottom: 12),
116 + child: PrimaryButton(
117 + onPressed: () {
118 + sendTemplateViewModel.addRecipient();
119 + Future.delayed(const Duration(milliseconds: 250), () {
120 + controller.jumpToPage(
121 + sendTemplateViewModel.recipients.length - 1);
122 + });
123 + },
124 + text: S.of(context).add_receiver,
125 + color: Colors.transparent,
126 + textColor: Theme.of(context)
127 + .accentTextTheme
128 + .displaySmall!
129 + .decorationColor!,
130 + isDottedBorder: true,
131 + borderColor: Theme.of(context)
132 + .primaryTextTheme
133 + .displaySmall!
134 + .decorationColor!)),
135 + PrimaryButton(
136 + onPressed: () {
137 + if (_formKey.currentState != null &&
138 + _formKey.currentState!.validate()) {
139 + final mainTemplate = sendTemplateViewModel.recipients[0];
140 + print(sendTemplateViewModel.recipients.map((element) =>
141 + element.toTemplate(
142 + cryptoCurrency:
143 + sendTemplateViewModel.cryptoCurrency.title,
144 + fiatCurrency:
145 + sendTemplateViewModel.fiatCurrency)));
146 + sendTemplateViewModel.addTemplate(
147 + isCurrencySelected: mainTemplate.isCurrencySelected,
148 + name: mainTemplate.name,
149 + address: mainTemplate.address,
150 + amount: mainTemplate.output.cryptoAmount,
151 + amountFiat: mainTemplate.output.fiatAmount,
152 + additionalRecipients: sendTemplateViewModel.recipients
153 + .map((element) => element.toTemplate(
154 + cryptoCurrency: sendTemplateViewModel
155 + .cryptoCurrency.title,
156 + fiatCurrency:
157 + sendTemplateViewModel.fiatCurrency))
158 + .toList());
159 + Navigator.of(context).pop();
160 + }
161 + },
162 + text: S.of(context).save,
163 + color: Colors.green,
164 + textColor: Colors.white)
165 + ])));
166 }
167
265 - void _setEffects(BuildContext context) {
266 - if (_effectsInstalled) {
267 - return;
168 + TemplateViewModel _defineCurrentRecipient() {
169 + if (controller.page == null) {
170 + throw Exception('Controller page is null');
171 }
269 -
270 - final output = sendTemplateViewModel.output;
271 -
272 - reaction((_) => output.fiatAmount, (String amount) {
273 - if (amount != _fiatAmountController.text) {
274 - _fiatAmountController.text = amount;
275 - }
276 - });
277 -
278 - reaction((_) => output.cryptoAmount, (String amount) {
279 - if (amount != _cryptoAmountController.text) {
280 - _cryptoAmountController.text = amount;
281 - }
282 - });
283 -
284 - reaction((_) => output.address, (String address) {
285 - if (address != _addressController.text) {
286 - _addressController.text = address;
287 - }
288 - });
289 -
290 - _cryptoAmountController.addListener(() {
291 - final amount = _cryptoAmountController.text;
292 -
293 - if (amount != output.cryptoAmount) {
294 - output.setCryptoAmount(amount);
295 - }
296 - });
297 -
298 - _fiatAmountController.addListener(() {
299 - final amount = _fiatAmountController.text;
300 -
301 - if (amount != output.fiatAmount) {
302 - output.setFiatAmount(amount);
303 - }
304 - });
305 -
306 - _addressController.addListener(() {
307 - final address = _addressController.text;
308 -
309 - if (output.address != address) {
310 - output.address = address;
311 - }
312 - });
313 -
314 - _effectsInstalled = true;
172 + final itemCount = controller.page!.round();
173 + return sendTemplateViewModel.recipients[itemCount];
174 }
175 }
lib/src/screens/send/widgets/send_template_card.dart new
+267
@@ -0,0 +1,267 @@
1 +import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
2 +import 'package:cake_wallet/utils/payment_request.dart';
3 +import 'package:cake_wallet/view_model/send/template_view_model.dart';
4 +import 'package:flutter_mobx/flutter_mobx.dart';
5 +import 'package:flutter/material.dart';
6 +import 'package:flutter/services.dart';
7 +import 'package:cake_wallet/generated/i18n.dart';
8 +import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
9 +import 'package:cake_wallet/src/widgets/address_text_field.dart';
10 +import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
11 +import 'package:mobx/mobx.dart';
12 +
13 +class SendTemplateCard extends StatelessWidget {
14 + SendTemplateCard(
15 + {super.key,
16 + required this.template,
17 + required this.index,
18 + required this.sendTemplateViewModel});
19 +
20 + final TemplateViewModel template;
21 + final int index;
22 + final SendTemplateViewModel sendTemplateViewModel;
23 +
24 + final _addressController = TextEditingController();
25 + final _cryptoAmountController = TextEditingController();
26 + final _fiatAmountController = TextEditingController();
27 + final _nameController = TextEditingController();
28 + final FocusNode _cryptoAmountFocus = FocusNode();
29 + final FocusNode _fiatAmountFocus = FocusNode();
30 +
31 + bool _effectsInstalled = false;
32 +
33 + @override
34 + Widget build(BuildContext context) {
35 + _setEffects(context);
36 +
37 + return Container(
38 + decoration: BoxDecoration(
39 + borderRadius: BorderRadius.only(
40 + bottomLeft: Radius.circular(24),
41 + bottomRight: Radius.circular(24)),
42 + gradient: LinearGradient(colors: [
43 + Theme.of(context).primaryTextTheme.titleMedium!.color!,
44 + Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
45 + ], begin: Alignment.topLeft, end: Alignment.bottomRight)),
46 + child: Column(children: <Widget>[
47 + Padding(
48 + padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
49 + child: Column(children: <Widget>[
50 + if (index == 0)
51 + BaseTextFormField(
52 + controller: _nameController,
53 + hintText: sendTemplateViewModel.recipients.length > 1
54 + ? S.of(context).template_name
55 + : S.of(context).send_name,
56 + borderColor: Theme.of(context)
57 + .primaryTextTheme
58 + .headlineSmall!
59 + .color!,
60 + textStyle: TextStyle(
61 + fontSize: 14,
62 + fontWeight: FontWeight.w500,
63 + color: Colors.white),
64 + placeholderTextStyle: TextStyle(
65 + color: Theme.of(context)
66 + .primaryTextTheme
67 + .headlineSmall!
68 + .decorationColor!,
69 + fontWeight: FontWeight.w500,
70 + fontSize: 14),
71 + validator: sendTemplateViewModel.templateValidator),
72 + Padding(
73 + padding: EdgeInsets.only(top: 20),
74 + child: AddressTextField(
75 + selectedCurrency: sendTemplateViewModel.cryptoCurrency,
76 + controller: _addressController,
77 + onURIScanned: (uri) {
78 + final paymentRequest = PaymentRequest.fromUri(uri);
79 + _addressController.text = paymentRequest.address;
80 + _cryptoAmountController.text = paymentRequest.amount;
81 + },
82 + options: [
83 + AddressTextFieldOption.paste,
84 + AddressTextFieldOption.qrCode,
85 + AddressTextFieldOption.addressBook
86 + ],
87 + onPushPasteButton: (context) async {
88 + template.output.resetParsedAddress();
89 + await template.output.fetchParsedAddress(context);
90 + },
91 + onPushAddressBookButton: (context) async {
92 + template.output.resetParsedAddress();
93 + await template.output.fetchParsedAddress(context);
94 + },
95 + buttonColor: Theme.of(context)
96 + .primaryTextTheme
97 + .headlineMedium!
98 + .color!,
99 + borderColor: Theme.of(context)
100 + .primaryTextTheme
101 + .headlineSmall!
102 + .color!,
103 + textStyle: TextStyle(
104 + fontSize: 14,
105 + fontWeight: FontWeight.w500,
106 + color: Colors.white),
107 + hintStyle: TextStyle(
108 + fontSize: 14,
109 + fontWeight: FontWeight.w500,
110 + color: Theme.of(context)
111 + .primaryTextTheme
112 + .headlineSmall!
113 + .decorationColor!),
114 + validator: sendTemplateViewModel.addressValidator)),
115 + Padding(
116 + padding: const EdgeInsets.only(top: 20),
117 + child: Focus(
118 + onFocusChange: (hasFocus) {
119 + if (hasFocus) {
120 + template.selectCurrency();
121 + }
122 + },
123 + child: BaseTextFormField(
124 + focusNode: _cryptoAmountFocus,
125 + controller: _cryptoAmountController,
126 + keyboardType: TextInputType.numberWithOptions(
127 + signed: false, decimal: true),
128 + inputFormatters: [
129 + FilteringTextInputFormatter.deny(
130 + RegExp('[\\-|\\ ]'))
131 + ],
132 + prefixIcon: Observer(
133 + builder: (_) => PrefixCurrencyIcon(
134 + title: sendTemplateViewModel
135 + .cryptoCurrency.title,
136 + isSelected: template.isCurrencySelected)),
137 + hintText: '0.0000',
138 + borderColor: Theme.of(context)
139 + .primaryTextTheme
140 + .headlineSmall!
141 + .color!,
142 + textStyle: TextStyle(
143 + fontSize: 14,
144 + fontWeight: FontWeight.w500,
145 + color: Colors.white),
146 + placeholderTextStyle: TextStyle(
147 + color: Theme.of(context)
148 + .primaryTextTheme
149 + .headlineSmall!
150 + .decorationColor!,
151 + fontWeight: FontWeight.w500,
152 + fontSize: 14),
153 + validator: sendTemplateViewModel.amountValidator))),
154 + Padding(
155 + padding: const EdgeInsets.only(top: 20),
156 + child: Focus(
157 + onFocusChange: (hasFocus) {
158 + if (hasFocus) {
159 + template.selectFiat();
160 + }
161 + },
162 + child: BaseTextFormField(
163 + focusNode: _fiatAmountFocus,
164 + controller: _fiatAmountController,
165 + keyboardType: TextInputType.numberWithOptions(
166 + signed: false, decimal: true),
167 + inputFormatters: [
168 + FilteringTextInputFormatter.deny(
169 + RegExp('[\\-|\\ ]'))
170 + ],
171 + prefixIcon: Observer(
172 + builder: (_) => PrefixCurrencyIcon(
173 + title: sendTemplateViewModel.fiatCurrency,
174 + isSelected: template.isFiatSelected)),
175 + hintText: '0.00',
176 + borderColor: Theme.of(context)
177 + .primaryTextTheme
178 + .headlineSmall!
179 + .color!,
180 + textStyle: TextStyle(
181 + fontSize: 14,
182 + fontWeight: FontWeight.w500,
183 + color: Colors.white),
184 + placeholderTextStyle: TextStyle(
185 + color: Theme.of(context)
186 + .primaryTextTheme
187 + .headlineSmall!
188 + .decorationColor!,
189 + fontWeight: FontWeight.w500,
190 + fontSize: 14))))
191 + ]))
192 + ]));
193 + }
194 +
195 + void _setEffects(BuildContext context) {
196 + if (_effectsInstalled) {
197 + return;
198 + }
199 +
200 + final output = template.output;
201 +
202 + if (template.address.isNotEmpty) {
203 + _addressController.text = template.address;
204 + }
205 + if (template.name.isNotEmpty) {
206 + _nameController.text = template.name;
207 + }
208 + if (template.output.cryptoAmount.isNotEmpty) {
209 + _cryptoAmountController.text = template.output.cryptoAmount;
210 + }
211 + if (template.output.fiatAmount.isNotEmpty) {
212 + _fiatAmountController.text = template.output.fiatAmount;
213 + }
214 +
215 + _addressController.addListener(() {
216 + final address = _addressController.text;
217 +
218 + if (template.address != address) {
219 + template.address = address;
220 + }
221 + });
222 + _cryptoAmountController.addListener(() {
223 + final amount = _cryptoAmountController.text;
224 +
225 + if (amount != output.cryptoAmount) {
226 + output.setCryptoAmount(amount);
227 + }
228 + });
229 + _fiatAmountController.addListener(() {
230 + final amount = _fiatAmountController.text;
231 +
232 + if (amount != output.fiatAmount) {
233 + output.setFiatAmount(amount);
234 + }
235 + });
236 + _nameController.addListener(() {
237 + final name = _nameController.text;
238 +
239 + if (name != template.name) {
240 + template.name = name;
241 + }
242 + });
243 +
244 + reaction((_) => template.address, (String address) {
245 + if (address != _addressController.text) {
246 + _addressController.text = address;
247 + }
248 + });
249 + reaction((_) => output.cryptoAmount, (String amount) {
250 + if (amount != _cryptoAmountController.text) {
251 + _cryptoAmountController.text = amount;
252 + }
253 + });
254 + reaction((_) => output.fiatAmount, (String amount) {
255 + if (amount != _fiatAmountController.text) {
256 + _fiatAmountController.text = amount;
257 + }
258 + });
259 + reaction((_) => template.name, (String name) {
260 + if (name != _nameController.text) {
261 + _nameController.text = name;
262 + }
263 + });
264 +
265 + _effectsInstalled = true;
266 + }
267 +}
lib/src/widgets/template_tile.dart
+44 -40
@@ -8,7 +8,8 @@ class TemplateTile extends StatefulWidget {
8 required this.amount,
9 required this.from,
10 required this.onTap,
11 - required this.onRemove
11 + required this.onRemove,
12 + this.hasMultipleRecipients,
13 }) : super(key: key);
14
15 final String to;
@@ -16,6 +17,7 @@ class TemplateTile extends StatefulWidget {
17 final String from;
18 final VoidCallback onTap;
19 final VoidCallback onRemove;
20 + final bool? hasMultipleRecipients;
21
22 @override
23 TemplateTileState createState() => TemplateTileState(
@@ -51,45 +53,47 @@ class TemplateTileState extends State<TemplateTile> {
53 final toIcon = Image.asset('assets/images/to_icon.png', color: color);
54
55 final content = Row(
54 - mainAxisAlignment: MainAxisAlignment.start,
55 - mainAxisSize: MainAxisSize.min,
56 - children: <Widget>[
57 - Text(
58 - amount,
59 - style: TextStyle(
60 - fontSize: 16,
61 - fontWeight: FontWeight.w600,
62 - color: color
63 - ),
64 - ),
65 - Padding(
66 - padding: EdgeInsets.only(left: 5),
67 - child: Text(
68 - from,
69 - style: TextStyle(
70 - fontSize: 16,
71 - fontWeight: FontWeight.w600,
72 - color: color
73 - ),
74 - ),
75 - ),
76 - Padding(
77 - padding: EdgeInsets.only(left: 5),
78 - child: toIcon,
79 - ),
80 - Padding(
81 - padding: EdgeInsets.only(left: 5),
82 - child: Text(
83 - to,
84 - style: TextStyle(
85 - fontSize: 16,
86 - fontWeight: FontWeight.w600,
87 - color: color
88 - ),
89 - ),
90 - ),
91 - ],
92 - );
56 + mainAxisAlignment: MainAxisAlignment.start,
57 + mainAxisSize: MainAxisSize.min,
58 + children: widget.hasMultipleRecipients ?? false
59 + ? [
60 + Text(
61 + to,
62 + style: TextStyle(
63 + fontSize: 16, fontWeight: FontWeight.w600, color: color),
64 + ),
65 + ]
66 + : [
67 + Text(
68 + amount,
69 + style: TextStyle(
70 + fontSize: 16, fontWeight: FontWeight.w600, color: color),
71 + ),
72 + Padding(
73 + padding: EdgeInsets.only(left: 5),
74 + child: Text(
75 + from,
76 + style: TextStyle(
77 + fontSize: 16,
78 + fontWeight: FontWeight.w600,
79 + color: color),
80 + ),
81 + ),
82 + Padding(
83 + padding: EdgeInsets.only(left: 5),
84 + child: toIcon,
85 + ),
86 + Padding(
87 + padding: EdgeInsets.only(left: 5),
88 + child: Text(
89 + to,
90 + style: TextStyle(
91 + fontSize: 16,
92 + fontWeight: FontWeight.w600,
93 + color: color),
94 + ),
95 + ),
96 + ]);
97
98 final tile = Container(
99 padding: EdgeInsets.only(right: 10),
lib/store/templates/send_template_store.dart
+18 -16
@@ -23,25 +23,27 @@ abstract class SendTemplateBase with Store {
23 templates.replaceRange(0, templates.length, templateSource.values.toList());
24
25 @action
26 - Future<void> addTemplate({
27 - required String name,
28 - required bool isCurrencySelected,
29 - required String address,
30 - required String cryptoCurrency,
31 - required String fiatCurrency,
32 - required String amount,
33 - required String amountFiat}) async {
26 + Future<void> addTemplate(
27 + {required String name,
28 + required bool isCurrencySelected,
29 + required String address,
30 + required String cryptoCurrency,
31 + required String fiatCurrency,
32 + required String amount,
33 + required String amountFiat,
34 + required List<Template> additionalRecipients}) async {
35 final template = Template(
35 - nameRaw: name,
36 - isCurrencySelectedRaw: isCurrencySelected,
37 - addressRaw: address,
38 - cryptoCurrencyRaw: cryptoCurrency,
39 - fiatCurrencyRaw: fiatCurrency,
40 - amountRaw: amount,
41 - amountFiatRaw: amountFiat);
36 + nameRaw: name,
37 + isCurrencySelectedRaw: isCurrencySelected,
38 + addressRaw: address,
39 + cryptoCurrencyRaw: cryptoCurrency,
40 + fiatCurrencyRaw: fiatCurrency,
41 + amountRaw: amount,
42 + amountFiatRaw: amountFiat,
43 + additionalRecipientsRaw: additionalRecipients);
44 await templateSource.add(template);
45 }
46
47 @action
48 Future<void> remove({required Template template}) async => await template.delete();
47 -}
\ No newline at end of file
49 +}
lib/view_model/send/send_template_view_model.dart
+42 -42
@@ -1,4 +1,5 @@
1 -import 'package:cake_wallet/view_model/send/output.dart';
1 +import 'package:cake_wallet/view_model/send/template_view_model.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/wallet_type.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:cake_wallet/entities/template.dart';
@@ -6,10 +7,7 @@ import 'package:cake_wallet/store/templates/send_template_store.dart';
7 import 'package:cake_wallet/core/template_validator.dart';
8 import 'package:cake_wallet/core/address_validator.dart';
9 import 'package:cake_wallet/core/amount_validator.dart';
9 -import 'package:cake_wallet/core/validator.dart';
10 import 'package:cw_core/wallet_base.dart';
11 -import 'package:cw_core/crypto_currency.dart';
12 -import 'package:cake_wallet/entities/fiat_currency.dart';
11 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
12 import 'package:cake_wallet/store/settings_store.dart';
13
@@ -19,72 +17,74 @@ class SendTemplateViewModel = SendTemplateViewModelBase
17 with _$SendTemplateViewModel;
18
19 abstract class SendTemplateViewModelBase with Store {
20 + final WalletBase _wallet;
21 + final SettingsStore _settingsStore;
22 + final SendTemplateStore _sendTemplateStore;
23 + final FiatConversionStore _fiatConversationStore;
24 +
25 SendTemplateViewModelBase(this._wallet, this._settingsStore,
26 this._sendTemplateStore, this._fiatConversationStore)
24 - : output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _wallet.currency) {
25 - output = Output(_wallet, _settingsStore, _fiatConversationStore, () => currency);
27 + : recipients = ObservableList<TemplateViewModel>() {
28 + addRecipient();
29 }
30
28 - Output output;
29 -
30 - Validator get amountValidator =>
31 - AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
32 -
33 - Validator get addressValidator => AddressValidator(type: _wallet.currency);
31 + ObservableList<TemplateViewModel> recipients;
32
35 - Validator get templateValidator => TemplateValidator();
33 + @action
34 + void addRecipient() {
35 + recipients.add(TemplateViewModel(
36 + cryptoCurrency: cryptoCurrency,
37 + wallet: _wallet,
38 + settingsStore: _settingsStore,
39 + fiatConversationStore: _fiatConversationStore));
40 + }
41
37 - CryptoCurrency get currency => _wallet.currency;
42 + @action
43 + void removeRecipient(TemplateViewModel recipient) {
44 + recipients.remove(recipient);
45 + }
46
39 - FiatCurrency get fiat => _settingsStore.fiatCurrency;
47 + AmountValidator get amountValidator =>
48 + AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
49
41 - @observable
42 - bool isCurrencySelected = true;
50 + AddressValidator get addressValidator =>
51 + AddressValidator(type: _wallet.currency);
52
44 - @observable
45 - bool isFiatSelected = false;
53 + TemplateValidator get templateValidator => TemplateValidator();
54
47 - @action
48 - void selectCurrency () {
49 - isCurrencySelected = true;
50 - isFiatSelected = false;
51 - }
55 + @computed
56 + CryptoCurrency get cryptoCurrency => _wallet.currency;
57
53 - @action
54 - void selectFiat () {
55 - isFiatSelected = true;
56 - isCurrencySelected = false;
57 - }
58 + @computed
59 + String get fiatCurrency => _settingsStore.fiatCurrency.title;
60
61 @computed
62 ObservableList<Template> get templates => _sendTemplateStore.templates;
63
62 - final WalletBase _wallet;
63 - final SettingsStore _settingsStore;
64 - final SendTemplateStore _sendTemplateStore;
65 - final FiatConversionStore _fiatConversationStore;
66 -
64 + @action
65 void updateTemplate() => _sendTemplateStore.update();
66
67 + @action
68 void addTemplate(
69 {required String name,
71 - required bool isCurrencySelected,
72 - required String address,
73 - required String cryptoCurrency,
74 - required String fiatCurrency,
75 - required String amount,
76 - required String amountFiat}) {
70 + required bool isCurrencySelected,
71 + required String address,
72 + required String amount,
73 + required String amountFiat,
74 + required List<Template> additionalRecipients}) {
75 _sendTemplateStore.addTemplate(
76 name: name,
77 isCurrencySelected: isCurrencySelected,
78 address: address,
81 - cryptoCurrency: cryptoCurrency,
79 + cryptoCurrency: cryptoCurrency.title,
80 fiatCurrency: fiatCurrency,
81 amount: amount,
84 - amountFiat: amountFiat);
82 + amountFiat: amountFiat,
83 + additionalRecipients: additionalRecipients);
84 updateTemplate();
85 }
86
87 + @action
88 void removeTemplate({required Template template}) {
89 _sendTemplateStore.remove(template: template);
90 updateTemplate();
lib/view_model/send/template_view_model.dart new
+80
@@ -0,0 +1,80 @@
1 +import 'package:cake_wallet/entities/template.dart';
2 +import 'package:cake_wallet/view_model/send/output.dart';
3 +import 'package:mobx/mobx.dart';
4 +import 'package:cw_core/wallet_base.dart';
5 +import 'package:cw_core/crypto_currency.dart';
6 +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
7 +import 'package:cake_wallet/store/settings_store.dart';
8 +
9 +part 'template_view_model.g.dart';
10 +
11 +class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
12 +
13 +abstract class TemplateViewModelBase with Store {
14 + final CryptoCurrency cryptoCurrency;
15 + final WalletBase _wallet;
16 + final SettingsStore _settingsStore;
17 + final FiatConversionStore _fiatConversationStore;
18 +
19 + TemplateViewModelBase(
20 + {required this.cryptoCurrency,
21 + required WalletBase wallet,
22 + required SettingsStore settingsStore,
23 + required FiatConversionStore fiatConversationStore})
24 + : _wallet = wallet,
25 + _settingsStore = settingsStore,
26 + _fiatConversationStore = fiatConversationStore,
27 + output = Output(wallet, settingsStore, fiatConversationStore,
28 + () => wallet.currency) {
29 + output = Output(
30 + _wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency);
31 + }
32 +
33 + @observable
34 + Output output;
35 +
36 + @observable
37 + String name = '';
38 +
39 + @observable
40 + String address = '';
41 +
42 + @observable
43 + bool isCurrencySelected = true;
44 +
45 + @observable
46 + bool isFiatSelected = false;
47 +
48 + @action
49 + void selectCurrency() {
50 + isCurrencySelected = true;
51 + isFiatSelected = false;
52 + }
53 +
54 + @action
55 + void selectFiat() {
56 + isFiatSelected = true;
57 + isCurrencySelected = false;
58 + }
59 +
60 + @action
61 + void reset() {
62 + name = '';
63 + address = '';
64 + isCurrencySelected = true;
65 + isFiatSelected = false;
66 + output.reset();
67 + }
68 +
69 + Template toTemplate(
70 + {required String cryptoCurrency, required String fiatCurrency}) {
71 + return Template(
72 + isCurrencySelectedRaw: isCurrencySelected,
73 + nameRaw: name,
74 + addressRaw: address,
75 + cryptoCurrencyRaw: cryptoCurrency,
76 + fiatCurrencyRaw: fiatCurrency,
77 + amountRaw: output.cryptoAmount,
78 + amountFiatRaw: output.fiatAmount);
79 + }
80 +}
res/values/strings_ar.arb
+2 -1
@@ -635,5 +635,6 @@
635 "generate_name": "توليد الاسم",
636 "balance_page": "صفحة التوازن",
637 "share": "يشارك",
638 - "slidable": "قابل للانزلاق"
638 + "slidable": "قابل للانزلاق",
639 + "template_name": "اسم القالب"
640 }
res/values/strings_bg.arb
+2 -1
@@ -631,5 +631,6 @@
631 "generate_name": "Генериране на име",
632 "balance_page": "Страница за баланс",
633 "share": "Дял",
634 - "slidable": "Плъзгащ се"
634 + "slidable": "Плъзгащ се",
635 + "template_name": "Име на шаблон"
636 }
res/values/strings_cs.arb
+2 -1
@@ -631,5 +631,6 @@
631 "generate_name": "Generovat jméno",
632 "balance_page": "Stránka zůstatku",
633 "share": "Podíl",
634 - "slidable": "Posuvné"
634 + "slidable": "Posuvné",
635 + "template_name": "Název šablony"
636 }
res/values/strings_de.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Namen generieren",
638 "balance_page": "Balance-Seite",
639 "share": "Aktie",
640 - "slidable": "Verschiebbar"
640 + "slidable": "Verschiebbar",
641 + "template_name": "Vorlagenname"
642 }
res/values/strings_en.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Generate Name",
638 "balance_page": "Balance Page",
639 "share": "Share",
640 - "slidable": "Slidable"
640 + "slidable": "Slidable",
641 + "template_name": "Template Name"
642 }
res/values/strings_es.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Generar nombre",
638 "balance_page": "Página de saldo",
639 "share": "Compartir",
640 - "slidable": "deslizable"
640 + "slidable": "deslizable",
641 + "template_name": "Nombre de la plantilla"
642 }
res/values/strings_fr.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Générer un nom",
638 "balance_page": "Page Solde",
639 "share": "Partager",
640 - "slidable": "Glissable"
640 + "slidable": "Glissable",
641 + "template_name": "Nom du modèle"
642 }
res/values/strings_ha.arb
+2 -1
@@ -617,6 +617,7 @@
617 "generate_name": "Ƙirƙirar Suna",
618 "balance_page": "Ma'auni Page",
619 "share": "Raba",
620 - "slidable": "Mai iya zamewa"
620 + "slidable": "Mai iya zamewa",
621 + "template_name": "Sunan Samfura"
622 }
623
res/values/strings_hi.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "नाम जनरेट करें",
638 "balance_page": "बैलेंस पेज",
639 "share": "शेयर करना",
640 - "slidable": "फिसलने लायक"
640 + "slidable": "फिसलने लायक",
641 + "template_name": "टेम्पलेट नाम"
642 }
res/values/strings_hr.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Generiraj ime",
638 "balance_page": "Stranica sa stanjem",
639 "share": "Udio",
640 - "slidable": "Klizna"
640 + "slidable": "Klizna",
641 + "template_name": "Naziv predloška"
642 }
res/values/strings_id.arb
+2 -1
@@ -627,5 +627,6 @@
627 "generate_name": "Hasilkan Nama",
628 "balance_page": "Halaman Saldo",
629 "share": "Membagikan",
630 - "slidable": "Dapat digeser"
630 + "slidable": "Dapat digeser",
631 + "template_name": "Nama Templat"
632 }
res/values/strings_it.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Genera nome",
638 "balance_page": "Pagina di equilibrio",
639 "share": "Condividere",
640 - "slidable": "Scorrevole"
640 + "slidable": "Scorrevole",
641 + "template_name": "Nome modello"
642 }
res/values/strings_ja.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "名前の生成",
638 "balance_page": "残高ページ",
639 "share": "共有",
640 - "slidable": "スライド可能"
640 + "slidable": "スライド可能",
641 + "template_name": "テンプレート名"
642 }
res/values/strings_ko.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "이름 생성",
638 "balance_page": "잔액 페이지",
639 "share": "공유하다",
640 - "slidable": "슬라이딩 가능"
640 + "slidable": "슬라이딩 가능",
641 + "template_name": "템플릿 이름"
642 }
res/values/strings_my.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "အမည်ဖန်တီးပါ။",
638 "balance_page": "လက်ကျန်စာမျက်နှာ",
639 "share": "မျှဝေပါ။",
640 - "slidable": "လျှောချနိုင်သည်။"
640 + "slidable": "လျှောချနိုင်သည်။",
641 + "template_name": "နမူနာပုံစံ"
642 }
res/values/strings_nl.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Naam genereren",
638 "balance_page": "Saldo pagina",
639 "share": "Deel",
640 - "slidable": "Verschuifbaar"
640 + "slidable": "Verschuifbaar",
641 + "template_name": "Sjabloonnaam"
642 }
res/values/strings_pl.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Wygeneruj nazwę",
638 "balance_page": "Strona salda",
639 "share": "Udział",
640 - "slidable": "Przesuwne"
640 + "slidable": "Przesuwne",
641 + "template_name": "Nazwa szablonu"
642 }
res/values/strings_pt.arb
+2 -1
@@ -636,5 +636,6 @@
636 "generate_name": "Gerar nome",
637 "balance_page": "Página de saldo",
638 "share": "Compartilhar",
639 - "slidable": "Deslizável"
639 + "slidable": "Deslizável",
640 + "template_name": "Nome do modelo"
641 }
res/values/strings_ru.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Создать имя",
638 "balance_page": "Страница баланса",
639 "share": "Делиться",
640 - "slidable": "Скользящий"
640 + "slidable": "Скользящий",
641 + "template_name": "Имя Шаблона"
642 }
res/values/strings_th.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "สร้างชื่อ",
638 "balance_page": "หน้ายอดคงเหลือ",
639 "share": "แบ่งปัน",
640 - "slidable": "เลื่อนได้"
640 + "slidable": "เลื่อนได้",
641 + "template_name": "ชื่อแม่แบบ"
642 }
res/values/strings_tr.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "İsim Oluştur",
638 "balance_page": "Bakiye Sayfası",
639 "share": "Paylaşmak",
640 - "slidable": "kaydırılabilir"
640 + "slidable": "kaydırılabilir",
641 + "template_name": "şablon adı"
642 }
res/values/strings_uk.arb
+2 -1
@@ -637,5 +637,6 @@
637 "generate_name": "Згенерувати назву",
638 "balance_page": "Сторінка балансу",
639 "share": "Поділіться",
640 - "slidable": "Розсувний"
640 + "slidable": "Розсувний",
641 + "template_name": "Назва шаблону"
642 }
res/values/strings_ur.arb
+2 -1
@@ -631,5 +631,6 @@
631 "generate_name": "نام پیدا کریں۔",
632 "balance_page": "بیلنس صفحہ",
633 "share": "بانٹیں",
634 - "slidable": "سلائیڈ ایبل"
634 + "slidable": "سلائیڈ ایبل",
635 + "template_name": "ٹیمپلیٹ کا نام"
636 }
res/values/strings_yo.arb
+2 -1
@@ -633,5 +633,6 @@
633 "generate_name": "Ṣẹda Orukọ",
634 "balance_page": "Oju-iwe iwọntunwọnsi",
635 "share": "Pinpin",
636 - "slidable": "Slidable"
636 + "slidable": "Slidable",
637 + "template_name": "Orukọ Awoṣe"
638 }
res/values/strings_zh.arb
+2 -1
@@ -636,5 +636,6 @@
636 "generate_name": "生成名称",
637 "balance_page": "余额页",
638 "share": "分享",
639 - "slidable": "可滑动"
639 + "slidable": "可滑动",
640 + "template_name": "模板名称"
641 }