mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
Send-success art: replace the Cake birthday cake with a Hash Bags money sack that has a # on it and erupts #/$/₿ (assets/images/hash_bag.png). Used by the success page, send-confirm sheet, exchange-trade, and dEURO savings. Asset renames (files + refs): - birthday_cake.png -> hash_bag.png - restore_cupcake(.png/_dark) -> restore_hashbag.png - qr-cake.png -> qr-hashbag.png - arrow_bottom_cake_green.png -> arrow_bottom_green.png (+ 2.0x/3.0x) Identifier renames: - CakeImageWidget -> HashImageWidget (widget + file + 93 refs) - typography _cakeRegular/_cakeBold/_cakeSemiBold -> _hash* Transaction filter: drop the Cake Pay "Orders" row and every non-Trocador trade provider; the swap gate is already Trocador-only, so this just aligns the filter UI with reality. Android launcher: add breathing room to the adaptive-foreground # (all densities, monochrome included); iOS icon unchanged. About page: swap the Cake easter eggs for Hash Bags ones. Deferred (needs a build to verify): new-ui .svg.vec filename renames (cake_squircle_icon, cakewallet-wordmark, cake-setting) — already Hash Bags in content, only the filenames are cake, and they route through the .vec precompile.
1380 lines
59 KiB
Dart
1380 lines
59 KiB
Dart
import 'package:hash_wallet/core/address_validator.dart';
|
|
import 'package:hash_wallet/core/auth_service.dart';
|
|
import 'package:hash_wallet/core/execution_state.dart';
|
|
import 'package:hash_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
|
|
import 'package:hash_wallet/di.dart';
|
|
import 'package:hash_wallet/entities/contact_record.dart';
|
|
import 'package:hash_wallet/entities/fiat_currency.dart';
|
|
import 'package:hash_wallet/entities/priority_for_wallet_type.dart';
|
|
import 'package:hash_wallet/evm/evm.dart';
|
|
import 'package:hash_wallet/exchange/trade.dart';
|
|
import 'package:hash_wallet/generated/i18n.dart';
|
|
import 'package:hash_wallet/monero/monero.dart';
|
|
import 'package:hash_wallet/new-ui/modal_navigator.dart';
|
|
import 'package:hash_wallet/new-ui/pages/coin_control_page.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/animated_dropdown.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/picker.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/send_memo_input.dart';
|
|
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
|
import 'package:hash_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart';
|
|
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
|
import 'package:hash_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart';
|
|
import 'package:hash_wallet/src/widgets/standard_checkbox.dart';
|
|
import 'package:hash_wallet/store/app_store.dart';
|
|
import 'package:cw_core/lnurl.dart';
|
|
import 'package:cw_core/wallet_info.dart';
|
|
import "package:cw_core/wallet_type.dart";
|
|
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/directional_switcher.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/recipient_dot_row.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/send_address_input.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/send_amount_input.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart';
|
|
import 'package:hash_wallet/routes.dart' show Routes;
|
|
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
|
|
import 'package:hash_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
|
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
|
import 'package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
|
import 'package:hash_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart';
|
|
import 'package:hash_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
|
|
import 'package:hash_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart';
|
|
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
|
import 'package:hash_wallet/utils/payment_request.dart';
|
|
import 'package:hash_wallet/utils/show_pop_up.dart';
|
|
import 'package:hash_wallet/view_model/payment/payment_view_model.dart';
|
|
import 'package:hash_wallet/view_model/send/output.dart';
|
|
import 'package:hash_wallet/view_model/send/send_view_model.dart';
|
|
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
|
|
import 'package:hash_wallet/view_model/wallet_switcher_view_model.dart';
|
|
import 'package:cw_core/crypto_currency.dart';
|
|
import 'package:cw_core/currency.dart';
|
|
import 'package:cw_core/transaction_priority.dart';
|
|
import 'package:cw_core/utils/print_verbose.dart';
|
|
|
|
import 'package:hash_wallet/main.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/new_primary_button.dart';
|
|
import 'package:hash_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart';
|
|
import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart';
|
|
import 'package:cw_core/unspent_coin_type.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
|
|
|
class SendPageHelpContent {
|
|
final String imagePath;
|
|
final String title;
|
|
final String description;
|
|
final String? disclaimer;
|
|
|
|
const SendPageHelpContent(
|
|
{required this.title, required this.imagePath, required this.description, this.disclaimer});
|
|
}
|
|
|
|
class SendPageModes {
|
|
final bool showAddressField;
|
|
final String title;
|
|
final String? description;
|
|
final String? confirmSheetIconPath;
|
|
final SendPageHelpContent? helpContent;
|
|
final bool popOnConfirmation;
|
|
|
|
const SendPageModes(
|
|
{required this.title,
|
|
this.description,
|
|
required this.showAddressField,
|
|
this.confirmSheetIconPath,
|
|
this.helpContent,
|
|
this.popOnConfirmation = true});
|
|
|
|
static final SendPageModes normal = SendPageModes(title: S.current.send, showAddressField: true);
|
|
|
|
static final SendPageModes lightningDeposit = SendPageModes(
|
|
title: S.current.bitcoin_lightning_deposit,
|
|
description: S.current.to_lightning,
|
|
showAddressField: false,
|
|
helpContent: SendPageHelpContent(
|
|
title: S.current.bitcoin_lightning_deposit,
|
|
imagePath: "assets/new-ui/lightning_deposit_help.svg",
|
|
description: S.current.lightning_deposit_desc,
|
|
disclaimer: S.current.lightning_deposit_disclaimer),
|
|
popOnConfirmation: false);
|
|
|
|
static final SendPageModes lightningWithdrawal = SendPageModes(
|
|
title: S.current.bitcoin_lightning_withdraw,
|
|
description: S.current.to_on_chain,
|
|
showAddressField: false,
|
|
helpContent: SendPageHelpContent(
|
|
title: S.current.bitcoin_lightning_withdraw,
|
|
imagePath: "assets/new-ui/lightning_withdraw_help.svg",
|
|
description: S.current.lightning_withdraw_desc,
|
|
disclaimer: S.current.lightning_withdraw_disclaimer),
|
|
popOnConfirmation: false);
|
|
|
|
static final SendPageModes mwebDeposit = SendPageModes(
|
|
title: "${S.current.mask} Litecoin",
|
|
showAddressField: false,
|
|
confirmSheetIconPath: "assets/new-ui/mask.svg",
|
|
helpContent: SendPageHelpContent(
|
|
title: S.current.about_litecoin_privacy,
|
|
imagePath: "assets/new-ui/mweb_help.svg",
|
|
description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2,
|
|
disclaimer: S.current.mweb_help_disclaimer),
|
|
popOnConfirmation: false);
|
|
|
|
static final SendPageModes mwebWithdrawal = SendPageModes(
|
|
title: "${S.current.unmask} Litecoin",
|
|
showAddressField: false,
|
|
confirmSheetIconPath: "assets/new-ui/unmask.svg",
|
|
helpContent: SendPageHelpContent(
|
|
title: S.current.about_litecoin_privacy,
|
|
imagePath: "assets/new-ui/mweb_help.svg",
|
|
description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2,
|
|
disclaimer: S.current.mweb_help_disclaimer),
|
|
popOnConfirmation: false);
|
|
|
|
static final all = [normal, lightningDeposit, lightningWithdrawal, mwebDeposit, mwebWithdrawal];
|
|
}
|
|
|
|
class SendPageParams {
|
|
final PaymentRequest? initialPaymentRequest;
|
|
final SendPageModes mode;
|
|
final CryptoCurrency? initialCurrency;
|
|
final UnspentCoinType unspentCoinType;
|
|
|
|
SendPageParams({
|
|
this.initialPaymentRequest,
|
|
SendPageModes? mode,
|
|
this.unspentCoinType = UnspentCoinType.any,
|
|
this.initialCurrency,
|
|
}) : mode = mode ?? SendPageModes.normal;
|
|
}
|
|
|
|
class NewSendPage extends StatefulWidget {
|
|
NewSendPage(
|
|
{super.key,
|
|
required this.sendViewModel,
|
|
required this.paymentViewModel,
|
|
required this.walletSwitcherViewModel,
|
|
required this.contactListViewModel,
|
|
required this.authService,
|
|
required SendPageParams params})
|
|
: initialPaymentRequest = params.initialPaymentRequest,
|
|
mode = params.mode {
|
|
if (params.initialCurrency != null) {
|
|
sendViewModel.selectedCryptoCurrency = params.initialCurrency!;
|
|
}
|
|
}
|
|
|
|
final SendViewModel sendViewModel;
|
|
final PaymentViewModel paymentViewModel;
|
|
final WalletSwitcherViewModel walletSwitcherViewModel;
|
|
final ContactListViewModel contactListViewModel;
|
|
final AuthService authService;
|
|
final PaymentRequest? initialPaymentRequest;
|
|
final SendPageModes mode;
|
|
|
|
@override
|
|
State<NewSendPage> createState() => _NewSendPageState();
|
|
}
|
|
|
|
class _NewSendPageState extends State<NewSendPage> {
|
|
bool _fiatInputMode = false;
|
|
int _selectedOutput = 0;
|
|
|
|
List<TextEditingController> _amountControllers = [];
|
|
List<TextEditingController> _addressControllers = [];
|
|
List<TextEditingController> _memoControllers = [];
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _addressFocusNode = FocusNode();
|
|
BuildContext? loadingBottomSheetContext;
|
|
BuildContext? dialogContext;
|
|
ContactRecord? newContactAddress;
|
|
|
|
bool _justHandledPasteButton = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_addInputControllers();
|
|
|
|
reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, ((bool all) {
|
|
if (all) {
|
|
_fiatInputMode = false;
|
|
_amountControllers[_selectedOutput].text = S.current.all;
|
|
}
|
|
}));
|
|
|
|
reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, ((address) {
|
|
if (_addressControllers[_selectedOutput].text != address) {
|
|
_addressControllers[_selectedOutput].text = address;
|
|
}
|
|
}));
|
|
|
|
reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (String memo) {
|
|
if (memo != _memoControllers[_selectedOutput].text) {
|
|
_memoControllers[_selectedOutput].text = memo;
|
|
}
|
|
});
|
|
|
|
if (widget.initialPaymentRequest != null &&
|
|
widget.sendViewModel.walletCurrencyName ==
|
|
widget.initialPaymentRequest!.scheme.toLowerCase()) {
|
|
_addressControllers[0].text = widget.initialPaymentRequest!.address;
|
|
_amountControllers[0].text = widget.initialPaymentRequest!.amount;
|
|
_memoControllers[0].text = widget.initialPaymentRequest!.note;
|
|
}
|
|
|
|
/// if the current wallet doesn't match the one in the qr code
|
|
if (widget.initialPaymentRequest != null &&
|
|
widget.sendViewModel.walletCurrencyName !=
|
|
widget.initialPaymentRequest!.scheme.toLowerCase()) {
|
|
WidgetsBinding.instance.addPostFrameCallback(
|
|
(timeStamp) {
|
|
if (mounted) {
|
|
final prefix = widget.initialPaymentRequest!.scheme.isNotEmpty
|
|
? "${widget.initialPaymentRequest!.scheme}:"
|
|
: "";
|
|
final amount = widget.initialPaymentRequest!.amount.isNotEmpty
|
|
? "?amount=${widget.initialPaymentRequest!.amount}"
|
|
: "";
|
|
final uri = prefix + widget.initialPaymentRequest!.address + amount;
|
|
_handlePaymentFlow(uri, widget.initialPaymentRequest!);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
_addressFocusNode.addListener(() async {
|
|
if (!_addressFocusNode.hasFocus && _addressControllers[_selectedOutput].text.isNotEmpty) {
|
|
final output = widget.sendViewModel.outputs[_selectedOutput];
|
|
output.fetchParsedAddress(context).then((val) {
|
|
if (_addressControllers[_selectedOutput].text != output.extractedAddress) {
|
|
_addressControllers[_selectedOutput].text = output.extractedAddress;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Observer(builder: (_) {
|
|
final output = widget.sendViewModel.outputs[_selectedOutput];
|
|
return SafeArea(
|
|
bottom: false,
|
|
child: KeyboardHideOverlay(
|
|
unfocusOnTap: true,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
|
|
child: SafeArea(
|
|
child: Column(
|
|
spacing: 12,
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: [
|
|
ModalTopBar(
|
|
title: widget.mode.title,
|
|
subtitle: widget.mode.description,
|
|
leadingIcon: Icon(Icons.close),
|
|
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
|
|
trailingWidget: Observer(
|
|
builder: (_) => Row(
|
|
spacing: 8,
|
|
children: [
|
|
if (widget.sendViewModel.outputs.length > 1)
|
|
ModernButton(
|
|
size: 36,
|
|
icon: HashImageWidget(imageUrl:
|
|
"assets/new-ui/remove_recipient.svg",
|
|
colorFilter: ColorFilter.mode(
|
|
Theme.of(context).colorScheme.primary,
|
|
BlendMode.srcIn,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
final outputIndex = _selectedOutput;
|
|
if (_selectedOutput != 0) {
|
|
_setOutput(_selectedOutput - 1);
|
|
} else {
|
|
_setOutput(1);
|
|
}
|
|
_removeInputControllers(outputIndex);
|
|
widget.sendViewModel.removeOutput(output);
|
|
if (outputIndex == 0) _setOutput(0);
|
|
}),
|
|
if (widget.mode == SendPageModes.normal &&
|
|
widget.sendViewModel.hasMultiRecipient)
|
|
ModernButton(
|
|
size: 36,
|
|
icon: Icon(Icons.add),
|
|
onPressed: () {
|
|
_addInputControllers();
|
|
widget.sendViewModel.addOutput();
|
|
_setOutput(widget.sendViewModel.outputs.length - 1);
|
|
}),
|
|
if (widget.mode.helpContent != null)
|
|
ModernButton(
|
|
size: 36,
|
|
icon: HashImageWidget(imageUrl:
|
|
"assets/new-ui/help.svg",
|
|
colorFilter: ColorFilter.mode(
|
|
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).push(CupertinoPageRoute(
|
|
builder: (context) => Material(
|
|
child: SendHelpPage(content: widget.mode.helpContent!))));
|
|
})
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: [
|
|
Form(
|
|
key: _formKey,
|
|
child: DirectionalAnimatedSwitcher(
|
|
duration: Duration(milliseconds: 300),
|
|
child: Column(
|
|
key: ValueKey(_selectedOutput),
|
|
spacing: 24,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (widget.mode.showAddressField)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 12,
|
|
children: [
|
|
Text(S.of(context).address_or_alias),
|
|
NewSendAddressInput(
|
|
displayName: output.displayName,
|
|
validator: output.isParsedAddress
|
|
? widget.sendViewModel.textValidator
|
|
: widget.sendViewModel.addressValidator,
|
|
addressController: _addressControllers[_selectedOutput],
|
|
focusNode: _addressFocusNode,
|
|
onURIScanned: (uri) async {
|
|
output.resetParsedAddress();
|
|
await output.fetchParsedAddress(context);
|
|
|
|
// Process the payment through the new flow
|
|
await _handlePaymentFlow(
|
|
uri.toString(),
|
|
PaymentRequest.fromString(uri.toString()),
|
|
);
|
|
},
|
|
onEditingComplete: () {
|
|
_addressFocusNode.unfocus();
|
|
// output.fetchParsedAddress(context).then((val){
|
|
// if(_addressControllers[_selectedOutput].text != output.extractedAddress) {
|
|
// _addressControllers[_selectedOutput].text = output.extractedAddress;
|
|
// }
|
|
// });
|
|
},
|
|
onPushAddressBookButton: (context) async {
|
|
output.resetParsedAddress();
|
|
},
|
|
onSelectedContact: (contact) {
|
|
output.loadContact(contact);
|
|
},
|
|
onPushPasteButton: (context) async {
|
|
if(_justHandledPasteButton) return;
|
|
_justHandledPasteButton = true;
|
|
try {
|
|
output.resetParsedAddress();
|
|
await output.fetchParsedAddress(context);
|
|
|
|
final address = output.isParsedAddress
|
|
? output.extractedAddress
|
|
: output.address;
|
|
|
|
await _handlePaymentFlow(
|
|
address,
|
|
PaymentRequest(
|
|
address,
|
|
_amountControllers[_selectedOutput].text,
|
|
_memoControllers[_selectedOutput].text,
|
|
"",
|
|
null,
|
|
),
|
|
);
|
|
} finally {
|
|
_justHandledPasteButton = false;
|
|
}
|
|
|
|
_handleLightningInvoicePaste();
|
|
},
|
|
selectedCurrency:
|
|
widget.sendViewModel.selectedCryptoCurrency,
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 12,
|
|
children: [
|
|
Text(S.of(context).amount),
|
|
NewSendAmountInput(
|
|
validator: output.sendAll
|
|
? widget.sendViewModel.allAmountValidator
|
|
: widget.sendViewModel.amountValidator(output),
|
|
amountController: _amountControllers[_selectedOutput],
|
|
currency: _fiatInputMode
|
|
? widget.sendViewModel.fiatCurrency.title
|
|
: widget.sendViewModel.selectedCryptoCurrencySymbol,
|
|
currencyIconPath: _fiatInputMode
|
|
? ""
|
|
: widget.sendViewModel.selectedCryptoCurrency
|
|
.iconPath ??
|
|
"",
|
|
hasPicker: (_fiatInputMode ||
|
|
widget.sendViewModel.hasMultipleTokens),
|
|
onPickerClicked: () {
|
|
_presentCurrencyPicker(context);
|
|
},
|
|
),
|
|
FiatAmountBar(
|
|
fiatInputMode: _fiatInputMode,
|
|
onSwitchButtonPressed: () {
|
|
setState(() {
|
|
if(!_fiatInputMode) {
|
|
widget.sendViewModel.outputs[_selectedOutput]
|
|
.cryptoAmount =
|
|
_amountControllers[_selectedOutput].text;
|
|
}
|
|
_fiatInputMode = !_fiatInputMode;
|
|
_amountControllers[_selectedOutput].text =
|
|
_fiatInputMode
|
|
? output.fiatAmount
|
|
: output.displayCryptoAmount;
|
|
});
|
|
},
|
|
fiatAmount: _wrapAmount(output.roundedFiatAmount(6), 20),
|
|
cryptoAmount:
|
|
_wrapAmount(output.roundedCryptoAmount(6), 20),
|
|
allAmount: widget.sendViewModel.balance,
|
|
cryptoCurrency:
|
|
widget.sendViewModel.selectedCryptoCurrencySymbol,
|
|
fiatCurrency: widget.sendViewModel.fiatCurrency.title,
|
|
onAllButtonPressed: () async {
|
|
output.setSendAll(
|
|
await widget.sendViewModel.sendingBalance);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
if (widget.sendViewModel.isMwebAvailable && widget.mode == SendPageModes.normal)
|
|
StandardCheckbox(
|
|
caption: S.of(context).litecoin_mweb_allow_coins,
|
|
captionColor: Theme.of(context).colorScheme.onSurface,
|
|
borderColor: Theme.of(context).colorScheme.primary,
|
|
iconColor: Theme.of(context).colorScheme.primary,
|
|
value: [UnspentCoinType.any, UnspentCoinType.mweb]
|
|
.contains(widget.sendViewModel.coinTypeToSpendFrom),
|
|
onChanged: (value) =>
|
|
widget.sendViewModel.setAllowMwebCoins(value),
|
|
),
|
|
if(widget.sendViewModel.hasMemos)
|
|
Observer(
|
|
builder: (_) => NewSendMemoInput(
|
|
memoController: _memoControllers[_selectedOutput],
|
|
maxMemoLength: widget.sendViewModel.maxMemoLength,
|
|
memoLength: output.memo.length,
|
|
)),
|
|
if (widget.sendViewModel.hasCoinControl ||
|
|
widget.sendViewModel.hasFees)
|
|
AnimatedDropdown(
|
|
dropdownText: S.of(context).advanced_settings,
|
|
content: Column(children: [
|
|
if (widget.sendViewModel.hasFees)
|
|
ListItemRegularRowWidget(
|
|
keyValue: "",
|
|
label: S.of(context).fees,
|
|
subtitle:
|
|
"~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})",
|
|
onTap: () {
|
|
if (widget
|
|
.sendViewModel.feesViewModel.hasFeesPriority)
|
|
pickTransactionPriority(context, output);
|
|
},
|
|
),
|
|
if (widget.sendViewModel.hasCoinControl)
|
|
ListItemRegularRowWidget(
|
|
keyValue: "",
|
|
label: "Coin Control",
|
|
onTap: () {
|
|
showCupertinoModalBottomSheet(enableDrag: false, useRootNavigator: true, isDismissible: false, context: context, builder: (context){
|
|
return NewCoinControlPage(unspentCoinsListViewModel: widget.sendViewModel.unspentCoinsListViewModel,);
|
|
});
|
|
}
|
|
),
|
|
]),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Observer(
|
|
builder: (_) => Column(
|
|
spacing: 12,
|
|
children: [
|
|
if (!widget.sendViewModel.isReadyForSend)
|
|
SendSyncingIndicator(
|
|
status: widget.sendViewModel.wallet.syncStatus),
|
|
if (widget.sendViewModel.outputs.length > 1)
|
|
RecipientDotRow(
|
|
numDots: widget.sendViewModel.outputs.length,
|
|
onSelected: _setOutput,
|
|
selectedDot: _selectedOutput,
|
|
),
|
|
Observer(
|
|
builder: (_) {
|
|
return LoadingPrimaryButton(
|
|
key: ValueKey('send_page_send_button_key'),
|
|
onPressed: () async {
|
|
//Request dummy node to get the focus out of the text fields
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
|
|
if (widget.sendViewModel.state is IsExecutingState) return;
|
|
|
|
if (widget.mode == SendPageModes.normal) {
|
|
_handleSend();
|
|
} else if (widget.mode == SendPageModes.lightningDeposit ||
|
|
widget.mode == SendPageModes.mwebDeposit) {
|
|
Navigator.of(context).push(CupertinoPageRoute(
|
|
builder: (context) => Material(
|
|
child: L2ActionWalletSelector(
|
|
showOtherWallets: false,
|
|
action: l2actions.deposit,
|
|
sendViewModel: widget.sendViewModel,
|
|
contactListViewModel:
|
|
widget.contactListViewModel,
|
|
walletSwitcherViewModel:
|
|
widget.walletSwitcherViewModel,
|
|
onSendInitiated: _handleSend,
|
|
))));
|
|
} else if (widget.mode ==
|
|
SendPageModes.lightningWithdrawal ||
|
|
widget.mode == SendPageModes.mwebWithdrawal) {
|
|
Navigator.of(context).push(CupertinoPageRoute(
|
|
builder: (context) => Material(
|
|
child: L2ActionWalletSelector(
|
|
showOtherWallets: false,
|
|
action: l2actions.withdraw,
|
|
sendViewModel: widget.sendViewModel,
|
|
contactListViewModel:
|
|
widget.contactListViewModel,
|
|
walletSwitcherViewModel:
|
|
widget.walletSwitcherViewModel,
|
|
onSendInitiated: _handleSend,
|
|
))));
|
|
}
|
|
},
|
|
text: S.of(context).continue_text,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
isLoading: widget.sendViewModel.state is IsExecutingState ||
|
|
widget.sendViewModel.state is TransactionCommitting ||
|
|
widget.sendViewModel.state
|
|
is IsAwaitingDeviceResponseState ||
|
|
widget.sendViewModel.state
|
|
is LoadingTemplateExecutingState,
|
|
isDisabled: !widget.sendViewModel.isReadyForSend ||
|
|
widget.sendViewModel.state is ExecutedSuccessfullyState,
|
|
);
|
|
},
|
|
),
|
|
SizedBox(),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
void _setOutput(int index) {
|
|
setState(() {
|
|
_selectedOutput = index;
|
|
});
|
|
// final output = widget.sendViewModel.outputs[index];
|
|
// _amountController.text = _fiatInputMode ? output.fiatAmount : output.cryptoAmount;
|
|
// _addressController.text = output.address;
|
|
}
|
|
|
|
void _addInputControllers() {
|
|
_amountControllers.add(TextEditingController());
|
|
_addressControllers.add(TextEditingController());
|
|
_memoControllers.add(TextEditingController());
|
|
|
|
_amountControllers[_amountControllers.length - 1].addListener(() {
|
|
if (_selectedOutput > widget.sendViewModel.outputs.length - 1) {
|
|
printV(
|
|
"_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!");
|
|
return;
|
|
}
|
|
|
|
final amount = _amountControllers[_selectedOutput].text.replaceAll(',', '.');
|
|
final output = widget.sendViewModel.outputs[_selectedOutput];
|
|
|
|
if (_fiatInputMode) {
|
|
if (amount != output.fiatAmount) {
|
|
output.sendAll = false;
|
|
output.setFiatAmount(amount);
|
|
}
|
|
} else {
|
|
final isAll = mounted && amount != S.of(context).all;
|
|
if (output.sendAll && isAll) {
|
|
output.sendAll = false;
|
|
}
|
|
|
|
if (S.current.all.contains(amount)) return;
|
|
|
|
final cAmount = widget.sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
|
|
output.cryptoAmount, widget.sendViewModel.selectedCryptoCurrency);
|
|
if (amount != cAmount) {
|
|
final newAmount = widget.sendViewModel.amountParsingProxy
|
|
.getCanonicalCryptoAmount(amount, widget.sendViewModel.selectedCryptoCurrency);
|
|
output.setCryptoAmount(newAmount);
|
|
}
|
|
}
|
|
});
|
|
|
|
_addressControllers[_amountControllers.length - 1].addListener(() {
|
|
if (_selectedOutput > widget.sendViewModel.outputs.length - 1) {
|
|
printV(
|
|
"_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!");
|
|
return;
|
|
}
|
|
|
|
final address = _addressControllers[_selectedOutput].text;
|
|
final output = widget.sendViewModel.outputs[_selectedOutput];
|
|
|
|
if (output.address != address && output.extractedAddress != address) {
|
|
output.resetParsedAddress();
|
|
output.address = address;
|
|
}
|
|
});
|
|
|
|
_memoControllers[_amountControllers.length - 1].addListener(() {
|
|
if (_selectedOutput > widget.sendViewModel.outputs.length - 1) {
|
|
printV(
|
|
"_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!");
|
|
return;
|
|
}
|
|
final memo = _memoControllers[_selectedOutput].text;
|
|
final output = widget.sendViewModel.outputs[_selectedOutput];
|
|
|
|
if (memo != output.memo && memo.length <= widget.sendViewModel.maxMemoLength) {
|
|
output.memo = memo;
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
void _handleSend() async {
|
|
//TODO refactor this action. code was copied over from old ui. i don't like it.
|
|
|
|
for (var i = 0; i < widget.sendViewModel.outputs.length; i++) {
|
|
if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) {
|
|
if (_fiatInputMode) {
|
|
widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text);
|
|
} else {
|
|
final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount(
|
|
_amountControllers[i].text.replaceAll(",", "."),
|
|
widget.sendViewModel.selectedCryptoCurrency);
|
|
widget.sendViewModel.outputs[i].setCryptoAmount(amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
|
if (widget.sendViewModel.outputs.length > 1) {
|
|
showErrorValidationAlert(context);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
final notValidItems = widget.sendViewModel.outputs
|
|
.where((item) => item.address.isEmpty || (!item.sendAll && item.cryptoAmount.isEmpty))
|
|
.toList();
|
|
|
|
if (notValidItems.isNotEmpty) {
|
|
showErrorValidationAlert(context);
|
|
return;
|
|
}
|
|
|
|
if (widget.sendViewModel.wallet.isHardwareWallet) {
|
|
if (!widget.sendViewModel.hardwareWalletViewModel!.isConnected) {
|
|
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
|
arguments: ConnectDevicePageParams(
|
|
walletType: widget.sendViewModel.walletType,
|
|
hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!,
|
|
onConnectDevice: (BuildContext context, _) {
|
|
widget.sendViewModel.hardwareWalletViewModel!
|
|
.initWallet(widget.sendViewModel.wallet);
|
|
Navigator.of(context).pop();
|
|
},
|
|
));
|
|
} else {
|
|
widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet);
|
|
}
|
|
}
|
|
|
|
if (widget.sendViewModel.wallet.type == WalletType.monero) {
|
|
int amount = 0;
|
|
for (var item in widget.sendViewModel.outputs) {
|
|
amount += item.formattedCryptoAmount;
|
|
}
|
|
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
|
|
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
|
|
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet));
|
|
await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state
|
|
}
|
|
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
final check = widget.sendViewModel.shouldDisplayTotp();
|
|
widget.authService.authenticateAction(
|
|
navigatorKey.currentContext ?? context,
|
|
conditionToDetermineIfToUse2FA: check,
|
|
onAuthSuccess: (value) async {
|
|
if (value) {
|
|
if (!widget.mode.popOnConfirmation) {
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
}
|
|
showModalBottomSheet(
|
|
isScrollControlled: true,
|
|
context: navigatorKey.currentContext ?? context,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (context) {
|
|
return SendConfirmSheet(
|
|
title: widget.mode.title,
|
|
iconPath: widget.mode.helpContent?.imagePath,
|
|
sendViewModel: widget.sendViewModel,
|
|
);
|
|
}).then((value) async {
|
|
if (widget.sendViewModel.state is TransactionCommitted &&
|
|
widget.mode.popOnConfirmation) {
|
|
if (!mounted) return;
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
}
|
|
widget.sendViewModel.dismissTransaction();
|
|
});
|
|
|
|
await widget.sendViewModel.createTransaction();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
void _removeInputControllers(int index) {
|
|
_amountControllers.removeAt(index);
|
|
_addressControllers.removeAt(index);
|
|
_memoControllers.removeAt(index);
|
|
}
|
|
|
|
void _presentCurrencyPicker(BuildContext context) {
|
|
if (!_fiatInputMode && !widget.sendViewModel.hasMultipleTokens) {
|
|
return;
|
|
}
|
|
|
|
final output = widget.sendViewModel.outputs[_selectedOutput];
|
|
|
|
showPopUp<void>(
|
|
context: context,
|
|
builder: (_) => CurrencyPicker(
|
|
key: ValueKey('send_page_currency_picker_dialog_button_key'),
|
|
selectedAtIndex: _fiatInputMode
|
|
? widget.sendViewModel.fiatCurrencies.indexOf(widget.sendViewModel.fiatCurrency)
|
|
: widget.sendViewModel.currencies.indexOf(widget.sendViewModel.selectedCryptoCurrency),
|
|
items:
|
|
_fiatInputMode ? widget.sendViewModel.fiatCurrencies : widget.sendViewModel.currencies,
|
|
hintText: S.of(context).search_currency,
|
|
onItemSelected: (Currency cur) async {
|
|
late final selectedCurrency;
|
|
if (_fiatInputMode) {
|
|
selectedCurrency = widget.sendViewModel.setFiatCurrency(cur as FiatCurrency);
|
|
} else {
|
|
selectedCurrency =
|
|
widget.sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency);
|
|
}
|
|
await output.calculateEstimatedFee();
|
|
return selectedCurrency;
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleLightningInvoicePaste() {
|
|
try {
|
|
final lnAmount = CryptoCurrency.btcln.formatAmount(
|
|
BigInt.from(getBolt11Amount(_addressControllers[_selectedOutput].text) ?? 0));
|
|
if (lnAmount != 0) {
|
|
_amountControllers[_selectedOutput].text = lnAmount;
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
Future<void> _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async {
|
|
if (uri.contains('@') || paymentRequest.address.contains('@')) return;
|
|
|
|
if (OpenCryptoPayService.isOpenCryptoPayQR(uri) &&
|
|
widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) {
|
|
final request = await widget.sendViewModel.getOpenCryptoPayRequest(uri);
|
|
if (request == null) return;
|
|
_applyPaymentRequest(request);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final result = await widget.paymentViewModel.processAddress(uri);
|
|
|
|
if (paymentRequest.contractAddress != null) {
|
|
await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
|
|
}
|
|
|
|
switch (result.type) {
|
|
case PaymentFlowType.singleWallet:
|
|
case PaymentFlowType.multipleWallets:
|
|
case PaymentFlowType.noWallets:
|
|
await _showPaymentConfirmation(
|
|
widget.paymentViewModel,
|
|
widget.walletSwitcherViewModel,
|
|
paymentRequest,
|
|
result,
|
|
);
|
|
break;
|
|
case PaymentFlowType.evmNetworkSelection:
|
|
await _showTokenSelectionFlow(
|
|
widget.paymentViewModel,
|
|
widget.walletSwitcherViewModel,
|
|
paymentRequest,
|
|
fixedNetwork: result.walletType,
|
|
);
|
|
break;
|
|
case PaymentFlowType.currentWalletCompatible:
|
|
case PaymentFlowType.error:
|
|
case PaymentFlowType.incompatible:
|
|
_applyPaymentRequest(paymentRequest);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
printV('Payment flow error: $e');
|
|
_applyPaymentRequest(paymentRequest);
|
|
}
|
|
}
|
|
|
|
Future<void> _showPaymentConfirmation(
|
|
PaymentViewModel paymentViewModel,
|
|
WalletSwitcherViewModel walletSwitcherViewModel,
|
|
PaymentRequest paymentRequest,
|
|
PaymentFlowResult result,
|
|
) async {
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
|
|
await showModalBottomSheet<void>(
|
|
context: context,
|
|
isDismissible: true,
|
|
isScrollControlled: true,
|
|
builder: (BuildContext context) {
|
|
return PaymentConfirmationBottomSheet(
|
|
paymentFlowResult: result,
|
|
paymentViewModel: paymentViewModel,
|
|
walletSwitcherViewModel: walletSwitcherViewModel,
|
|
paymentRequest: paymentRequest,
|
|
onSelectWallet: () => _handleSelectWallet(
|
|
paymentViewModel,
|
|
walletSwitcherViewModel,
|
|
paymentRequest,
|
|
result,
|
|
),
|
|
onChangeWallet: () => _handleChangeWallet(
|
|
paymentViewModel,
|
|
walletSwitcherViewModel,
|
|
paymentRequest,
|
|
result,
|
|
),
|
|
onSwap: (bottomSheetContext) =>
|
|
_handleSwapFlow(paymentViewModel, result, bottomSheetContext),
|
|
onSwitchNetwork: () => _handleSwitchNetwork(
|
|
paymentViewModel,
|
|
walletSwitcherViewModel,
|
|
paymentRequest,
|
|
result,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _showTokenSelectionFlow(
|
|
PaymentViewModel paymentViewModel,
|
|
WalletSwitcherViewModel walletSwitcherViewModel,
|
|
PaymentRequest paymentRequest, {
|
|
WalletType? fixedNetwork,
|
|
}) async {
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
|
|
await showModalBottomSheet<void>(
|
|
context: context,
|
|
isDismissible: true,
|
|
isScrollControlled: true,
|
|
builder: (BuildContext context) {
|
|
return TokenSelectionBottomSheet(
|
|
paymentViewModel: paymentViewModel,
|
|
paymentRequest: paymentRequest,
|
|
fixedNetwork: fixedNetwork,
|
|
onNext: (PaymentFlowResult newResult) {
|
|
final selectedChainId = newResult.chainId;
|
|
final isCompatible =
|
|
selectedChainId == evm!.getSelectedChainId(widget.sendViewModel.wallet);
|
|
|
|
if (isCompatible) {
|
|
widget.sendViewModel.setSelectedCryptoCurrency(
|
|
newResult.addressDetectionResult!.detectedCurrency!.title,
|
|
);
|
|
_applyPaymentRequest(paymentRequest);
|
|
} else {
|
|
_showPaymentConfirmation(
|
|
paymentViewModel,
|
|
walletSwitcherViewModel,
|
|
paymentRequest,
|
|
newResult,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _handleSelectWallet(
|
|
PaymentViewModel paymentViewModel,
|
|
WalletSwitcherViewModel walletSwitcherViewModel,
|
|
PaymentRequest paymentRequest,
|
|
PaymentFlowResult result,
|
|
) async {
|
|
Navigator.of(context).pop();
|
|
|
|
await showModalBottomSheet<WalletInfo>(
|
|
context: context,
|
|
isDismissible: true,
|
|
isScrollControlled: true,
|
|
builder: (BuildContext dialogContext) {
|
|
return WalletSwitcherBottomSheet(
|
|
viewModel: walletSwitcherViewModel,
|
|
filterWalletType: paymentViewModel.detectedWalletType,
|
|
);
|
|
},
|
|
);
|
|
|
|
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
|
|
|
if (success) {
|
|
if (isEVMCompatibleChain(widget.sendViewModel.wallet.type) && result.chainId != null) {
|
|
final appStore = getIt.get<AppStore>();
|
|
final node = appStore.settingsStore.getCurrentNode(
|
|
widget.sendViewModel.wallet.type,
|
|
chainId: result.chainId,
|
|
);
|
|
await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node);
|
|
}
|
|
|
|
await widget.sendViewModel.wallet.updateBalance();
|
|
|
|
final detectedCurrency = result.addressDetectionResult!.detectedCurrency;
|
|
if (detectedCurrency != null) {
|
|
widget.sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title);
|
|
}
|
|
|
|
_applyPaymentRequest(paymentRequest);
|
|
}
|
|
}
|
|
|
|
Future<void> _handleChangeWallet(
|
|
PaymentViewModel paymentViewModel,
|
|
WalletSwitcherViewModel walletSwitcherViewModel,
|
|
PaymentRequest paymentRequest,
|
|
PaymentFlowResult result,
|
|
) async {
|
|
if (mounted && Navigator.of(context).canPop()) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
if (result.type == PaymentFlowType.singleWallet && result.wallet != null) {
|
|
walletSwitcherViewModel.selectWallet(result.wallet!);
|
|
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
|
if (success) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isDismissible: false,
|
|
builder: (BuildContext context) {
|
|
loadingBottomSheetContext = context;
|
|
return LoadingBottomSheet(
|
|
titleText: S.of(context).loading_your_wallet,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
// If EVM wallet and chainId is specified, switch to that chain
|
|
if (isEVMCompatibleChain(widget.sendViewModel.wallet.type) && result.chainId != null) {
|
|
final appStore = getIt.get<AppStore>();
|
|
final node = appStore.settingsStore.getCurrentNode(
|
|
widget.sendViewModel.wallet.type,
|
|
chainId: result.chainId,
|
|
);
|
|
await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node);
|
|
}
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
if (loadingBottomSheetContext != null &&
|
|
loadingBottomSheetContext!.mounted &&
|
|
Navigator.canPop(loadingBottomSheetContext!)) {
|
|
Navigator.of(loadingBottomSheetContext!).pop();
|
|
}
|
|
|
|
await widget.sendViewModel.wallet.updateBalance();
|
|
widget.sendViewModel
|
|
.setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title);
|
|
_applyPaymentRequest(paymentRequest);
|
|
}
|
|
} else if (result.wallets.isNotEmpty && result.wallets.length == 1) {
|
|
walletSwitcherViewModel.selectWallet(result.wallets.first);
|
|
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
|
if (success) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (context.mounted) {
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isDismissible: false,
|
|
builder: (BuildContext context) {
|
|
loadingBottomSheetContext = context;
|
|
return LoadingBottomSheet(
|
|
titleText: S.of(context).loading_your_wallet,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
// If EVM wallet and chainId is specified, switch to that chain
|
|
if (isEVMCompatibleChain(widget.sendViewModel.wallet.type) && result.chainId != null) {
|
|
final appStore = getIt.get<AppStore>();
|
|
final node = appStore.settingsStore.getCurrentNode(
|
|
widget.sendViewModel.wallet.type,
|
|
chainId: result.chainId,
|
|
);
|
|
await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node);
|
|
}
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
|
Navigator.of(loadingBottomSheetContext!).pop();
|
|
}
|
|
|
|
await widget.sendViewModel.wallet.updateBalance();
|
|
widget.sendViewModel
|
|
.setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title);
|
|
_applyPaymentRequest(paymentRequest);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _handleSwitchNetwork(
|
|
PaymentViewModel paymentViewModel,
|
|
WalletSwitcherViewModel walletSwitcherViewModel,
|
|
PaymentRequest paymentRequest,
|
|
PaymentFlowResult result,
|
|
) async {
|
|
if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) return;
|
|
|
|
if (context.mounted && Navigator.of(context).canPop()) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
try {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (context.mounted) {
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isDismissible: false,
|
|
builder: (BuildContext context) {
|
|
loadingBottomSheetContext = context;
|
|
return LoadingBottomSheet(
|
|
titleText: S.of(context).loading_your_wallet,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
await paymentViewModel.selectChain();
|
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
|
Navigator.of(loadingBottomSheetContext!).pop();
|
|
}
|
|
|
|
await widget.sendViewModel.wallet.updateBalance();
|
|
final detectedCurrency = result.addressDetectionResult?.detectedCurrency;
|
|
if (detectedCurrency != null) {
|
|
widget.sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title);
|
|
}
|
|
_applyPaymentRequest(paymentRequest);
|
|
} catch (e) {
|
|
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
|
Navigator.of(loadingBottomSheetContext!).pop();
|
|
}
|
|
printV('Switch network error: $e');
|
|
}
|
|
}
|
|
|
|
/// Apply payment request to current form
|
|
void _applyPaymentRequest(PaymentRequest paymentRequest) {
|
|
if (widget.sendViewModel.usePayjoin) {
|
|
widget.sendViewModel.payjoinUri = paymentRequest.pjUri;
|
|
}
|
|
_addressControllers[_selectedOutput].text = paymentRequest.address;
|
|
if (paymentRequest.amount.isNotEmpty) {
|
|
try{
|
|
_amountControllers[_selectedOutput].text = widget.sendViewModel.amountParsingProxy
|
|
.getDisplayCryptoAmount(
|
|
paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency);
|
|
} catch(e) {
|
|
|
|
}
|
|
}
|
|
_memoControllers[_selectedOutput].text = paymentRequest.note;
|
|
}
|
|
|
|
Future<void> _handleSwapFlow(
|
|
PaymentViewModel paymentViewModel,
|
|
PaymentFlowResult result,
|
|
BuildContext bottomSheetContext,
|
|
) async {
|
|
Navigator.of(bottomSheetContext).pop();
|
|
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
if (!mounted) return;
|
|
|
|
final bottomSheet = getIt.get<SwapConfirmationBottomSheet>(param1: result);
|
|
await showModalBottomSheet<Trade?>(
|
|
context: context,
|
|
isDismissible: true,
|
|
isScrollControlled: true,
|
|
builder: (BuildContext context) => bottomSheet,
|
|
);
|
|
}
|
|
|
|
void showErrorValidationAlert(BuildContext context) {
|
|
int emptyAddressIndex = -1;
|
|
for (int i = 0; i < widget.sendViewModel.outputs.length; i++) {
|
|
if (widget.sendViewModel.outputs[i].address.isEmpty) {
|
|
emptyAddressIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
showPopUp<void>(
|
|
context: context,
|
|
builder: (context) => AlertWithOneAction(
|
|
alertTitle: S.of(context).error,
|
|
alertContent: emptyAddressIndex == -1
|
|
? S.of(context).check_receiver_forms
|
|
: S.of(context).enter_recipient_address,
|
|
buttonText: S.of(context).ok,
|
|
buttonAction: () => Navigator.of(context).pop(),
|
|
),
|
|
);
|
|
if (emptyAddressIndex != -1) {
|
|
_setOutput(emptyAddressIndex);
|
|
}
|
|
}
|
|
|
|
bool isRegularElectrumAddress(String address) {
|
|
final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch];
|
|
final excludedPatterns = [
|
|
RegExp(AddressValidator.silentPaymentAddressPatternMainnet),
|
|
RegExp(AddressValidator.silentPaymentAddressPatternTestnet),
|
|
RegExp(AddressValidator.mWebAddressPattern),
|
|
RegExp(AddressValidator.bolt11InvoiceMatcher),
|
|
];
|
|
|
|
final trimmed = address.trim();
|
|
|
|
bool isValid = false;
|
|
for (final type in supportedTypes) {
|
|
final addressPattern = AddressValidator.getAddressFromStringPattern(type);
|
|
if (addressPattern != null) {
|
|
final regex = RegExp('^$addressPattern\$');
|
|
if (regex.hasMatch(trimmed)) {
|
|
isValid = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (final pattern in excludedPatterns) {
|
|
if (pattern.hasMatch(trimmed)) return false;
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
Future<void> pickTransactionPriority(BuildContext pageContext, Output output) async {
|
|
final items = priorityForWalletType(widget.sendViewModel.walletType);
|
|
final selectedItem = items.indexOf(widget.sendViewModel.feesViewModel.transactionPriority);
|
|
final customItemIndex = widget.sendViewModel.feesViewModel.getCustomPriorityIndex(items);
|
|
final isBitcoinWallet = widget.sendViewModel.walletType == WalletType.bitcoin;
|
|
final maxCustomFeeRate = widget.sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
|
|
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
|
|
await showCupertinoModalBottomSheet(
|
|
context: pageContext,
|
|
expand: false,
|
|
builder: (BuildContext modalContext) {
|
|
int selectedIdx = selectedItem;
|
|
return Observer(builder: (context) {
|
|
double? customFeeRate = isBitcoinWallet
|
|
? widget.sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble()
|
|
: null;
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return IntrinsicHeight(
|
|
// height: MediaQuery.of(context).size.height*0.4,
|
|
child: ModalNavigator(
|
|
parentContext: modalContext,
|
|
heightMode: ModalHeightModes.autoLock,
|
|
rootPage: Material(
|
|
child: NewPicker(
|
|
title: S.of(context).set_fees,
|
|
description: S.of(context).set_fees_desc,
|
|
sliderPageTitle: S.of(context).custom_fee,
|
|
sliderInitialValue: customFeeRate,
|
|
sliderMaxValue: maxCustomFeeRate,
|
|
sliderValueDescription: "sat/byte",
|
|
items: items
|
|
.map((item) => PickerItem<TransactionPriority>(
|
|
title: item.title,
|
|
subtitle: item.description,
|
|
hint: item.hint,
|
|
value: item,
|
|
isSliderItem: items.indexOf(item) == customItemIndex,
|
|
))
|
|
.toList(),
|
|
onItemSelected: (TransactionPriority priority) async {
|
|
widget.sendViewModel.feesViewModel.setTransactionPriority(priority);
|
|
setState(() => selectedIdx = items.indexOf(priority));
|
|
await output.calculateEstimatedFee();
|
|
},
|
|
onSliderChanged: (double value) {
|
|
widget.sendViewModel.feesViewModel.customBitcoinFeeRate = value.round();
|
|
},
|
|
selectedIndex: selectedIdx),
|
|
)),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
String _wrapAmount(String amount, int maxChars) {
|
|
return amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}...";
|
|
}
|
|
}
|
|
|
|
class SendHelpPage extends StatelessWidget {
|
|
const SendHelpPage({super.key, required this.content});
|
|
|
|
final SendPageHelpContent content;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
ModalTopBar(
|
|
title: content.title,
|
|
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
|
onLeadingPressed: Navigator.of(context).pop,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
|
child: Column(
|
|
spacing: 12,
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
HashImageWidget(imageUrl:content.imagePath),
|
|
Text(
|
|
content.description,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: Theme.of(context).colorScheme.onSurface),
|
|
),
|
|
if (content.disclaimer != null) ...[
|
|
SizedBox(),
|
|
SizedBox(),
|
|
Text(content.disclaimer!,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
|
]
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 18),
|
|
child: NewPrimaryButton(
|
|
onPressed: Navigator.of(context).pop,
|
|
text: S.of(context).i_understand,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
textColor: Theme.of(context).colorScheme.onPrimary))
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|