/* * 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 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import '../../app_config.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/expandable.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; import '../home_view/home_view.dart'; import '../send_view/sub_widgets/building_transaction_dialog.dart'; import 'confirm_change_now_send.dart'; class SendFromView extends ConsumerStatefulWidget { const SendFromView({ super.key, required this.coin, required this.trade, required this.amount, required this.address, this.shouldPopRoot = false, this.fromDesktopStep4 = false, }); static const String routeName = "/sendFrom"; final CryptoCurrency coin; final Amount amount; final String address; final Trade trade; final bool shouldPopRoot; final bool fromDesktopStep4; @override ConsumerState createState() => _SendFromViewState(); } class _SendFromViewState extends ConsumerState { late final CryptoCurrency coin; late final Amount amount; late final String address; late final Trade trade; @override void initState() { coin = widget.coin; address = widget.address; amount = widget.amount; trade = widget.trade; super.initState(); } @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final walletIds = ref .watch(pWallets) .wallets .where((e) => e.info.coin == coin) .map((e) => e.walletId) .toList(); final isDesktop = Util.isDesktop; return ConditionalParent( condition: !isDesktop, builder: (child) { return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { Navigator.of(context).pop(); }, ), title: Text( "Send from", style: STextStyles.navBarTitle(context), ), ), body: Padding( padding: const EdgeInsets.all(16), child: child, ), ), ); }, child: ConditionalParent( condition: isDesktop, builder: (child) => DesktopDialog( maxHeight: double.infinity, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.only( left: 32, ), child: Text( "Send from ${AppConfig.prefix}", style: STextStyles.desktopH3(context), ), ), DesktopDialogCloseButton( onPressedOverride: Navigator.of( context, rootNavigator: widget.shouldPopRoot, ).pop, ), ], ), Padding( padding: const EdgeInsets.only( left: 32, right: 32, bottom: 32, ), child: child, ), ], ), ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Row( children: [ Text( "You need to send ${ref.watch(pAmountFormatter(coin)).format(amount)}", style: isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) : STextStyles.itemSubtitle(context), ), ], ), const SizedBox( height: 16, ), ConditionalParent( condition: !isDesktop, builder: (child) => Expanded( child: child, ), child: ListView.builder( primary: isDesktop ? false : null, shrinkWrap: isDesktop, itemCount: walletIds.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: SendFromCard( walletId: walletIds[index], amount: amount, address: address, trade: trade, fromDesktopStep4: widget.fromDesktopStep4, ), ); }, ), ), ], ), ), ); } } class SendFromCard extends ConsumerStatefulWidget { const SendFromCard({ super.key, required this.walletId, required this.amount, required this.address, required this.trade, this.fromDesktopStep4 = false, }); final String walletId; final Amount amount; final String address; final Trade trade; final bool fromDesktopStep4; @override ConsumerState createState() => _SendFromCardState(); } class _SendFromCardState extends ConsumerState { late final String walletId; late final Amount amount; late final String address; late final Trade trade; Future _send({bool? shouldSendPublicFiroFunds}) async { final coin = ref.read(pWalletCoin(walletId)); try { bool wasCancelled = false; final wallet = ref.read(pWallets).getWallet(walletId); unawaited( showDialog( context: context, useSafeArea: false, barrierDismissible: false, builder: (context) { return ConditionalParent( condition: Util.isDesktop, builder: (child) => DesktopDialog( maxWidth: 400, maxHeight: double.infinity, child: Padding( padding: const EdgeInsets.all(32), child: child, ), ), child: BuildingTransactionDialog( coin: coin, isSpark: wallet is FiroWallet && shouldSendPublicFiroFunds != true, onCancel: () { wasCancelled = true; Navigator.of(context).pop(); }, ), ); }, ), ); // Currently CwBasedInterface wallets (xmr/wow) shouldn't even have // access to this screen but this is needed to get past an error that // would occur only to lead to another error which is why xmr/wow wallets // don't have access to this screen currently if (wallet is LibMoneroWallet) { await wallet.init(); await wallet.open(); } final time = Future.delayed( const Duration( milliseconds: 2500, ), ); TxData txData; Future txDataFuture; // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { final memo = coin is Stellar ? trade.payInExtraId.isNotEmpty ? trade.payInExtraId : null : null; txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ ( address: address, amount: amount, isChange: false, ), ], memo: memo, feeRateType: FeeRateType.average, ), ); } else { final firoWallet = wallet as FiroWallet; // otherwise do firo send based on balance selected if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ ( address: address, amount: amount, isChange: false, ), ], feeRateType: FeeRateType.average, ), ); } else { txDataFuture = firoWallet.prepareSendSpark( txData: TxData( recipients: [ ( address: address, amount: amount, isChange: false, ), ], // feeRateType: FeeRateType.average, ), ); } } final results = await Future.wait([ txDataFuture, time, ]); txData = results.first as TxData; if (!wasCancelled) { // pop building dialog if (mounted) { Navigator.of( context, rootNavigator: Util.isDesktop, ).pop(); } txData = txData.copyWith( note: "${trade.payInCurrency.toUpperCase()}/" "${trade.payOutCurrency.toUpperCase()} exchange", ); if (mounted) { await Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => ConfirmChangeNowSendView( txData: txData, walletId: walletId, routeOnSuccessName: Util.isDesktop ? DesktopExchangeView.routeName : HomeView.routeName, trade: trade, shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, fromDesktopStep4: widget.fromDesktopStep4, ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, ), ), ); } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Error); if (mounted) { // pop building dialog Navigator.of(context).pop(); await showDialog( context: context, useSafeArea: false, barrierDismissible: true, builder: (context) { return StackDialog( title: "Transaction failed", message: e.toString(), rightButton: TextButton( style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle(context), child: Text( "Ok", style: STextStyles.button(context).copyWith( color: Theme.of(context) .extension()! .buttonTextSecondary, ), ), onPressed: () { Navigator.of(context).pop(); }, ), ); }, ); } } } @override void initState() { walletId = widget.walletId; amount = widget.amount; address = widget.address; trade = widget.trade; super.initState(); } @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(walletId); final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale), ); final coin = ref.watch(pWalletCoin(walletId)); final isFiro = coin is Firo; return RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ConditionalParent( condition: isFiro, builder: (child) => Expandable( header: Container( color: Colors.transparent, child: Padding( padding: const EdgeInsets.all(12), child: child, ), ), body: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ MaterialButton( splashColor: Theme.of(context).extension()!.highlight, key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), padding: const EdgeInsets.all(0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), onPressed: () async { if (mounted) { unawaited( _send( shouldSendPublicFiroFunds: false, ), ); } }, child: Container( color: Colors.transparent, child: Padding( padding: const EdgeInsets.only( top: 6, left: 16, right: 16, bottom: 6, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Use private balance", style: STextStyles.itemSubtitle(context), ), Text( ref.watch(pAmountFormatter(coin)).format( ref .watch(pWalletBalanceTertiary(walletId)) .spendable, ), style: STextStyles.itemSubtitle(context), ), ], ), SvgPicture.asset( Assets.svg.chevronRight, height: 14, width: 7, color: Theme.of(context) .extension()! .infoItemLabel, ), ], ), ), ), ), MaterialButton( splashColor: Theme.of(context).extension()!.highlight, key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), padding: const EdgeInsets.all(0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), onPressed: () async { if (mounted) { unawaited( _send( shouldSendPublicFiroFunds: true, ), ); } }, child: Container( color: Colors.transparent, child: Padding( padding: const EdgeInsets.only( top: 6, left: 16, right: 16, bottom: 6, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Use public balance", style: STextStyles.itemSubtitle(context), ), Text( ref.watch(pAmountFormatter(coin)).format( ref .watch(pWalletBalance(walletId)) .spendable, ), style: STextStyles.itemSubtitle(context), ), ], ), SvgPicture.asset( Assets.svg.chevronRight, height: 14, width: 7, color: Theme.of(context) .extension()! .infoItemLabel, ), ], ), ), ), ), const SizedBox( height: 6, ), ], ), ), child: ConditionalParent( condition: !isFiro, builder: (child) => MaterialButton( splashColor: Theme.of(context).extension()!.highlight, key: Key("walletsSheetItemButtonKey_$walletId"), padding: const EdgeInsets.all(8), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), onPressed: () async { if (mounted) { unawaited( _send(), ); } }, child: child, ), child: Row( children: [ Container( decoration: BoxDecoration( color: ref.watch(pCoinColor(coin)).withOpacity(0.5), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), child: Padding( padding: const EdgeInsets.all(6), child: SvgPicture.file( File( ref.watch( coinIconProvider(coin), ), ), width: 24, height: 24, ), ), ), const SizedBox( width: 12, ), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ref.watch(pWalletName(walletId)), style: STextStyles.titleBold12(context), ), if (!isFiro) const SizedBox( height: 2, ), if (!isFiro) Text( ref.watch(pAmountFormatter(coin)).format( ref.watch(pWalletBalance(walletId)).spendable, ), style: STextStyles.itemSubtitle(context), ), ], ), ), ], ), ), ), ); } }