From cac7b8a83dae4b902be4af3a1da54f8d32dfdbb0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 12 Jun 2024 13:16:49 -0500 Subject: [PATCH] add WIP boost transaction dialog --- .../tx_v2/boost_transaction_view.dart | 2049 +++++++++++++++++ .../tx_v2/transaction_v2_details_view.dart | 37 +- 2 files changed, 2085 insertions(+), 1 deletion(-) create mode 100644 lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart new file mode 100644 index 000000000..4f7c34f9b --- /dev/null +++ b/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart @@ -0,0 +1,2049 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../../models/isar/models/ethereum/eth_contract.dart'; +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../providers/db/main_db_provider.dart'; +import '../../../../providers/global/address_book_service_provider.dart'; +import '../../../../providers/providers.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/amount/amount.dart'; +import '../../../../utilities/amount/amount_formatter.dart'; +import '../../../../utilities/block_explorers.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/logger.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart'; +import '../../../../wallets/isar/models/spark_coin.dart'; +import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../../../widgets/background.dart'; +import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/icon_widgets/copy_icon.dart'; +import '../../../../widgets/icon_widgets/pencil_icon.dart'; +import '../../../../widgets/rounded_white_container.dart'; +import '../../../../widgets/stack_dialog.dart'; +import '../../sub_widgets/tx_icon.dart'; +import '../../wallet_view.dart'; +import '../dialogs/cancelling_transaction_progress_dialog.dart'; + +class BoostTransactionView extends ConsumerStatefulWidget { + const BoostTransactionView({ + super.key, + required this.transaction, + required this.walletId, + required this.coin, + }); + + static const String routeName = "/boostTransaction"; + + final TransactionV2 transaction; + final String walletId; + final CryptoCurrency coin; + + @override + ConsumerState createState() => + _BoostTransactionViewState(); +} + +class _BoostTransactionViewState extends ConsumerState { + late final bool isDesktop; + late TransactionV2 _transaction; + late final String walletId; + + late final CryptoCurrency coin; + late final Amount amount; + late final Amount fee; + late final String amountPrefix; + late final String unit; + late final int minConfirms; + late final EthContract? ethContract; + + bool get isTokenTx => ethContract != null; + + late final List<({List addresses, Amount amount})> data; + + bool showFeePending = false; + + String? _sparkMemo; + + @override + void initState() { + isDesktop = Util.isDesktop; + _transaction = widget.transaction; + walletId = widget.walletId; + + coin = widget.coin; + + if (_transaction.subType == TransactionSubType.ethToken) { + ethContract = ref + .read(mainDBProvider) + .getEthContractSync(_transaction.contractAddress!); + + unit = ethContract!.symbol; + } else { + ethContract = null; + unit = coin.ticker; + } + + minConfirms = + ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms; + + final fractionDigits = ethContract?.decimals ?? coin.fractionDigits; + + fee = _transaction.getFee(fractionDigits: fractionDigits); + + if (_transaction.subType == TransactionSubType.cashFusion || + _transaction.type == TransactionType.sentToSelf) { + amountPrefix = ""; + } else { + amountPrefix = _transaction.type == TransactionType.outgoing ? "-" : "+"; + } + + if (_transaction.isEpiccashTransaction) { + switch (_transaction.type) { + case TransactionType.outgoing: + case TransactionType.unknown: + amount = _transaction.getAmountSentFromThisWallet( + fractionDigits: fractionDigits, + ); + break; + + case TransactionType.incoming: + case TransactionType.sentToSelf: + amount = _transaction.getAmountReceivedInThisWallet( + fractionDigits: fractionDigits, + ); + break; + } + data = _transaction.outputs + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ) + ), + ) + .toList(); + } else if (_transaction.subType == TransactionSubType.cashFusion) { + amount = _transaction.getAmountReceivedInThisWallet( + fractionDigits: fractionDigits, + ); + data = _transaction.outputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ) + ), + ) + .toList(); + } else { + switch (_transaction.type) { + case TransactionType.outgoing: + amount = _transaction.getAmountSentFromThisWallet( + fractionDigits: fractionDigits, + ); + data = _transaction.outputs + .where((e) => !e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ) + ), + ) + .toList(); + break; + + case TransactionType.incoming: + case TransactionType.sentToSelf: + if (_transaction.subType == TransactionSubType.sparkMint || + _transaction.subType == TransactionSubType.sparkSpend) { + _sparkMemo = ref + .read(mainDBProvider) + .isar + .sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .memoIsNotEmpty() + .and() + .heightEqualTo(_transaction.height) + .anyOf( + _transaction.outputs + .where( + (e) => + e.walletOwns && + e.addresses.isEmpty && + e.scriptPubKeyHex.length >= 488, + ) + .map((e) => e.scriptPubKeyHex.substring(2, 488)) + .toList(), + (q, element) => q.serializedCoinB64StartsWith(element), + ) + .memoProperty() + .findFirstSync(); + } + + if (_transaction.subType == TransactionSubType.sparkMint) { + amount = _transaction.getAmountSparkSelfMinted( + fractionDigits: fractionDigits, + ); + } else if (_transaction.subType == TransactionSubType.sparkSpend) { + final changeAddress = + (ref.read(pWallets).getWallet(walletId) as SparkInterface) + .sparkChangeAddress; + amount = Amount( + rawValue: _transaction.outputs + .where( + (e) => e.walletOwns && !e.addresses.contains(changeAddress), + ) + .fold(BigInt.zero, (p, e) => p + e.value), + fractionDigits: coin.fractionDigits, + ); + } else { + amount = _transaction.getAmountReceivedInThisWallet( + fractionDigits: fractionDigits, + ); + } + data = _transaction.outputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ) + ), + ) + .toList(); + break; + + case TransactionType.unknown: + amount = _transaction.getAmountSentFromThisWallet( + fractionDigits: fractionDigits, + ); + data = _transaction.inputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ) + ), + ) + .toList(); + break; + } + } + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( + currentChainHeight: height, + minConfirms: minConfirms, + ); + + Future fetchContactNameFor(String address) async { + if (address.isEmpty) { + return address; + } + try { + final contacts = ref.read(addressBookServiceProvider).contacts.where( + (element) => element.addresses + .where((element) => element.address == address) + .isNotEmpty, + ); + if (contacts.isNotEmpty) { + return contacts.first.name; + } else { + return address; + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + return address; + } + } + + Future showExplorerWarning(String explorer) async { + final bool? shouldContinue = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + if (!isDesktop) { + return StackDialog( + title: "Attention", + message: + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + icon: Row( + children: [ + Consumer( + builder: (_, ref, __) { + return Checkbox( + value: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.hideBlockExplorerWarning), + ), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }, + ), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ), + ], + ), + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ); + } else { + return DesktopDialog( + maxWidth: 550, + maxHeight: 300, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Attention", + style: STextStyles.desktopH2(context), + ), + Row( + children: [ + Consumer( + builder: (_, ref, __) { + return Checkbox( + value: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hideBlockExplorerWarning, + ), + ), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }, + ), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + Text( + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 35), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(false); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(true); + }, + ), + ], + ), + ], + ), + ), + ); + } + }, + ); + return shouldContinue ?? false; + } + + @override + Widget build(BuildContext context) { + final currentHeight = ref.watch(pWalletChainHeight(walletId)); + + final String outputLabel; + + if (_transaction.subType == TransactionSubType.cashFusion) { + outputLabel = "Outputs"; + } else if (_transaction.type == TransactionType.incoming) { + if (data.length == 1 && data.first.addresses.length == 1) { + outputLabel = "Receiving address"; + } else { + outputLabel = "Receiving addresses"; + } + } else { + outputLabel = "Sent to"; + } + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: child, + ), + child: Scaffold( + backgroundColor: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Boost transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Boost transaction", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: isDesktop + ? const EdgeInsets.only( + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context) + .extension()! + .backgroundAppBar + : null, + padding: const EdgeInsets.all(0), + child: child, + ); + }, + child: SingleChildScrollView( + primary: isDesktop ? false : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, + currentHeight: currentHeight, + coin: coin, + ), + const SizedBox( + width: 16, + ), + SelectableText( + whatIsIt( + _transaction, + currentHeight, + ), + style: + STextStyles.desktopTextMedium( + context, + ), + ), + ], + ), + Column( + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + SelectableText( + "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.titleBold12( + context, + ), + ), + const SizedBox( + height: 2, + ), + if (ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + )) + SelectableText( + "$amountPrefix${(amount.decimal * ref.watch( + priceAnd24hChangeNotifierProvider + .select( + (value) => value + .getPrice( + coin, + ) + .item1, + ), + )).toAmount(fractionDigits: 2).fiatString( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${ref.watch( + prefsChangeNotifierProvider + .select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + ], + ), + if (!isDesktop) + TxIcon( + transaction: _transaction, + currentHeight: currentHeight, + coin: coin, + ), + ], + ), + ), + ), + ), + + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + whatIsIt( + _transaction, + currentHeight, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: _transaction.type == + TransactionType + .outgoing && + _transaction.subType != + TransactionSubType + .cashFusion + ? Theme.of(context) + .extension()! + .accentColorOrange + : Theme.of(context) + .extension()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12(context), + ), + // ), + // ), + ], + ), + ), + if (!((coin is Monero || coin is Wownero) && + _transaction.type == + TransactionType.outgoing) && + !((coin is Firo) && + _transaction.subType == + TransactionSubType.mint)) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!((coin is Monero || coin is Wownero) && + _transaction.type == + TransactionType.outgoing) && + !((coin is Firo) && + _transaction.subType == + TransactionSubType.mint)) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: kDebugMode, + builder: (child) { + return Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + child, + // CustomTextButton( + // text: "Info", + // onTap: () async { + // final adr = await ref + // .read(mainDBProvider) + // .getAddress(walletId, + // addresses.first); + // if (adr != null && + // mounted) { + // if (isDesktop) { + // await showDialog< + // void>( + // context: context, + // builder: (_) => + // DesktopDialog( + // maxHeight: double + // .infinity, + // child: + // AddressDetailsView( + // addressId: + // adr.id, + // walletId: widget + // .walletId, + // ), + // ), + // ); + // } else { + // await Navigator.of( + // context) + // .pushNamed( + // AddressDetailsView + // .routeName, + // arguments: Tuple2( + // adr.id, + // widget.walletId, + // ), + // ); + // } + // } + // }, + // ) + ], + ); + }, + child: Text( + outputLabel, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + ), + const SizedBox( + height: 8, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (data.length == 1 && + data.first.addresses.length == + 1) + FutureBuilder( + future: fetchContactNameFor( + data.first.addresses.first, + ), + builder: ( + builderContext, + AsyncSnapshot + snapshot, + ) { + String + addressOrContactName = + data.first.addresses + .first; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context, + ), + ); + }, + ) + else + for (int i = 0; + i < data.length; + i++) + ConditionalParent( + condition: i > 0, + builder: (child) => Column( + crossAxisAlignment: + CrossAxisAlignment + .stretch, + children: [ + const _Divider(), + child, + ], + ), + child: Padding( + padding: + const EdgeInsets.all( + 8.0, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + ...data[i] + .addresses + .map( + (e) { + return FutureBuilder( + future: + fetchContactNameFor( + e, + ), + builder: ( + builderContext, + AsyncSnapshot< + String> + snapshot, + ) { + final String + addressOrContactName; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot + .hasData) { + addressOrContactName = + snapshot + .data!; + } else { + addressOrContactName = + e; + } + + return OutputCard( + address: + addressOrContactName, + amount: data[ + i] + .amount, + coin: coin, + ); + }, + ); + }, + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + // if (isDesktop) + // IconCopyButton( + // data: addresses.first, + // ), + ], + ), + ), + if (coin is Epiccash) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "On chain note", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + const SizedBox( + height: 8, + ), + SelectableText( + _transaction.onChainNote ?? "", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + ), + if (isDesktop) + IconCopyButton( + data: _transaction.onChainNote ?? "", + ), + ], + ), + ), + // isDesktop + // ? const _Divider() + // : const SizedBox( + // height: 12, + // ), + // RoundedWhiteContainer( + // padding: isDesktop + // ? const EdgeInsets.all(16) + // : const EdgeInsets.all(12), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: + // MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // (coin is Epiccash) + // ? "Local Note" + // : "Note ", + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ) + // : STextStyles.itemSubtitle(context), + // ), + // isDesktop + // ? IconPencilButton( + // onPressed: () { + // showDialog( + // context: context, + // builder: (context) { + // return DesktopDialog( + // maxWidth: 580, + // maxHeight: 360, + // child: EditNoteView( + // txid: _transaction.txid, + // walletId: walletId, + // ), + // ); + // }, + // ); + // }, + // ) + // : GestureDetector( + // onTap: () { + // Navigator.of(context).pushNamed( + // EditNoteView.routeName, + // arguments: Tuple2( + // _transaction.txid, + // walletId, + // ), + // ); + // }, + // child: Row( + // children: [ + // SvgPicture.asset( + // Assets.svg.pencil, + // width: 10, + // height: 10, + // color: Theme.of(context) + // .extension< + // StackColors>()! + // .infoItemIcons, + // ), + // const SizedBox( + // width: 4, + // ), + // Text( + // "Edit", + // style: STextStyles.link2( + // context, + // ), + // ), + // ], + // ), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // SelectableText( + // ref + // .watch( + // pTransactionNote( + // ( + // txid: (coin is Epiccash) + // ? _transaction.slateId + // .toString() + // : _transaction.txid, + // walletId: walletId + // ), + // ), + // ) + // ?.value ?? + // "", + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of(context) + // .extension()! + // .textDark, + // ) + // : STextStyles.itemSubtitle12(context), + // ), + // ], + // ), + // ), + if (_sparkMemo != null) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (_sparkMemo != null) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "Memo", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + SelectableText( + _sparkMemo!, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + // isDesktop + // ? const _Divider() + // : const SizedBox( + // height: 12, + // ), + // RoundedWhiteContainer( + // padding: isDesktop + // ? const EdgeInsets.all(16) + // : const EdgeInsets.all(12), + // child: Row( + // mainAxisAlignment: + // MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Text( + // "Date", + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ) + // : STextStyles.itemSubtitle(context), + // ), + // if (isDesktop) + // const SizedBox( + // height: 2, + // ), + // if (isDesktop) + // SelectableText( + // Format.extractDateFrom( + // _transaction.timestamp, + // ), + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of(context) + // .extension()! + // .textDark, + // ) + // : STextStyles.itemSubtitle12( + // context, + // ), + // ), + // ], + // ), + // if (!isDesktop) + // SelectableText( + // Format.extractDateFrom( + // _transaction.timestamp, + // ), + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of(context) + // .extension()! + // .textDark, + // ) + // : STextStyles.itemSubtitle12(context), + // ), + // if (isDesktop) + // IconCopyButton( + // data: Format.extractDateFrom( + // _transaction.timestamp, + // ), + // ), + // ], + // ), + // ), + if (coin is! NanoCurrency) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin is! NanoCurrency) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder( + builder: (context) { + final String feeString = showFeePending + ? _transaction.isConfirmed( + currentHeight, + minConfirms, + ) + ? ref + .watch(pAmountFormatter(coin)) + .format( + fee, + ) + : "Pending" + : ref + .watch(pAmountFormatter(coin)) + .format( + fee, + ); + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction fee", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context, + ), + ), + // TODO [prio=high]: Boost tx fee UI. + ], + ), + if (!isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (isDesktop) + IconCopyButton(data: feeString), + ], + ); + }, + ), + ), + // isDesktop + // ? const _Divider() + // : const SizedBox( + // height: 12, + // ), + // RoundedWhiteContainer( + // padding: isDesktop + // ? const EdgeInsets.all(16) + // : const EdgeInsets.all(12), + // child: Builder( + // builder: (context) { + // final String height; + // + // if (widget.coin is Bitcoincash || + // widget.coin is Ecash) { + // height = + // "${_transaction.height != null && _transaction.height! > 0 ? _transaction.height! : "Pending"}"; + // } else { + // height = widget.coin is! Epiccash && + // _transaction.isConfirmed( + // currentHeight, + // minConfirms, + // ) + // ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" + // : _transaction.getConfirmations( + // currentHeight, + // ) > + // 0 + // ? "${_transaction.height}" + // : "Pending"; + // } + // + // return Row( + // mainAxisAlignment: + // MainAxisAlignment.spaceBetween, + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Text( + // "Block height", + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ) + // : STextStyles.itemSubtitle( + // context, + // ), + // ), + // if (isDesktop) + // const SizedBox( + // height: 2, + // ), + // if (isDesktop) + // SelectableText( + // height, + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of(context) + // .extension< + // StackColors>()! + // .textDark, + // ) + // : STextStyles.itemSubtitle12( + // context, + // ), + // ), + // ], + // ), + // if (!isDesktop) + // SelectableText( + // height, + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of(context) + // .extension()! + // .textDark, + // ) + // : STextStyles.itemSubtitle12( + // context, + // ), + // ), + // if (isDesktop) + // IconCopyButton(data: height), + // ], + // ); + // }, + // ), + // ), + + // if (kDebugMode) + // isDesktop + // ? const _Divider() + // : const SizedBox( + // height: 12, + // ), + // if (kDebugMode) + // RoundedWhiteContainer( + // padding: isDesktop + // ? const EdgeInsets.all(16) + // : const EdgeInsets.all(12), + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: + // MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Tx sub type", + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ) + // : STextStyles.itemSubtitle(context), + // ), + // SelectableText( + // _transaction.subType.toString(), + // style: isDesktop + // ? STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of(context) + // .extension()! + // .textDark, + // ) + // : STextStyles.itemSubtitle12(context), + // ), + // ], + // ), + // ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + const SizedBox( + height: 8, + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (coin is! Epiccash) + const SizedBox( + height: 8, + ), + if (coin is! Epiccash) + CustomTextButton( + text: "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider, + ) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}", + ); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: LaunchMode + .externalApplication, + ); + } catch (_) { + if (mounted) { + unawaited( + showDialog( + context: context, + builder: (_) => + StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } + } finally { + // Future.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], + ), + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.txid, + ), + ], + ), + ), + // if ((coin is FiroTestNet || coin is Firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin is FiroTestNet || coin is Firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin is Epiccash) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin is Epiccash) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // ), + // ), + ], + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.slateId ?? "Unknown", + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (whatIsIt( + _transaction, + currentHeight, + ) != + "Sending") + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: (coin is Epiccash && + _transaction.getConfirmations(currentHeight) < 1 && + _transaction.isCancelled == false) + ? ConditionalParent( + condition: isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, + ), + child: SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).extension()!.textError, + ), + ), + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + + if (wallet is EpiccashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + ), + ); + return; + } + + unawaited( + showDialog( + barrierDismissible: false, + context: context, + builder: (_) => + const CancellingTransactionProgressDialog(), + ), + ); + + final result = + await wallet.cancelPendingTransactionAndPost(id); + if (mounted) { + // pop progress dialog + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + wallet.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, + ), + ); + } else { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } + } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "ERROR: Wallet type is not Epic Cash", + context: context, + ), + ); + return; + } + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), + ), + ), + ) + : null, + ), + ); + } +} + +class OutputCard extends ConsumerWidget { + const OutputCard({ + super.key, + required this.address, + required this.amount, + required this.coin, + }); + + final String address; + final Amount amount; + final CryptoCurrency coin; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Address", + style: Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + SelectableText( + address, + style: Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format(amount), + style: Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ], + ), + ], + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + color: Theme.of(context).extension()!.backgroundAppBar, + ); + } +} + +class IconCopyButton extends StatelessWidget { + const IconCopyButton({ + super.key, + required this.data, + }); + + final String data; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: data)); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(5), + child: CopyIcon( + width: 16, + height: 16, + color: Theme.of(context).extension()!.textDark, + ), + ), + ), + ); + } +} + +class IconPencilButton extends StatelessWidget { + const IconPencilButton({ + super.key, + this.onPressed, + }); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () => onPressed?.call(), + child: Padding( + padding: const EdgeInsets.all(5), + child: PencilIcon( + width: 16, + height: 16, + color: Theme.of(context).extension()!.textDark, + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index d542070c2..e700d967b 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -58,6 +58,7 @@ import '../../sub_widgets/tx_icon.dart'; import '../../wallet_view.dart'; import '../dialogs/cancelling_transaction_progress_dialog.dart'; import '../edit_note_view.dart'; +import 'boost_transaction_view.dart'; class TransactionV2DetailsView extends ConsumerStatefulWidget { const TransactionV2DetailsView({ @@ -1347,7 +1348,41 @@ class _TransactionV2DetailsViewState CustomTextButton( text: "Boost transaction", onTap: () async { - // TODO [prio=high]: Show RBF UI. + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => + DesktopDialog( + maxHeight: + MediaQuery.of( + context) + .size + .height - + 64, + maxWidth: 580, + child: + BoostTransactionView( + transaction: + _transaction, + coin: coin, + walletId: walletId, + ), + ), + ); + } else { + unawaited( + Navigator.of(context) + .pushNamed( + BoostTransactionView + .routeName, + arguments: ( + tx: _transaction, + coin: coin, + walletId: walletId, + ), + ), + ); + } }, ), ],