dev
dart 491 lines 20.3 KB
Raw
1 import 'package:cake_wallet/core/amount_validator.dart';
2 import 'package:cake_wallet/entities/contact_base.dart';
3 import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
4 import 'package:cake_wallet/routes.dart';
5 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
6 import 'package:cake_wallet/utils/show_bar.dart';
7 import 'package:cake_wallet/utils/show_pop_up.dart';
8 import 'package:cake_wallet/utils/payment_request.dart';
9 import 'package:cw_core/currency.dart';
10 import 'package:flutter/services.dart';
11 import 'package:flutter/material.dart';
12 import 'package:cake_wallet/generated/i18n.dart';
13 import 'package:cake_wallet/src/widgets/address_text_field.dart';
14 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
15 import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
16
17 class ExchangeCard<T extends Currency> extends StatefulWidget {
18 ExchangeCard({
19 Key? key,
20 required this.initialCurrency,
21 required this.initialAddress,
22 required this.initialWalletName,
23 required this.initialIsAmountEditable,
24 required this.isAmountEstimated,
25 required this.currencies,
26 required this.onCurrencySelected,
27 required this.fillColor,
28 this.imageArrow,
29 this.currencyValueValidator,
30 this.addressTextFieldValidator,
31 this.title = '',
32 this.initialIsAddressEditable = true,
33 this.hasRefundAddress = false,
34 this.isMoneroWallet = false,
35 this.currencyButtonColor = Colors.transparent,
36 this.addressButtonsColor = Colors.transparent,
37 this.borderColor = Colors.transparent,
38 this.hasAllAmount = false,
39 this.isAllAmountEnabled = false,
40 this.showAddressField = true,
41 this.showLimitsField = true,
42 this.amountFocusNode,
43 this.addressFocusNode,
44 this.allAmount,
45 this.currencyRowPadding,
46 this.addressRowPadding,
47 this.onPushPasteButton,
48 this.onPushAddressBookButton,
49 this.onDispose,
50 this.useSatoshis = false,
51 this.onTapCurrencyPicker,
52 required this.cardInstanceName,
53 }) : super(key: key);
54
55 final List<T> currencies;
56 final Function(T) onCurrencySelected;
57 final String title;
58 final T initialCurrency;
59 final String initialWalletName;
60 final String initialAddress;
61 final bool initialIsAmountEditable;
62 final bool initialIsAddressEditable;
63 final bool isAmountEstimated;
64 final bool hasRefundAddress;
65 final bool isMoneroWallet;
66 final Image? imageArrow;
67 final Color currencyButtonColor;
68 final Color? addressButtonsColor;
69 final Color borderColor;
70 final FormFieldValidator<String>? currencyValueValidator;
71 final FormFieldValidator<String>? addressTextFieldValidator;
72 final FormFieldValidator<String> allAmountValidator = AllAmountValidator();
73 final FocusNode? amountFocusNode;
74 final FocusNode? addressFocusNode;
75 final bool hasAllAmount;
76 final bool showAddressField;
77 final bool showLimitsField;
78 final bool isAllAmountEnabled;
79 final VoidCallback? allAmount;
80 final EdgeInsets? currencyRowPadding;
81 final EdgeInsets? addressRowPadding;
82 final void Function(BuildContext context)? onPushPasteButton;
83 final void Function(BuildContext context)? onPushAddressBookButton;
84 final Function()? onDispose;
85 final String cardInstanceName;
86 final Color fillColor;
87 final bool useSatoshis;
88 final void Function(BuildContext context)? onTapCurrencyPicker;
89
90 @override
91 ExchangeCardState<T> createState() => ExchangeCardState<T>();
92 }
93
94 class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
95 ExchangeCardState()
96 : _title = '',
97 _min = '',
98 _max = '',
99 _isAmountEditable = false,
100 _isAddressEditable = false,
101 _walletName = '',
102 _isAmountEstimated = false,
103 _isMoneroWallet = false,
104 _cardInstanceName = '';
105
106 final addressController = TextEditingController();
107 final amountController = TextEditingController();
108
109 String _cardInstanceName;
110 String _title;
111 String? _min;
112 String? _max;
113 late T _selectedCurrency;
114 String _walletName;
115 bool _isAmountEditable;
116 bool _isAddressEditable;
117 bool _isAmountEstimated;
118 bool _isMoneroWallet;
119
120 @override
121 void initState() {
122 _cardInstanceName = widget.cardInstanceName;
123 _title = widget.title;
124 _isAmountEditable = widget.initialIsAmountEditable;
125 _isAddressEditable = widget.initialIsAddressEditable;
126 _walletName = widget.initialWalletName;
127 _selectedCurrency = widget.initialCurrency;
128 _isAmountEstimated = widget.isAmountEstimated;
129 _isMoneroWallet = widget.isMoneroWallet;
130 addressController.text = _normalizeAddressFormat(widget.initialAddress);
131
132 super.initState();
133 }
134
135 @override
136 void dispose() {
137 widget.onDispose?.call();
138
139 super.dispose();
140 }
141
142 void changeLimits({String? min, String? max}) {
143 setState(() {
144 _min = min;
145 _max = max;
146 });
147 }
148
149 void changeSelectedCurrency(T currency) {
150 setState(() => _selectedCurrency = currency);
151 }
152
153 void changeWalletName(String walletName) {
154 setState(() => _walletName = walletName);
155 }
156
157 void changeIsAction(bool isActive) {
158 setState(() => _isAmountEditable = isActive);
159 }
160
161 void isAmountEditable({bool isEditable = true}) {
162 setState(() => _isAmountEditable = isEditable);
163 }
164
165 void isAddressEditable({bool isEditable = true}) {
166 setState(() => _isAddressEditable = isEditable);
167 }
168
169 void changeAddress({required String address}) {
170 setState(() => addressController.text = _normalizeAddressFormat(address));
171 }
172
173 void changeAmount({required String amount}) {
174 setState(() => amountController.text = amount);
175 }
176
177 void changeIsAmountEstimated(bool isEstimated) {
178 setState(() => _isAmountEstimated = isEstimated);
179 }
180
181 @override
182 Widget build(BuildContext context) {
183 if (widget.isAllAmountEnabled) {
184 WidgetsBinding.instance.addPostFrameCallback((_) {
185 amountController.text = S.of(context).all;
186 });
187 }
188
189 final copyImage = Image.asset(
190 'assets/images/copy_content.png',
191 height: 16,
192 width: 16,
193 color: Theme.of(context).colorScheme.onSurface,
194 );
195
196 return Container(
197 width: double.infinity,
198 color: Colors.transparent,
199 child: Column(
200 crossAxisAlignment: CrossAxisAlignment.start,
201 children: <Widget>[
202 SizedBox(height: 10),
203 Row(
204 mainAxisAlignment: MainAxisAlignment.start,
205 children: <Widget>[
206 SizedBox(height: 40),
207 Text(
208 key: ValueKey('${_cardInstanceName}_title_key'),
209 _title,
210 style: Theme.of(context).textTheme.titleLarge!.copyWith(
211 fontSize: 18,
212 fontWeight: FontWeight.w600,
213 color: Theme.of(context).colorScheme.onSurfaceVariant,
214 ),
215 )
216 ],
217 ),
218 CurrencyAmountTextField(
219 hasUnderlineBorder: true,
220 borderWidth: 0.0,
221 padding: EdgeInsets.zero,
222 currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'),
223 selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'),
224 selectedCurrencyTagTextKey:
225 ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'),
226 amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'),
227 sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'),
228 currencyAmountTextFieldWidgetKey:
229 ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'),
230 imageArrow: widget.imageArrow,
231 selectedCurrency: widget.useSatoshis ? "SATS" : "$_selectedCurrency",
232 selectedCurrencyDecimals: widget.useSatoshis ? 0 : _selectedCurrency.decimals,
233 amountFocusNode: widget.amountFocusNode,
234 amountController: amountController,
235 onTapPicker: () => _presentPicker(context),
236 isAmountEditable: _isAmountEditable,
237 isPickerEnable: true,
238 allAmountButton: widget.hasAllAmount,
239 currencyValueValidator: widget.currencyValueValidator,
240 tag: _selectedCurrency.tag,
241 allAmountCallback: widget.allAmount,
242 fillColor: widget.fillColor,
243 ),
244 Divider(height: 1, color: Theme.of(context).colorScheme.outlineVariant),
245 Padding(
246 padding: EdgeInsets.only(top: 5),
247 child: widget.showLimitsField
248 ? Container(
249 height: 15,
250 child: Row(
251 mainAxisAlignment: MainAxisAlignment.start,
252 children: <Widget>[
253 _min != null
254 ? Text(
255 key: ValueKey('${_cardInstanceName}_min_limit_text_key'),
256 S.of(context).min_value(_min ?? '', _selectedCurrency.toString()),
257 style: Theme.of(context).textTheme.bodySmall!.copyWith(
258 fontSize: 10,
259 height: 1.2,
260 color: Theme.of(context).colorScheme.onSurfaceVariant,
261 ),
262 )
263 : Offstage(),
264 _min != null ? SizedBox(width: 10) : Offstage(),
265 _max != null
266 ? Text(
267 key: ValueKey('${_cardInstanceName}_max_limit_text_key'),
268 S.of(context).max_value(_max ?? '', _selectedCurrency.toString()),
269 style: Theme.of(context).textTheme.bodySmall!.copyWith(
270 fontSize: 10,
271 height: 1.2,
272 color: Theme.of(context).colorScheme.onSurfaceVariant,
273 ),
274 )
275 : Offstage(),
276 ],
277 ),
278 )
279 : Offstage(),
280 ),
281 !_isAddressEditable && widget.hasRefundAddress
282 ? Padding(
283 padding: EdgeInsets.only(top: 20),
284 child: Text(
285 S.of(context).refund_address,
286 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
287 fontWeight: FontWeight.w500,
288 color: Theme.of(context).colorScheme.onSurfaceVariant,
289 ),
290 ),
291 )
292 : Offstage(),
293 _isAddressEditable
294 ? widget.showAddressField
295 ? FocusTraversalOrder(
296 order: NumericFocusOrder(2),
297 child: Padding(
298 padding: widget.addressRowPadding ?? EdgeInsets.only(top: 12),
299 child: AddressTextField(
300 hasUnderlineBorder: true,
301 borderWidth: 0.0,
302 addressKey:
303 ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
304 focusNode: widget.addressFocusNode,
305 controller: addressController,
306 onURIScanned: (uri) {
307 final paymentRequest = PaymentRequest.fromUri(uri);
308 addressController.text = paymentRequest.address;
309
310 if (amountController.text.isNotEmpty &&
311 paymentRequest.amount.isNotEmpty) {
312 _showAmountPopup(context, paymentRequest);
313 return;
314 }
315 widget.amountFocusNode?.requestFocus();
316 amountController.text = paymentRequest.amount;
317 },
318 placeholder:
319 widget.hasRefundAddress ? S.of(context).refund_address : null,
320 options: [
321 AddressTextFieldOption.paste,
322 AddressTextFieldOption.qrCode,
323 AddressTextFieldOption.addressBook,
324 ],
325 textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
326 fontSize: 16,
327 fontWeight: FontWeight.w600,
328 color: Theme.of(context).colorScheme.onSurface,
329 ),
330 hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
331 fontSize: 16,
332 fontWeight: FontWeight.w600,
333 color: Theme.of(context).colorScheme.onSurfaceVariant,
334 ),
335 buttonColor: widget.addressButtonsColor,
336 validator: widget.addressTextFieldValidator,
337 onPushPasteButton: widget.onPushPasteButton,
338 onPushAddressBookButton: widget.onPushAddressBookButton,
339 selectedCurrency: _selectedCurrency,
340 fillColor: widget.fillColor,
341 iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
342 ),
343 ),
344 )
345 : Offstage()
346 : Padding(
347 padding: EdgeInsets.only(top: 0),
348 child: Builder(
349 builder: (context) => Stack(
350 children: <Widget>[
351 FocusTraversalOrder(
352 order: NumericFocusOrder(3),
353 child: BaseTextFormField(
354 hasUnderlineBorder: true,
355 borderWidth: 0.0,
356 key:
357 ValueKey('${_cardInstanceName}_non_editable_address_textfield_key'),
358 controller: addressController,
359 suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36),
360 textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
361 fontSize: 16,
362 fontWeight: FontWeight.w600,
363 ),
364 validator: widget.addressTextFieldValidator,
365 fillColor: widget.fillColor,
366 ),
367 ),
368 Positioned(
369 top: 2,
370 right: 0,
371 child: SizedBox(
372 width: _isMoneroWallet ? 80 : 36,
373 child: Row(
374 children: <Widget>[
375 if (_isMoneroWallet)
376 Padding(
377 padding: EdgeInsets.only(left: 10),
378 child: Container(
379 width: 34,
380 height: 34,
381 padding: EdgeInsets.only(top: 0),
382 child: Semantics(
383 label: S.of(context).address_book,
384 child: InkWell(
385 key: ValueKey(
386 '${_cardInstanceName}_address_book_button_key'),
387 onTap: () async {
388 final contact = await Navigator.of(context).pushNamed(
389 Routes.pickerAddressBook,
390 arguments: [widget.initialCurrency, true],
391 );
392
393 if (contact is ContactBase) {
394 setState(
395 () => addressController.text = contact.address);
396 widget.onPushAddressBookButton?.call(context);
397 }
398 },
399 child: Container(
400 padding: EdgeInsets.all(8),
401 decoration: BoxDecoration(
402 color: widget.addressButtonsColor,
403 borderRadius: BorderRadius.all(Radius.circular(6))),
404 child: Image.asset(
405 'assets/images/open_book.png',
406 color: Theme.of(context).colorScheme.onSurface,
407 ),
408 ),
409 ),
410 ),
411 ),
412 ),
413 Padding(
414 padding: EdgeInsets.only(left: 2),
415 child: Container(
416 width: 34,
417 height: 34,
418 padding: EdgeInsets.only(top: 0),
419 child: Semantics(
420 label: S.of(context).copy_address,
421 child: InkWell(
422 key: ValueKey(
423 '${_cardInstanceName}_copy_refund_address_button_key'),
424 onTap: () {
425 Clipboard.setData(
426 ClipboardData(text: addressController.text));
427 showBar<void>(context, S.of(context).copied_to_clipboard);
428 },
429 child: Container(
430 padding: EdgeInsets.fromLTRB(8, 8, 0, 8),
431 color: Colors.transparent,
432 child: copyImage,
433 ),
434 ),
435 ),
436 ),
437 )
438 ],
439 ),
440 ),
441 )
442 ],
443 ),
444 ),
445 ),
446 ],
447 ),
448 );
449 }
450
451 void _presentPicker(BuildContext context) {
452 if (widget.onTapCurrencyPicker != null) {
453 widget.onTapCurrencyPicker!(context);
454 return;
455 }
456 showPopUp<void>(
457 context: context,
458 builder: (_) => CurrencyPicker(
459 key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'),
460 selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
461 items: widget.currencies,
462 hintText: S.of(context).search_currency,
463 isConvertFrom: widget.hasRefundAddress,
464 onItemSelected: (Currency item) => widget.onCurrencySelected(item as T),
465 ),
466 );
467 }
468
469 void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) {
470 showPopUp<void>(
471 context: context,
472 builder: (dialogContext) {
473 return AlertWithTwoActions(
474 alertTitle: S.of(dialogContext).overwrite_amount,
475 alertContent: S.of(dialogContext).qr_payment_amount,
476 rightButtonText: S.of(dialogContext).ok,
477 leftButtonText: S.of(dialogContext).cancel,
478 actionRightButton: () {
479 widget.amountFocusNode?.requestFocus();
480 amountController.text = paymentRequest.amount;
481 Navigator.of(dialogContext).pop();
482 },
483 actionLeftButton: () => Navigator.of(dialogContext).pop());
484 });
485 }
486
487 String _normalizeAddressFormat(String address) {
488 if (address.startsWith('bitcoincash:')) address = address.substring(12);
489 return address;
490 }
491 }