dev
dart 518 lines 18.8 KB
Raw
1 import 'package:cake_wallet/core/amount_parsing_proxy.dart';
2 import 'package:cake_wallet/core/address_validator.dart';
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/src/widgets/rounded_icon_button.dart';
5 import 'package:cake_wallet/themes/core/theme_extension.dart';
6 import 'package:cake_wallet/utils/address_formatter.dart';
7 import 'package:cake_wallet/utils/image_utill.dart';
8 import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
9 import 'package:cake_wallet/view_model/send/output.dart';
10 import 'package:cw_core/amount/amount_sanitizer.dart';
11 import 'package:cw_core/crypto_currency.dart';
12 import 'package:cw_core/pending_transaction.dart';
13 import 'package:cw_core/wallet_type.dart';
14 import 'package:flutter/material.dart';
15 import 'package:flutter/services.dart';
16 import 'package:flutter_mobx/flutter_mobx.dart';
17
18 import 'base_bottom_sheet_widget.dart';
19
20 class ConfirmSendingBottomSheet extends BaseBottomSheet {
21 ConfirmSendingBottomSheet({
22 required String titleText,
23 required FooterType footerType,
24 String? titleIconPath,
25 String? slideActionButtonText,
26 VoidCallback? onSlideActionComplete,
27 bool isSlideActionEnabled = true,
28 String? accessibleNavigationModeSlideActionButtonText,
29 required this.currency,
30 this.paymentId,
31 this.paymentIdValue,
32 this.expirationTime,
33 required this.amount,
34 required this.amountValue,
35 required this.fiatAmountValue,
36 required this.fee,
37 required this.feeValue,
38 required this.feeFiatAmount,
39 required this.outputs,
40 required this.walletType,
41 this.amountParsingProxy,
42 this.change,
43 this.explanation,
44 this.isOpenCryptoPay = false,
45 this.cakePayBuyCardViewModel,
46 this.quantity,
47 Key? key,
48 }) : showScrollbar = outputs.length > 3,
49 super(
50 titleText: titleText,
51 maxHeight: 900,
52 titleIconPath: titleIconPath,
53 footerType: footerType,
54 slideActionButtonText: slideActionButtonText ?? 'Swipe to send',
55 onSlideActionComplete: onSlideActionComplete,
56 isSlideActionEnabled: isSlideActionEnabled,
57 accessibleNavigationModeSlideActionButtonText:
58 accessibleNavigationModeSlideActionButtonText,
59 key: key);
60
61 final CryptoCurrency currency;
62 final String? paymentId;
63 final String? paymentIdValue;
64 final String? expirationTime;
65 final String amount;
66 final String amountValue;
67 final String fiatAmountValue;
68 final String fee;
69 final String feeValue;
70 final String feeFiatAmount;
71 final List<Output> outputs;
72 final WalletType walletType;
73 final PendingChange? change;
74 final bool isOpenCryptoPay;
75 final CakePayBuyCardViewModel? cakePayBuyCardViewModel;
76 final String? quantity;
77 final String? explanation;
78 final AmountParsingProxy? amountParsingProxy;
79
80 final bool showScrollbar;
81 final ScrollController scrollController = ScrollController();
82
83 bool get showAddress => !outputs
84 .any((e) => RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()));
85
86 @override
87 Widget contentWidget(BuildContext context) {
88 final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
89 fontSize: 16,
90 fontWeight: FontWeight.w500,
91 decoration: TextDecoration.none,
92 );
93 final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith(
94 fontWeight: FontWeight.w600,
95 color: Theme.of(context).colorScheme.onSurfaceVariant,
96 decoration: TextDecoration.none,
97 );
98
99 final tileBackgroundColor = context.currentTheme.isDark
100 ? context.customColors.backgroundGradientColor.withAlpha(140)
101 : context.customColors.cardGradientColorPrimary;
102
103 Widget content = Padding(
104 padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8),
105 child: Column(
106 children: [
107 if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null)
108 Padding(
109 padding: const EdgeInsets.only(bottom: 8),
110 child: Observer(
111 builder: (_) => AddressTile(
112 itemTitle: paymentId!,
113 itemTitleTextStyle: itemTitleTextStyle,
114 amountTextStyle: itemSubTitleTextStyle,
115 walletType: walletType,
116 amount: expirationTime != null
117 ? S.current.offer_expires_in +
118 ' ${cakePayBuyCardViewModel!.formattedRemainingTime}'
119 : null,
120 address: paymentIdValue!,
121 itemSubTitleTextStyle: itemSubTitleTextStyle,
122 tileBackgroundColor: tileBackgroundColor,
123 applyAddressFormatting: false,
124 copyButton: true,
125 )),
126 ),
127 if (explanation != null)
128 Padding(
129 padding: const EdgeInsets.only(bottom: 8),
130 child: StandardInfoTile(
131 value: explanation!,
132 itemTitleTextStyle: itemTitleTextStyle,
133 tileBackgroundColor: tileBackgroundColor,
134 ),
135 ),
136 StandardTile(
137 itemTitle: amount,
138 itemValue: amountValue,
139 itemTitleTextStyle: itemTitleTextStyle,
140 itemSubTitle: fiatAmountValue,
141 itemSubTitleTextStyle: itemSubTitleTextStyle,
142 tileBackgroundColor: tileBackgroundColor,
143 ),
144 const SizedBox(height: 8),
145 StandardTile(
146 itemTitle: fee,
147 itemValue: feeValue,
148 itemTitleTextStyle: itemTitleTextStyle,
149 itemSubTitle: feeFiatAmount,
150 itemSubTitleTextStyle: itemSubTitleTextStyle,
151 tileBackgroundColor: tileBackgroundColor,
152 ),
153 const SizedBox(height: 8),
154 Column(
155 children: [
156 if (showAddress)
157 ListView.separated(
158 padding: const EdgeInsets.only(top: 0),
159 shrinkWrap: true,
160 physics: const NeverScrollableScrollPhysics(),
161 itemCount: outputs.length,
162 separatorBuilder: (_, __) => const SizedBox(height: 8),
163 itemBuilder: (context, index) {
164 final isBatchSending = outputs.length > 1;
165 final item = outputs[index];
166 final contactName = item.parsedAddress.profileName;
167 final isCakePayName = contactName == 'Cake Pay';
168 final batchContactTitle =
169 '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
170 final _address = item.isParsedAddress ? item.extractedAddress : item.address;
171 final _amount =
172 '${item.cryptoAmount.sanitized()} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}';
173 return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
174 ? ExpansionAddressTile(
175 contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
176 name: isBatchSending ? batchContactTitle : contactName,
177 address: _address,
178 amount: _amount,
179 walletType: walletType,
180 isBatchSending: isBatchSending,
181 itemTitleTextStyle: itemTitleTextStyle,
182 itemSubTitleTextStyle: itemSubTitleTextStyle,
183 tileBackgroundColor: tileBackgroundColor,
184 )
185 : AddressTile(
186 itemTitle: isCakePayName
187 ? item.parsedAddress.profileName
188 : S.of(context).address,
189 imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null,
190 itemTitleTextStyle: itemTitleTextStyle,
191 walletType: walletType,
192 amount: isCakePayName ? item.fiatAmount : _amount,
193 address: _address,
194 itemSubTitle: isCakePayName ? quantity : null,
195 itemSubTitleTextStyle: itemSubTitleTextStyle,
196 tileBackgroundColor: tileBackgroundColor,
197 );
198 },
199 ),
200 if (change != null)
201 Padding(
202 padding: const EdgeInsets.only(top: 8),
203 child: ExpansionAddressTile(
204 contactType: 'Change',
205 name: S.of(context).send_change_to_you,
206 address: change!.address,
207 amount:
208 '${amountParsingProxy?.getDisplayCryptoString(change!.amount.toInt(), currency)} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}',
209 isBatchSending: true,
210 walletType: walletType,
211 itemTitleTextStyle: itemTitleTextStyle,
212 itemSubTitleTextStyle: itemSubTitleTextStyle,
213 tileBackgroundColor: tileBackgroundColor,
214 ),
215 ),
216 ],
217 ),
218 const SizedBox(height: 8),
219 ],
220 ),
221 );
222
223 if (showScrollbar) {
224 return SizedBox(
225 height: 380,
226 child: Scrollbar(
227 controller: scrollController,
228 thumbVisibility: true,
229 child: SingleChildScrollView(
230 controller: scrollController,
231 physics: const BouncingScrollPhysics(),
232 child: content,
233 ),
234 ),
235 );
236 } else {
237 return content;
238 }
239 }
240 }
241
242 class StandardTile extends StatelessWidget {
243 const StandardTile({
244 super.key,
245 required this.itemTitle,
246 required this.itemValue,
247 required this.itemTitleTextStyle,
248 this.itemSubTitle,
249 required this.itemSubTitleTextStyle,
250 required this.tileBackgroundColor,
251 });
252
253 final String itemTitle;
254 final String itemValue;
255 final TextStyle itemTitleTextStyle;
256 final String? itemSubTitle;
257 final TextStyle itemSubTitleTextStyle;
258 final Color tileBackgroundColor;
259
260 @override
261 Widget build(BuildContext context) {
262 return Semantics(
263 container: true,
264 label: itemTitle,
265 child: Container(
266 padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
267 decoration: BoxDecoration(
268 borderRadius: BorderRadius.circular(10),
269 color: tileBackgroundColor,
270 ),
271 child: Row(
272 mainAxisAlignment: MainAxisAlignment.spaceBetween,
273 children: [
274 Text(itemTitle, style: itemTitleTextStyle),
275 Column(
276 crossAxisAlignment: CrossAxisAlignment.end,
277 children: [
278 Text(itemValue, style: itemTitleTextStyle),
279 itemSubTitle == null
280 ? Container()
281 : Text(itemSubTitle!, style: itemSubTitleTextStyle),
282 ],
283 ),
284 ],
285 ),
286 ),
287 );
288 }
289 }
290
291 class StandardInfoTile extends StatelessWidget {
292 const StandardInfoTile({
293 super.key,
294 required this.value,
295 required this.itemTitleTextStyle,
296 required this.tileBackgroundColor,
297 });
298
299 final String value;
300 final TextStyle itemTitleTextStyle;
301 final Color tileBackgroundColor;
302
303 @override
304 Widget build(BuildContext context) => Semantics(
305 container: true,
306 label: value,
307 child: Container(
308 padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
309 decoration: BoxDecoration(
310 borderRadius: BorderRadius.circular(10),
311 color: tileBackgroundColor,
312 ),
313 width: double.infinity,
314 child: Text(value, style: itemTitleTextStyle),
315 ),
316 );
317 }
318
319 class AddressTile extends StatelessWidget {
320 const AddressTile(
321 {super.key,
322 required this.itemTitle,
323 required this.itemTitleTextStyle,
324 required this.address,
325 required this.itemSubTitleTextStyle,
326 required this.tileBackgroundColor,
327 required this.walletType,
328 this.amountTextStyle,
329 this.applyAddressFormatting = true,
330 this.imagePath,
331 this.amount,
332 this.itemSubTitle,
333 this.copyButton = false});
334
335 final String itemTitle;
336 final TextStyle itemTitleTextStyle;
337 final String? amount;
338 final String address;
339 final TextStyle itemSubTitleTextStyle;
340 final TextStyle? amountTextStyle;
341 final Color tileBackgroundColor;
342 final WalletType walletType;
343 final bool applyAddressFormatting;
344 final String? imagePath;
345 final String? itemSubTitle;
346 final bool copyButton;
347
348 @override
349 Widget build(BuildContext context) {
350 return Container(
351 padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
352 decoration: BoxDecoration(
353 borderRadius: BorderRadius.circular(10),
354 color: tileBackgroundColor,
355 ),
356 child: Column(
357 crossAxisAlignment: CrossAxisAlignment.start,
358 children: [
359 Row(
360 mainAxisAlignment: MainAxisAlignment.spaceBetween,
361 children: [
362 Expanded(
363 child: Row(
364 children: [
365 if (imagePath != null)
366 Padding(
367 padding: const EdgeInsets.only(right: 8),
368 child: ClipRRect(
369 borderRadius: BorderRadius.circular(12),
370 child: ImageUtil.getImageFromPath(
371 imagePath: imagePath!, height: 40, width: 40),
372 ),
373 ),
374 Flexible(
375 child: Padding(
376 padding: const EdgeInsets.only(right: 8),
377 child: Text(
378 itemTitle,
379 style: itemTitleTextStyle,
380 overflow: TextOverflow.ellipsis,
381 maxLines: 1,
382 softWrap: false,
383 )),
384 ),
385 ],
386 ),
387 ),
388 if (amount != null) Text(amount!, style: amountTextStyle ?? itemTitleTextStyle),
389 ],
390 ),
391 address.isEmpty
392 ? Container()
393 : applyAddressFormatting
394 ? AddressFormatter.buildSegmentedAddress(
395 address: address,
396 walletType: walletType,
397 evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
398 fontWeight: FontWeight.w600,
399 decoration: TextDecoration.none,
400 ))
401 : Row(
402 mainAxisAlignment: MainAxisAlignment.spaceBetween,
403 children: [
404 Text(address,
405 style: copyButton
406 ? itemTitleTextStyle.copyWith(fontSize: 12)
407 : itemTitleTextStyle),
408 SizedBox(width: 8),
409 if (copyButton)
410 RoundedIconButton(
411 icon: Icons.copy_all_outlined,
412 onPressed: () async =>
413 await Clipboard.setData(ClipboardData(text: address)),
414 shape: RoundedRectangleBorder(
415 borderRadius: BorderRadius.circular(5),
416 ),
417 ),
418 ],
419 ),
420 itemSubTitle == null
421 ? Container()
422 : Row(
423 mainAxisAlignment: MainAxisAlignment.end,
424 children: [
425 Text(itemSubTitle!, style: itemSubTitleTextStyle),
426 ],
427 ),
428 ],
429 ),
430 );
431 }
432 }
433
434 class ExpansionAddressTile extends StatelessWidget {
435 const ExpansionAddressTile({
436 super.key,
437 required this.contactType,
438 required this.name,
439 required this.address,
440 required this.amount,
441 required this.isBatchSending,
442 required this.itemTitleTextStyle,
443 required this.itemSubTitleTextStyle,
444 required this.tileBackgroundColor,
445 required this.walletType,
446 });
447
448 final String contactType;
449 final String name;
450 final String address;
451 final String amount;
452 final bool isBatchSending;
453 final TextStyle itemTitleTextStyle;
454 final TextStyle itemSubTitleTextStyle;
455 final Color tileBackgroundColor;
456 final WalletType walletType;
457
458 @override
459 Widget build(BuildContext context) {
460 return Semantics(
461 container: true,
462 label: name,
463 child: Container(
464 decoration: BoxDecoration(
465 borderRadius: BorderRadius.all(Radius.circular(10)),
466 color: tileBackgroundColor,
467 ),
468 child: Theme(
469 data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
470 child: Padding(
471 padding: EdgeInsets.symmetric(horizontal: 14, vertical: isBatchSending ? 0 : 8),
472 child: ExpansionTile(
473 childrenPadding: isBatchSending ? const EdgeInsets.only(bottom: 8) : EdgeInsets.zero,
474 tilePadding: EdgeInsets.zero,
475 dense: true,
476 iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
477 visualDensity: VisualDensity.compact,
478 title: Row(
479 mainAxisAlignment: MainAxisAlignment.spaceBetween,
480 children: [
481 Expanded(
482 child: Text(
483 isBatchSending ? name : contactType,
484 style: itemTitleTextStyle,
485 softWrap: true,
486 ),
487 ),
488 Text(
489 isBatchSending ? amount : name,
490 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
491 fontWeight: FontWeight.w600,
492 decoration: TextDecoration.none,
493 ),
494 ),
495 ],
496 ),
497 children: [
498 Row(
499 children: [
500 Expanded(
501 child: AddressFormatter.buildSegmentedAddress(
502 address: address,
503 walletType: walletType,
504 evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
505 fontWeight: FontWeight.w600,
506 ),
507 ),
508 ),
509 ],
510 ),
511 ],
512 ),
513 ),
514 ),
515 ),
516 );
517 }
518 }