| 1 | import "package:cake_wallet/core/amount_validator.dart"; |
| 2 | import "package:cake_wallet/entities/qr_scanner.dart"; |
| 3 | import "package:cake_wallet/generated/i18n.dart"; |
| 4 | import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart"; |
| 5 | import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart"; |
| 6 | import "package:cake_wallet/new-ui/widgets/modern_button.dart"; |
| 7 | import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart"; |
| 8 | import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart"; |
| 9 | import "package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart"; |
| 10 | import "package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart"; |
| 11 | import "package:cake_wallet/new-ui/widgets/swap_page/swap_source_selector.dart"; |
| 12 | import "package:cake_wallet/src/widgets/cake_image_widget.dart"; |
| 13 | import "package:cake_wallet/utils/decimal_input_formatter.dart"; |
| 14 | import "package:cake_wallet/utils/permission_handler.dart"; |
| 15 | import "package:cake_wallet/view_model/exchange/exchange_view_model.dart"; |
| 16 | import "package:cake_wallet/view_model/wallet_switcher_view_model.dart"; |
| 17 | import "package:cw_core/crypto_currency.dart"; |
| 18 | import "package:cw_core/currencies_with_memo.dart"; |
| 19 | import "package:cw_core/currency.dart"; |
| 20 | import "package:cw_core/wallet_info.dart"; |
| 21 | import "package:cw_core/wallet_type.dart"; |
| 22 | import "package:flutter/material.dart"; |
| 23 | import "package:flutter/services.dart"; |
| 24 | import "package:flutter_mobx/flutter_mobx.dart"; |
| 25 | import "package:mobx/mobx.dart" as mobx; |
| 26 | import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; |
| 27 | import "package:permission_handler/permission_handler.dart"; |
| 28 | |
| 29 | class SwapAmountBox extends StatefulWidget { |
| 30 | SwapAmountBox({ |
| 31 | required this.exchangeViewModel, |
| 32 | required this.isReceiverCard, |
| 33 | required this.walletSwitcherViewModel, |
| 34 | required this.currency, |
| 35 | required this.currencies, |
| 36 | required this.onCurrencySelected, |
| 37 | this.currencyValueValidator, |
| 38 | this.addressTextFieldValidator, |
| 39 | this.title = "", |
| 40 | this.hasRefundAddress = false, |
| 41 | this.hasAllAmount = false, |
| 42 | this.useBaseUnit = false, |
| 43 | this.allAmount, |
| 44 | this.onPushPasteButton, |
| 45 | this.onPushAddressBookButton, |
| 46 | this.filteredNetwork, |
| 47 | this.sourceSelectorMode = false, |
| 48 | this.walletName, |
| 49 | this.balanceByAsset, |
| 50 | this.useSingleNetworkLayout = false, |
| 51 | super.key, |
| 52 | }); |
| 53 | |
| 54 | final List<Currency> currencies; |
| 55 | final Function(Currency) onCurrencySelected; |
| 56 | final String title; |
| 57 | final bool isReceiverCard; |
| 58 | final Currency currency; |
| 59 | final bool useBaseUnit; |
| 60 | final bool hasRefundAddress; |
| 61 | final FormFieldValidator<String>? currencyValueValidator; |
| 62 | final FormFieldValidator<String>? addressTextFieldValidator; |
| 63 | final FormFieldValidator<String> allAmountValidator = AllAmountValidator(); |
| 64 | final bool hasAllAmount; |
| 65 | final VoidCallback? allAmount; |
| 66 | final void Function(BuildContext context)? onPushPasteButton; |
| 67 | final void Function(BuildContext context)? onPushAddressBookButton; |
| 68 | final ExchangeViewModel exchangeViewModel; |
| 69 | final WalletSwitcherViewModel walletSwitcherViewModel; |
| 70 | final WalletType? filteredNetwork; |
| 71 | final bool sourceSelectorMode; |
| 72 | final String? walletName; |
| 73 | final Map<CryptoCurrency, CurrencyPickerBalance>? balanceByAsset; |
| 74 | final bool useSingleNetworkLayout; |
| 75 | |
| 76 | @override |
| 77 | State<SwapAmountBox> createState() => SwapAmountBoxState(); |
| 78 | } |
| 79 | |
| 80 | class SwapAmountBoxState extends State<SwapAmountBox> { |
| 81 | final addressController = TextEditingController(); |
| 82 | final amountController = TextEditingController(); |
| 83 | final fiatAmountController = TextEditingController(); |
| 84 | final amountFocusNode = FocusNode(); |
| 85 | final memoController = TextEditingController(); |
| 86 | final _amountFieldKey = GlobalKey<FormFieldState<String>>(); |
| 87 | mobx.ReactionDisposer? _memoReactionDisposer; |
| 88 | |
| 89 | @override |
| 90 | void initState() { |
| 91 | amountController |
| 92 | .addListener(() => _amountFieldKey.currentState?.didChange(amountController.text)); |
| 93 | |
| 94 | if (widget.isReceiverCard) { |
| 95 | memoController.text = widget.exchangeViewModel.receiveAddressExtraId; |
| 96 | |
| 97 | memoController.addListener(() { |
| 98 | if (widget.exchangeViewModel.receiveAddressExtraId != memoController.text) { |
| 99 | widget.exchangeViewModel.receiveAddressExtraId = memoController.text; |
| 100 | } |
| 101 | }); |
| 102 | |
| 103 | _memoReactionDisposer = |
| 104 | mobx.reaction((_) => widget.exchangeViewModel.receiveAddressExtraId, (String value) { |
| 105 | if (memoController.text != value) { |
| 106 | memoController.text = value; |
| 107 | } |
| 108 | }); |
| 109 | } |
| 110 | |
| 111 | super.initState(); |
| 112 | } |
| 113 | |
| 114 | @override |
| 115 | void dispose() { |
| 116 | _memoReactionDisposer?.call(); |
| 117 | memoController.dispose(); |
| 118 | addressController.dispose(); |
| 119 | amountController.dispose(); |
| 120 | fiatAmountController.dispose(); |
| 121 | amountFocusNode.dispose(); |
| 122 | super.dispose(); |
| 123 | } |
| 124 | |
| 125 | bool _fiatInputMode = false; |
| 126 | |
| 127 | /// Whether the amount field is currently taking fiat input, the page's |
| 128 | /// best-rate reaction needs this to know which controller holds the amount. |
| 129 | bool get fiatInputMode => _fiatInputMode; |
| 130 | |
| 131 | CurrencyPickerBalance? _sourceBalance() { |
| 132 | final selected = widget.currency; |
| 133 | if (selected is! CryptoCurrency) { |
| 134 | return null; |
| 135 | } |
| 136 | return balanceForAsset(widget.balanceByAsset, selected); |
| 137 | } |
| 138 | |
| 139 | @override |
| 140 | Widget build(BuildContext context) { |
| 141 | final currencyToShow = (widget.currency is CryptoCurrency) |
| 142 | ? widget.exchangeViewModel.amountParsingProxy |
| 143 | .getCryptoSymbol(widget.currency as CryptoCurrency) |
| 144 | : widget.currency.name.toUpperCase(); |
| 145 | |
| 146 | final chainIconPath = (widget.currency is CryptoCurrency) |
| 147 | ? _getCurrencyChainIconPath(widget.currency as CryptoCurrency) |
| 148 | : null; |
| 149 | |
| 150 | final colors = Theme.of(context).colorScheme; |
| 151 | |
| 152 | if (widget.sourceSelectorMode) { |
| 153 | return SwapSourceSelector( |
| 154 | currencyIconPath: widget.currency.iconPath ?? "", |
| 155 | currencyLabel: currencyToShow, |
| 156 | chainIconPath: chainIconPath, |
| 157 | walletName: widget.walletName, |
| 158 | availableBalance: _sourceBalance()?.amount.split(" ").first ?? "—", |
| 159 | onTap: _presentCurrencyPicker, |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | return Column( |
| 164 | spacing: 12, |
| 165 | crossAxisAlignment: CrossAxisAlignment.start, |
| 166 | children: [ |
| 167 | if (widget.title.isNotEmpty) |
| 168 | Text( |
| 169 | widget.title, |
| 170 | style: TextStyle( |
| 171 | fontSize: 12, |
| 172 | fontWeight: FontWeight.w500, |
| 173 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 174 | ), |
| 175 | ), |
| 176 | Container( |
| 177 | decoration: ShapeDecoration( |
| 178 | color: Theme.of(context).colorScheme.surfaceContainer, |
| 179 | shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(24)), |
| 180 | ), |
| 181 | child: Padding( |
| 182 | padding: const EdgeInsets.all(16), |
| 183 | child: Column( |
| 184 | spacing: 12, |
| 185 | children: [ |
| 186 | FormField<String>( |
| 187 | key: _amountFieldKey, |
| 188 | initialValue: amountController.text, |
| 189 | validator: _fiatInputMode ? null : widget.currencyValueValidator, |
| 190 | builder: (state) => Column( |
| 191 | crossAxisAlignment: CrossAxisAlignment.start, |
| 192 | children: [ |
| 193 | GestureDetector( |
| 194 | behavior: HitTestBehavior.opaque, |
| 195 | onTap: amountFocusNode.requestFocus, |
| 196 | child: Row( |
| 197 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 198 | spacing: 4, |
| 199 | children: [ |
| 200 | Expanded( |
| 201 | child: Row( |
| 202 | spacing: 4, |
| 203 | crossAxisAlignment: CrossAxisAlignment.center, |
| 204 | children: [ |
| 205 | Flexible( |
| 206 | child: IntrinsicWidth( |
| 207 | child: Observer( |
| 208 | builder: (_) { |
| 209 | final showFetching = !widget.isReceiverCard && |
| 210 | widget.exchangeViewModel.isFixedRateMode && |
| 211 | widget.exchangeViewModel.receiveAmount.isNotEmpty && |
| 212 | widget.exchangeViewModel.depositAmount.isEmpty; |
| 213 | return TextField( |
| 214 | keyboardType: TextInputType.numberWithOptions( |
| 215 | signed: false, |
| 216 | decimal: !widget.useBaseUnit, |
| 217 | ), |
| 218 | onChanged: state.didChange, |
| 219 | controller: _fiatInputMode |
| 220 | ? fiatAmountController |
| 221 | : amountController, |
| 222 | focusNode: amountFocusNode, |
| 223 | style: TextStyle( |
| 224 | fontSize: 28, |
| 225 | color: Theme.of(context).colorScheme.onSurface, |
| 226 | fontWeight: FontWeight.w400, |
| 227 | ), |
| 228 | decoration: InputDecoration( |
| 229 | contentPadding: EdgeInsets.zero, |
| 230 | isDense: true, |
| 231 | hintText: showFetching ? S.of(context).fetching : "0", |
| 232 | fillColor: Colors.transparent, |
| 233 | hoverColor: Colors.transparent, |
| 234 | focusedBorder: InputBorder.none, |
| 235 | enabledBorder: InputBorder.none, |
| 236 | ), |
| 237 | inputFormatters: <TextInputFormatter>[ |
| 238 | DecimalInputFormatter( |
| 239 | maxDecimals: widget.useBaseUnit |
| 240 | ? 0 |
| 241 | : widget.currency.decimals, |
| 242 | ), |
| 243 | ], |
| 244 | ); |
| 245 | }, |
| 246 | ), |
| 247 | ), |
| 248 | ), |
| 249 | if (_fiatInputMode) |
| 250 | Center( |
| 251 | child: Text( |
| 252 | widget.exchangeViewModel.fiat.title, |
| 253 | style: TextStyle( |
| 254 | fontSize: 16, |
| 255 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 256 | ), |
| 257 | ), |
| 258 | ), |
| 259 | ], |
| 260 | ), |
| 261 | ), |
| 262 | GestureDetector( |
| 263 | onTap: _presentCurrencyPicker, |
| 264 | child: Container( |
| 265 | decoration: BoxDecoration( |
| 266 | color: Theme.of(context).colorScheme.surface, |
| 267 | borderRadius: BorderRadius.circular(999999), |
| 268 | ), |
| 269 | child: Padding( |
| 270 | padding: |
| 271 | const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4), |
| 272 | child: Row( |
| 273 | mainAxisAlignment: MainAxisAlignment.center, |
| 274 | children: [ |
| 275 | CakeImageWidget( |
| 276 | imageUrl: widget.currency.iconPath ?? "", |
| 277 | width: 28, |
| 278 | height: 28, |
| 279 | ), |
| 280 | const SizedBox(width: 10), |
| 281 | Text( |
| 282 | currencyToShow, |
| 283 | textAlign: TextAlign.center, |
| 284 | ), |
| 285 | if (chainIconPath != null && chainIconPath.isNotEmpty) ...[ |
| 286 | const SizedBox(width: 4), |
| 287 | CakeImageWidget( |
| 288 | imageUrl: chainIconPath, |
| 289 | width: 12, |
| 290 | height: 12, |
| 291 | colorFilter: ColorFilter.mode( |
| 292 | Theme.of(context).colorScheme.onSurfaceVariant, |
| 293 | BlendMode.srcIn, |
| 294 | ), |
| 295 | ), |
| 296 | const SizedBox(width: 6), |
| 297 | ] else |
| 298 | const SizedBox(width: 10), |
| 299 | Container( |
| 300 | width: 16, |
| 301 | height: 16, |
| 302 | decoration: BoxDecoration( |
| 303 | borderRadius: BorderRadius.circular(9999999999), |
| 304 | color: Theme.of(context).colorScheme.surfaceContainerHigh, |
| 305 | ), |
| 306 | child: Padding( |
| 307 | padding: const EdgeInsets.all(4), |
| 308 | child: RotatedBox( |
| 309 | quarterTurns: 2, |
| 310 | child: CakeImageWidget( |
| 311 | imageUrl: "assets/new-ui/dropdown_arrow.svg", |
| 312 | width: 4, |
| 313 | height: 4, |
| 314 | colorFilter: ColorFilter.mode( |
| 315 | Theme.of(context).colorScheme.primary, |
| 316 | BlendMode.srcIn, |
| 317 | ), |
| 318 | ), |
| 319 | ), |
| 320 | ), |
| 321 | ), |
| 322 | const SizedBox(width: 4), |
| 323 | ], |
| 324 | ), |
| 325 | ), |
| 326 | ), |
| 327 | ), |
| 328 | ], |
| 329 | ), |
| 330 | ), |
| 331 | if (state.hasError) |
| 332 | Padding( |
| 333 | padding: const EdgeInsets.only(top: 6), |
| 334 | child: Semantics( |
| 335 | container: true, |
| 336 | liveRegion: true, |
| 337 | label: "${S.of(context).amount}${state.errorText!}", |
| 338 | excludeSemantics: true, |
| 339 | child: Text( |
| 340 | state.errorText!, |
| 341 | style: TextStyle( |
| 342 | fontSize: 12, |
| 343 | color: Theme.of(context).colorScheme.error, |
| 344 | ), |
| 345 | ), |
| 346 | ), |
| 347 | ), |
| 348 | ], |
| 349 | ), |
| 350 | ), |
| 351 | Observer( |
| 352 | builder: (_) { |
| 353 | final hasAllAmount = !(widget.isReceiverCard || |
| 354 | widget.exchangeViewModel.isSendFromExternal || |
| 355 | !widget.exchangeViewModel.hasAllAmount); |
| 356 | |
| 357 | return FiatAmountBar( |
| 358 | foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh, |
| 359 | textColor: Theme.of(context).colorScheme.onSurfaceVariant, |
| 360 | allAmountColor: hasAllAmount ? null : Colors.transparent, |
| 361 | allAmountTextColor: |
| 362 | hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant, |
| 363 | fiatInputMode: _fiatInputMode, |
| 364 | allAmount: !hasAllAmount |
| 365 | ? widget.isReceiverCard |
| 366 | ? null |
| 367 | : widget.exchangeViewModel.balanceDisplay |
| 368 | : widget.exchangeViewModel.depositAvailableAmount, |
| 369 | onSwitchButtonPressed: () { |
| 370 | setState(() => _fiatInputMode = !_fiatInputMode); |
| 371 | if (_fiatInputMode) { |
| 372 | updateFiatAmount(); |
| 373 | } else { |
| 374 | amountController.text = widget.isReceiverCard |
| 375 | ? widget.exchangeViewModel.receiveAmount |
| 376 | : widget.exchangeViewModel.depositAmount; |
| 377 | } |
| 378 | }, |
| 379 | onAllButtonPressed: () { |
| 380 | setState(() { |
| 381 | _fiatInputMode = false; |
| 382 | }); |
| 383 | widget.allAmount?.call(); |
| 384 | }, |
| 385 | cryptoAmount: widget.isReceiverCard |
| 386 | ? widget.exchangeViewModel.roundedReceiveAmount(6) |
| 387 | : widget.exchangeViewModel.roundedDepositAmount(6), |
| 388 | fiatAmount: widget.isReceiverCard |
| 389 | ? widget.exchangeViewModel.roundedReceiveAmountFiat(6) |
| 390 | : widget.exchangeViewModel.roundedDepositAmountFiat(6), |
| 391 | cryptoCurrencySymbol: currencyToShow, |
| 392 | fiatCurrencySymbol: widget.exchangeViewModel.fiat.symbol, |
| 393 | ); |
| 394 | }, |
| 395 | ), |
| 396 | Observer( |
| 397 | builder: (_) { |
| 398 | final addressEmpty = (widget.isReceiverCard && |
| 399 | widget.exchangeViewModel.receiveAddress.isEmpty) || |
| 400 | (!widget.isReceiverCard && widget.exchangeViewModel.depositAddress.isEmpty); |
| 401 | final addressPickerText = widget.isReceiverCard |
| 402 | ? (addressEmpty ? S.of(context).select_receiver : S.of(context).to) |
| 403 | : S.of(context).from; |
| 404 | final addressDescription = widget.isReceiverCard |
| 405 | ? widget.exchangeViewModel.receiveAddressDisplayName ?? |
| 406 | _middleTruncate(widget.exchangeViewModel.receiveAddress, 8, 8) |
| 407 | : widget.exchangeViewModel.isSendFromExternal |
| 408 | ? S.of(context).external |
| 409 | : widget.exchangeViewModel.wallet.name; |
| 410 | return Row( |
| 411 | spacing: 8, |
| 412 | children: [ |
| 413 | Flexible( |
| 414 | child: GestureDetector( |
| 415 | onTap: _presentWalletPicker, |
| 416 | child: Container( |
| 417 | height: 36, |
| 418 | decoration: BoxDecoration( |
| 419 | color: (addressEmpty && widget.isReceiverCard) |
| 420 | ? Theme.of(context).colorScheme.primary |
| 421 | : Theme.of(context).colorScheme.surfaceContainerHigh, |
| 422 | borderRadius: BorderRadius.circular(9999), |
| 423 | ), |
| 424 | child: Padding( |
| 425 | padding: const EdgeInsets.symmetric(horizontal: 12), |
| 426 | child: Observer( |
| 427 | builder: (_) => Row( |
| 428 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 429 | children: [ |
| 430 | Row( |
| 431 | spacing: 8, |
| 432 | mainAxisSize: MainAxisSize.max, |
| 433 | children: [ |
| 434 | Text( |
| 435 | addressPickerText, |
| 436 | style: TextStyle( |
| 437 | fontSize: 12, |
| 438 | fontWeight: FontWeight.w500, |
| 439 | color: (addressEmpty && widget.isReceiverCard) |
| 440 | ? Theme.of(context).colorScheme.onPrimary |
| 441 | : Theme.of(context).colorScheme.onSurface, |
| 442 | ), |
| 443 | ), |
| 444 | Text( |
| 445 | addressDescription, |
| 446 | style: TextStyle( |
| 447 | fontSize: 12, |
| 448 | fontWeight: FontWeight.w500, |
| 449 | color: Theme.of(context).colorScheme.primary, |
| 450 | ), |
| 451 | ), |
| 452 | ], |
| 453 | ), |
| 454 | RotatedBox( |
| 455 | quarterTurns: 2, |
| 456 | child: CakeImageWidget( |
| 457 | imageUrl: "assets/new-ui/dropdown_arrow.svg", |
| 458 | colorFilter: ColorFilter.mode( |
| 459 | (addressEmpty && widget.isReceiverCard) |
| 460 | ? Theme.of(context).colorScheme.onPrimary |
| 461 | : Theme.of(context).colorScheme.onSurface, |
| 462 | BlendMode.srcIn, |
| 463 | ), |
| 464 | ), |
| 465 | ), |
| 466 | ], |
| 467 | ), |
| 468 | ), |
| 469 | ), |
| 470 | ), |
| 471 | ), |
| 472 | ), |
| 473 | if (!widget.isReceiverCard && |
| 474 | widget.exchangeViewModel.isSendFromExternal && |
| 475 | widget.exchangeViewModel.depositAddress.isEmpty) |
| 476 | ModernButton.svg( |
| 477 | svgPath: "assets/new-ui/refund_address.svg", |
| 478 | onPressed: askForRefundAddress, |
| 479 | size: 36, |
| 480 | iconSize: 18, |
| 481 | backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, |
| 482 | semanticLabel: S.of(context).refund_address, |
| 483 | ), |
| 484 | if (widget.isReceiverCard && |
| 485 | widget.exchangeViewModel.receiveAddress.isEmpty) ...[ |
| 486 | ModernButton.svg( |
| 487 | svgPath: "assets/new-ui/paste.svg", |
| 488 | onPressed: () => widget.onPushPasteButton?.call(context), |
| 489 | size: 36, |
| 490 | iconSize: 20, |
| 491 | backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, |
| 492 | semanticLabel: S.of(context).paste, |
| 493 | ), |
| 494 | ModernButton.svg( |
| 495 | svgPath: "assets/new-ui/scan.svg", |
| 496 | onPressed: () => _presentQRScanner(context), |
| 497 | size: 36, |
| 498 | iconSize: 20, |
| 499 | backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, |
| 500 | semanticLabel: S.of(context).scan, |
| 501 | ), |
| 502 | ], |
| 503 | ], |
| 504 | ); |
| 505 | }, |
| 506 | ), |
| 507 | if (widget.isReceiverCard) |
| 508 | Observer( |
| 509 | builder: (_) { |
| 510 | final selected = widget.exchangeViewModel.receiveCurrency; |
| 511 | final labelType = memoLabelTypeFor(selected); |
| 512 | if (labelType == null) { |
| 513 | return const SizedBox.shrink(); |
| 514 | } |
| 515 | |
| 516 | final isDestinationTag = labelType == MemoLabelType.destinationTag; |
| 517 | final hint = isDestinationTag |
| 518 | ? S.of(context).destination_tag_optional |
| 519 | : S.of(context).memo_optional; |
| 520 | final disclaimer = isDestinationTag |
| 521 | ? S.of(context).destination_tag_swap_disclaimer |
| 522 | : S.of(context).memo_swap_disclaimer; |
| 523 | |
| 524 | return NewSendMemoInput( |
| 525 | memoController: memoController, |
| 526 | maxMemoLength: isDestinationTag ? 20 : 256, |
| 527 | memoLength: memoController.text.length, |
| 528 | hintText: hint, |
| 529 | disclaimerText: disclaimer, |
| 530 | ); |
| 531 | }, |
| 532 | ), |
| 533 | ], |
| 534 | ), |
| 535 | ), |
| 536 | ), |
| 537 | ], |
| 538 | ); |
| 539 | } |
| 540 | |
| 541 | void changeAddress({required String address}) { |
| 542 | setState(() => addressController.text = _normalizeAddressFormat(address)); |
| 543 | } |
| 544 | |
| 545 | void changeAmount({required String amount}) { |
| 546 | setState(() => amountController.text = amount); |
| 547 | } |
| 548 | |
| 549 | String _normalizeAddressFormat(String address) { |
| 550 | if (address.startsWith("bitcoincash:")) { |
| 551 | address = address.substring(12); |
| 552 | } |
| 553 | return address; |
| 554 | } |
| 555 | |
| 556 | void _presentCurrencyPicker() { |
| 557 | final rawCurrencies = widget.isReceiverCard |
| 558 | ? widget.exchangeViewModel.receiveCurrencies |
| 559 | : widget.exchangeViewModel.depositCurrencies; |
| 560 | final currencies = rawCurrencies.whereType<CryptoCurrency>().toList(); |
| 561 | final restrictToCurrentBalances = !widget.isReceiverCard && |
| 562 | widget.balanceByAsset != null && |
| 563 | widget.balanceByAsset!.isNotEmpty; |
| 564 | if (!restrictToCurrentBalances) { |
| 565 | appendEvmDefaultTokens(currencies); |
| 566 | } |
| 567 | if (widget.exchangeViewModel.wallet.type == WalletType.bitcoin) { |
| 568 | currencies.sort((a, b) { |
| 569 | if (a == CryptoCurrency.btcln) { |
| 570 | return -1; |
| 571 | } |
| 572 | if (b == CryptoCurrency.btcln) { |
| 573 | return 1; |
| 574 | } |
| 575 | return 0; |
| 576 | }); |
| 577 | } |
| 578 | |
| 579 | List<CryptoCurrency> items = currencies; |
| 580 | if (restrictToCurrentBalances) { |
| 581 | final allowed = { |
| 582 | for (final asset in widget.balanceByAsset!.keys) asset.title.toUpperCase(), |
| 583 | }; |
| 584 | items = currencies.where((c) => allowed.contains(c.title.toUpperCase())).toList(); |
| 585 | } |
| 586 | |
| 587 | if (widget.isReceiverCard && widget.filteredNetwork != null) { |
| 588 | final network = widget.filteredNetwork!; |
| 589 | items = items.where((c) => cryptoCurrencyOrTokenToWalletType(c) == network).toList(); |
| 590 | } |
| 591 | |
| 592 | if (items.length <= 1) { |
| 593 | return; |
| 594 | } |
| 595 | |
| 596 | final selected = widget.isReceiverCard |
| 597 | ? widget.exchangeViewModel.receiveCurrency |
| 598 | : widget.exchangeViewModel.depositCurrency; |
| 599 | |
| 600 | CurrencyPickerSheet.show( |
| 601 | context: context, |
| 602 | args: CurrencyPickerArgs( |
| 603 | items: items, |
| 604 | selected: selected, |
| 605 | filterByNetwork: widget.filteredNetwork, |
| 606 | balanceByAsset: widget.balanceByAsset, |
| 607 | useSingleNetworkLayout: widget.useSingleNetworkLayout, |
| 608 | recentsSource: RecentsSource.trades, |
| 609 | onSelected: widget.onCurrencySelected, |
| 610 | symbolResolver: widget.exchangeViewModel.amountParsingProxy.getCryptoSymbol, |
| 611 | ), |
| 612 | ); |
| 613 | } |
| 614 | |
| 615 | Future<void> _presentQRScanner(BuildContext context) async { |
| 616 | final isCameraPermissionGranted = |
| 617 | await PermissionHandler.checkPermission(Permission.camera, context); |
| 618 | if (!isCameraPermissionGranted) { |
| 619 | return; |
| 620 | } |
| 621 | final code = await presentQRScanner(context); |
| 622 | if (code == null) { |
| 623 | return; |
| 624 | } |
| 625 | if (code.isEmpty) { |
| 626 | return; |
| 627 | } |
| 628 | |
| 629 | try { |
| 630 | final uri = Uri.parse(code); |
| 631 | widget.exchangeViewModel.receiveAddress = uri.path; |
| 632 | } catch (_) {} |
| 633 | } |
| 634 | |
| 635 | void _presentWalletPicker() async { |
| 636 | final res = await showMaterialModalBottomSheet( |
| 637 | context: context, |
| 638 | backgroundColor: Colors.transparent, |
| 639 | builder: (context) => ConstrainedBox( |
| 640 | constraints: BoxConstraints( |
| 641 | maxHeight: MediaQuery.of(context).size.height * 0.6, |
| 642 | ), |
| 643 | child: SwapAddressSelectionModal( |
| 644 | isSelectingReceiver: widget.isReceiverCard, |
| 645 | exchangeViewModel: widget.exchangeViewModel, |
| 646 | ), |
| 647 | ), |
| 648 | ); |
| 649 | if (res != null && res is SwapAddressSelectionResult) { |
| 650 | if (widget.isReceiverCard) { |
| 651 | widget.exchangeViewModel.selectedAddressBookWallet = res.walletInfo; |
| 652 | widget.exchangeViewModel.receiveAddress = res.address!; |
| 653 | if (res.walletInfo?.name != null) { |
| 654 | if (res.accountName != null) { |
| 655 | widget.exchangeViewModel.receiveAddressDisplayName = |
| 656 | "${res.walletInfo!.name} → ${res.accountName}"; |
| 657 | } else { |
| 658 | widget.exchangeViewModel.receiveAddressDisplayName = res.walletInfo!.name; |
| 659 | } |
| 660 | } |
| 661 | } else if (res.address == null || res.address!.isEmpty) { |
| 662 | widget.exchangeViewModel.isSendFromExternal = true; |
| 663 | askForRefundAddress(); |
| 664 | } else { |
| 665 | widget.exchangeViewModel.isSendFromExternal = false; |
| 666 | switchToDepositWallet(res.walletInfo!.name); |
| 667 | } |
| 668 | } |
| 669 | } |
| 670 | |
| 671 | void switchToDepositWallet(String walletName) async { |
| 672 | final walletType = cryptoCurrencyOrTokenToWalletType(widget.exchangeViewModel.depositCurrency); |
| 673 | if (walletType == null) { |
| 674 | return; |
| 675 | } |
| 676 | final wallet = await WalletInfo.get(walletName, walletType); |
| 677 | if (wallet == null) { |
| 678 | return; |
| 679 | } |
| 680 | widget.exchangeViewModel.depositAddress = wallet.address; |
| 681 | addressController.text = _normalizeAddressFormat(wallet.address); |
| 682 | widget.walletSwitcherViewModel.selectWallet(wallet); |
| 683 | await widget.walletSwitcherViewModel.switchToSelectedWallet(); |
| 684 | } |
| 685 | |
| 686 | void askForRefundAddress() async { |
| 687 | final refundAddress = await showModalBottomSheet( |
| 688 | context: context, |
| 689 | isScrollControlled: true, |
| 690 | backgroundColor: Colors.transparent, |
| 691 | builder: (context) => RefundAddressModal( |
| 692 | selectedCurrency: widget.exchangeViewModel.depositCurrency, |
| 693 | isFromWalletSelection: true, |
| 694 | ), |
| 695 | ); |
| 696 | if (refundAddress != null && refundAddress is String) { |
| 697 | widget.exchangeViewModel.depositAddress = refundAddress; |
| 698 | } else { |
| 699 | widget.exchangeViewModel.depositAddress = ""; |
| 700 | } |
| 701 | } |
| 702 | |
| 703 | void updateFiatAmount() { |
| 704 | final newText = widget.isReceiverCard |
| 705 | ? widget.exchangeViewModel.receiveAmountFiat |
| 706 | : widget.exchangeViewModel.depositAmountFiat; |
| 707 | |
| 708 | if (double.tryParse(fiatAmountController.text) != double.tryParse(newText)) { |
| 709 | if (newText == "0.00") { |
| 710 | fiatAmountController.text = ""; |
| 711 | } else { |
| 712 | fiatAmountController.text = |
| 713 | newText.replaceAll(RegExp(r"(?<=\.\d*)0+$"), "").replaceAll(RegExp(r"\.$"), ""); |
| 714 | } |
| 715 | } |
| 716 | } |
| 717 | |
| 718 | String _middleTruncate(String s, int head, int tail) { |
| 719 | if (s.length <= head + tail + 3) { |
| 720 | return s; |
| 721 | } |
| 722 | return s.substring(0, head) + "..." + s.substring(s.length - tail); |
| 723 | } |
| 724 | |
| 725 | String? _getCurrencyChainIconPath(CryptoCurrency curr) { |
| 726 | try { |
| 727 | if (curr.chainIconPath != null) { |
| 728 | return curr.chainIconPath!; |
| 729 | } |
| 730 | |
| 731 | if (curr.tag != null) { |
| 732 | final currencyFromTag = CryptoCurrency.fromString(curr.tag!); |
| 733 | |
| 734 | if (currencyFromTag.chainIconPath != null) { |
| 735 | return currencyFromTag.chainIconPath!; |
| 736 | } |
| 737 | } |
| 738 | } catch (_) {} |
| 739 | return null; |
| 740 | } |
| 741 | } |