stack_wallet/lib/pages/exchange_view/send_from_view.dart

670 lines
21 KiB
Dart

/*
* 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/wallet_mixin_interfaces/cw_based_interface.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<SendFromView> createState() => _SendFromViewState();
}
class _SendFromViewState extends ConsumerState<SendFromView> {
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<StackColors>()!.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<SendFromCard> createState() => _SendFromCardState();
}
class _SendFromCardState extends ConsumerState<SendFromCard> {
late final String walletId;
late final Amount amount;
late final String address;
late final Trade trade;
Future<void> _send({bool? shouldSendPublicFiroFunds}) async {
final coin = ref.read(pWalletCoin(walletId));
try {
bool wasCancelled = false;
final wallet = ref.read(pWallets).getWallet(walletId);
unawaited(
showDialog<dynamic>(
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 CwBasedInterface) {
await wallet.init();
await wallet.open();
}
final time = Future<dynamic>.delayed(
const Duration(
milliseconds: 2500,
),
);
TxData txData;
Future<TxData> 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<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Transaction failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.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<StackColors>()!.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<StackColors>()!
.infoItemLabel,
),
],
),
),
),
),
MaterialButton(
splashColor:
Theme.of(context).extension<StackColors>()!.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<StackColors>()!
.infoItemLabel,
),
],
),
),
),
),
const SizedBox(
height: 6,
),
],
),
),
child: ConditionalParent(
condition: !isFiro,
builder: (child) => MaterialButton(
splashColor: Theme.of(context).extension<StackColors>()!.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),
),
],
),
),
],
),
),
),
);
}
}