diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 191f12a15..5d9c5328e 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -13,7 +13,9 @@ import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selectio import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -37,6 +39,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'sub_widgets/firo_balance_selection_sheet.dart'; + class SendView extends ConsumerStatefulWidget { const SendView({ Key? key, @@ -82,6 +86,9 @@ class _SendViewState extends ConsumerState { Decimal? _cachedAmountToSend; String? _address; + String? _privateBalanceString; + String? _publicBalanceString; + bool _addressToggleFlag = false; bool _cryptoAmountChangeLock = false; @@ -196,6 +203,26 @@ class _SendViewState extends ConsumerState { return cachedFees[amount]!; } + Future _firoBalanceFuture( + ChangeNotifierProvider provider, String locale) async { + final wallet = ref.read(provider).wallet as FiroWallet?; + + if (wallet != null) { + Decimal? balance; + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + balance = await wallet.availablePrivateBalance(); + } else { + balance = await wallet.availablePublicBalance(); + } + + return Format.localizedStringAsFixed( + value: balance, locale: locale, decimalPlaces: 8); + } + + return null; + } + @override void initState() { ref.refresh(feeSheetSessionCacheProvider); @@ -334,21 +361,59 @@ class _SendViewState extends ConsumerState { children: [ SvgPicture.asset( Assets.svg.iconFor(coin: coin), - width: 18, - height: 18, + width: 22, + height: 22, ), const SizedBox( width: 6, ), - Text( - ref.watch(provider - .select((value) => value.walletName)), - style: STextStyles.titleBold12, - ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + Text( + ref.watch(provider + .select((value) => value.walletName)), + style: STextStyles.titleBold12, + ), + if (coin == Coin.firo || + coin == Coin.firoTestNet) + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: STextStyles.titleBold12 + .copyWith(fontSize: 14), + ), + // const SizedBox( + // height: 2, + // ), + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.label + .copyWith(fontSize: 10), + ), + ], + ), const Spacer(), FutureBuilder( - future: ref.watch(provider.select( - (value) => value.availableBalance)), + future: (coin != Coin.firo && + coin != Coin.firoTestNet) + ? ref.watch(provider.select( + (value) => value.availableBalance)) + : ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" + ? (ref.watch(provider).wallet + as FiroWallet) + .availablePrivateBalance() + : (ref.watch(provider).wallet + as FiroWallet) + .availablePublicBalance(), builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == @@ -423,6 +488,9 @@ class _SendViewState extends ConsumerState { fontSize: 10, ), ), + const SizedBox( + height: 2, + ), AnimatedText( stringsToLoopThrough: const [ "Loading balance ", @@ -730,6 +798,141 @@ class _SendViewState extends ConsumerState { } }, ), + if (coin == Coin.firo) + const SizedBox( + height: 12, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.smallMed12, + textAlign: TextAlign.left, + ), + if (coin == Coin.firo) + const SizedBox( + height: 8, + ), + if (coin == Coin.firo) + Stack( + children: [ + const TextField( + readOnly: true, + textInputAction: TextInputAction.none, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: RawMaterialButton( + splashColor: CFColors.splashLight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => FiroBalanceSelectionSheet( + walletId: walletId, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.itemSubtitle12, + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture( + provider, locale), + builder: (context, + AsyncSnapshot + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + _privateBalanceString = + snapshot.data!; + } else { + _publicBalanceString = + snapshot.data!; + } + } + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" && + _privateBalanceString != + null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: + STextStyles.itemSubtitle, + ); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Public" && + _publicBalanceString != + null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: + STextStyles.itemSubtitle, + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: + STextStyles.itemSubtitle, + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: CFColors.gray3, + ), + ], + ), + ), + ) + ], + ), const SizedBox( height: 12, ), @@ -744,10 +947,34 @@ class _SendViewState extends ConsumerState { BlueTextButton( text: "Send all ${coin.ticker}", onTap: () async { - cryptoAmountController.text = (await ref - .read(provider) - .availableBalance) - .toStringAsFixed(Constants.decimalPlaces); + if (coin == Coin.firo || + coin == Coin.firoTestNet) { + final firoWallet = + ref.read(provider).wallet as FiroWallet; + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + cryptoAmountController.text = + (await firoWallet + .availablePrivateBalance()) + .toStringAsFixed( + Constants.decimalPlaces); + } else { + cryptoAmountController.text = + (await firoWallet + .availablePublicBalance()) + .toStringAsFixed( + Constants.decimalPlaces); + } + } else { + cryptoAmountController.text = (await ref + .read(provider) + .availableBalance) + .toStringAsFixed(Constants.decimalPlaces); + } }, ), ], diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart new file mode 100644 index 000000000..8cb1e64dd --- /dev/null +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -0,0 +1,287 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/cfcolors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; + +class FiroBalanceSelectionSheet extends ConsumerStatefulWidget { + const FiroBalanceSelectionSheet({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState createState() => + _FiroBalanceSelectionSheetState(); +} + +class _FiroBalanceSelectionSheetState + extends ConsumerState { + late final String walletId; + + final stringsToLoopThrough = [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ]; + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final firoWallet = manager.wallet as FiroWallet; + + return Container( + decoration: const BoxDecoration( + color: CFColors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + top: 10, + bottom: 0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + decoration: BoxDecoration( + color: CFColors.fieldGray, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + width: 60, + height: 4, + ), + ), + const SizedBox( + height: 36, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select balance", + style: STextStyles.pageTitleH2, + textAlign: TextAlign.left, + ), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + final state = + ref.read(publicPrivateBalanceStateProvider.state).state; + if (state != "Private") { + ref.read(publicPrivateBalanceStateProvider.state).state = + "Private"; + } + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: CFColors.link2, + value: "Private", + groupValue: ref + .watch( + publicPrivateBalanceStateProvider.state) + .state, + onChanged: (x) { + ref + .read(publicPrivateBalanceStateProvider + .state) + .state = "Private"; + + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Private balance", + style: STextStyles.titleBold12.copyWith( + color: const Color(0xFF44464E), + ), + textAlign: TextAlign.left, + ), + const SizedBox( + width: 2, + ), + FutureBuilder( + future: firoWallet.availablePrivateBalance(), + builder: + (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${snapshot.data!} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle, + textAlign: TextAlign.left, + ); + } else { + return AnimatedText( + stringsToLoopThrough: + stringsToLoopThrough, + style: STextStyles.itemSubtitle, + ); + } + }, + ) + ], + ), + // ], + // ), + ) + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + final state = + ref.read(publicPrivateBalanceStateProvider.state).state; + if (state != "Public") { + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; + } + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: CFColors.link2, + value: "Public", + groupValue: ref + .watch( + publicPrivateBalanceStateProvider.state) + .state, + onChanged: (x) { + ref + .read(publicPrivateBalanceStateProvider + .state) + .state = "Public"; + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Public balance", + style: STextStyles.titleBold12.copyWith( + color: const Color(0xFF44464E), + ), + textAlign: TextAlign.left, + ), + const SizedBox( + width: 2, + ), + FutureBuilder( + future: firoWallet.availablePublicBalance(), + builder: + (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${snapshot.data!} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle, + textAlign: TextAlign.left, + ); + } else { + return AnimatedText( + stringsToLoopThrough: + stringsToLoopThrough, + style: STextStyles.itemSubtitle, + ); + } + }, + ) + // ], + // ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + const SizedBox( + height: 24, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index e65133f4e..1d51aa74d 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/utilities/cfcolors.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -18,6 +20,9 @@ class WalletBalanceToggleSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final maxHeight = MediaQuery.of(context).size.height * 0.60; + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin)); + return Container( decoration: const BoxDecoration( color: CFColors.white, @@ -105,25 +110,44 @@ class WalletBalanceToggleSheet extends ConsumerWidget { const SizedBox( width: 12, ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Available balance", - style: STextStyles.titleBold12, - ), - const SizedBox( - height: 2, - ), - // TODO need text from design - Text( - "Current spendable (unlocked) balance", - style: STextStyles.itemSubtitle12.copyWith( - color: CFColors.neutral60, + if (coin != Coin.firo && coin != Coin.firoTestNet) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Available balance", + style: STextStyles.titleBold12, ), - ), - ], - ), + const SizedBox( + height: 2, + ), + Text( + "Current spendable (unlocked) balance", + style: STextStyles.itemSubtitle12.copyWith( + color: CFColors.neutral60, + ), + ), + ], + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Private balance", + style: STextStyles.titleBold12, + ), + const SizedBox( + height: 2, + ), + Text( + "Current private spendable (unlocked) balance", + style: STextStyles.itemSubtitle12.copyWith( + color: CFColors.neutral60, + ), + ), + ], + ), ], ), ), @@ -172,25 +196,44 @@ class WalletBalanceToggleSheet extends ConsumerWidget { const SizedBox( width: 12, ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Full balance", - style: STextStyles.titleBold12, - ), - const SizedBox( - height: 2, - ), - // TODO need text from design - Text( - "Total wallet balance", - style: STextStyles.itemSubtitle12.copyWith( - color: CFColors.neutral60, + if (coin != Coin.firo && coin != Coin.firoTestNet) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Full balance", + style: STextStyles.titleBold12, ), - ), - ], - ), + const SizedBox( + height: 2, + ), + Text( + "Total wallet balance", + style: STextStyles.itemSubtitle12.copyWith( + color: CFColors.neutral60, + ), + ), + ], + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Public balance", + style: STextStyles.titleBold12, + ), + const SizedBox( + height: 2, + ), + Text( + "Current public spendable (unlocked) balance", + style: STextStyles.itemSubtitle12.copyWith( + color: CFColors.neutral60, + ), + ), + ], + ), ], ), ), diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index b45caa3bc..8687e8a59 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -67,15 +68,25 @@ class _WalletSummaryInfoState extends State { Expanded( child: Consumer( builder: (_, ref, __) { - final totalBalanceFuture = ref - .watch(managerProvider.select((value) => value.totalBalance)); - - final availableBalanceFuture = ref.watch( - managerProvider.select((value) => value.availableBalance)); - final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); + Future? totalBalanceFuture; + Future? availableBalanceFuture; + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = + ref.watch(managerProvider.select((value) => value.wallet)) + as FiroWallet; + totalBalanceFuture = firoWallet.availablePublicBalance(); + availableBalanceFuture = firoWallet.availablePrivateBalance(); + } else { + totalBalanceFuture = ref.watch( + managerProvider.select((value) => value.totalBalance)); + + availableBalanceFuture = ref.watch( + managerProvider.select((value) => value.availableBalance)); + } + final locale = ref.watch(localeServiceChangeNotifierProvider .select((value) => value.locale)); @@ -114,12 +125,20 @@ class _WalletSummaryInfoState extends State { onTap: showSheet, child: Row( children: [ - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: STextStyles.subtitle.copyWith( - fontWeight: FontWeight.w500, + if (coin == Coin.firo || coin == Coin.firoTestNet) + Text( + "${_showAvailable ? "Private" : "Public"} Balance", + style: STextStyles.subtitle.copyWith( + fontWeight: FontWeight.w500, + ), + ), + if (coin != Coin.firo && coin != Coin.firoTestNet) + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: STextStyles.subtitle.copyWith( + fontWeight: FontWeight.w500, + ), ), - ), const SizedBox( width: 4, ), @@ -166,12 +185,20 @@ class _WalletSummaryInfoState extends State { onTap: showSheet, child: Row( children: [ - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: STextStyles.subtitle.copyWith( - fontWeight: FontWeight.w500, + if (coin == Coin.firo || coin == Coin.firoTestNet) + Text( + "${_showAvailable ? "Private" : "Public"} Balance", + style: STextStyles.subtitle.copyWith( + fontWeight: FontWeight.w500, + ), + ), + if (coin != Coin.firo && coin != Coin.firoTestNet) + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: STextStyles.subtitle.copyWith( + fontWeight: FontWeight.w500, + ), ), - ), const SizedBox( width: 4, ), diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 473875818..e117d42c3 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -71,11 +71,11 @@ class _TransactionDetailsViewState fee = Format.satoshisToAmount(_transaction.fees); amountPrefix = _transaction.txType.toLowerCase() == "sent" ? "- " : "+ "; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - showFeePending = true; - } else { - showFeePending = false; - } + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // showFeePending = true; + // } else { + // showFeePending = false; + // } super.initState(); } @@ -86,9 +86,10 @@ class _TransactionDetailsViewState String whatIsIt(String type) { if (type == "Received") { - if (_transaction.isMinting) { - return "Minting"; - } else if (_transaction.confirmedStatus) { + // if (_transaction.isMinting) { + // return "Minting"; + // } else + if (_transaction.confirmedStatus) { return "Received"; } else { return "Receiving"; @@ -507,83 +508,83 @@ class _TransactionDetailsViewState ], ), ), - if ((coin == Coin.firoTestNet || coin == Coin.firo) && - _transaction.subType == "mint") - const SizedBox( - height: 12, - ), - if ((coin == Coin.firoTestNet || coin == Coin.firo) && - _transaction.subType == "mint") - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Mint Transaction ID", - style: STextStyles.itemSubtitle, - ), - ], - ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.otherData ?? "Unknown", - style: STextStyles.itemSubtitle12, - ), - // ), - // ), - 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 (_) { - 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 == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle, + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12, + // ), + // // ), + // // ), + // 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 == Coin.epicCash) const SizedBox( height: 12, diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b9f240951..94228bee7 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; @@ -22,6 +24,7 @@ import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -31,12 +34,18 @@ import 'package:stackwallet/utilities/cfcolors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; +import '../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../providers/wallet/wallet_balance_toggle_state_provider.dart'; +import '../../utilities/enums/wallet_balance_toggle_state.dart'; + /// [eventBus] should only be set during testing class WalletView extends ConsumerStatefulWidget { const WalletView({ @@ -271,10 +280,78 @@ class _WalletViewState extends ConsumerState { } } + Future attemptAnonymize() async { + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + final firoWallet = ref.read(managerProvider).wallet as FiroWallet; + + final publicBalance = await firoWallet.availablePublicBalance(); + if (publicBalance <= Decimal.zero) { + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "No funds available to anonymize!", + context: context, + ), + ); + } + return; + } + + try { + await firoWallet.anonymizeAllPublicFunds(); + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Anonymize transaction submitted", + context: context, + ), + ); + } + } catch (e) { + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ); + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Anonymize all failed", + message: "Reason: $e", + ), + ); + } + } + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + final coin = ref.watch(managerProvider.select((value) => value.coin)); + return WillPopScope( onWillPop: _onWillPop, child: Scaffold( @@ -283,9 +360,7 @@ class _WalletViewState extends ConsumerState { title: Row( children: [ SvgPicture.asset( - Assets.svg.iconFor( - coin: ref - .watch(managerProvider.select((value) => value.coin))), + Assets.svg.iconFor(coin: coin), // color: CFColors.stackAccent, width: 24, height: 24, @@ -440,6 +515,69 @@ class _WalletViewState extends ConsumerState { ), ), ), + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: TextButton( + onPressed: () async { + await showDialog( + context: context, + builder: (context) => StackDialog( + title: "Attention!", + message: + "You're about to anonymize all of your public funds.", + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + "Cancel", + style: STextStyles.button.copyWith( + color: CFColors.stackAccent, + ), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .textButtonTheme + .style + ?.copyWith( + backgroundColor: + MaterialStateProperty.all( + CFColors.stackAccent, + ), + ), + child: Text( + "Continue", + style: STextStyles.button, + ), + ), + ), + ); + }, + child: Text( + "Anonymize funds", + style: STextStyles.button.copyWith( + color: CFColors.stackAccent, + ), + ), + ), + ), + ], + ), + ), const SizedBox( height: 20, ), @@ -550,6 +688,25 @@ class _WalletViewState extends ConsumerState { ref.read(managerProvider).walletId; final coin = ref.read(managerProvider).coin; + switch (ref + .read(walletBalanceToggleStateProvider + .state) + .state) { + case WalletBalanceToggleState.full: + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state = "Public"; + break; + case WalletBalanceToggleState.available: + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state = "Private"; + break; + } Navigator.of(context).pushNamed( SendView.routeName, arguments: Tuple2( diff --git a/lib/providers/wallet/public_private_balance_state_provider.dart b/lib/providers/wallet/public_private_balance_state_provider.dart new file mode 100644 index 000000000..351489f04 --- /dev/null +++ b/lib/providers/wallet/public_private_balance_state_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final publicPrivateBalanceStateProvider = + StateProvider((_) => "Private"); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 42af5d003..2ba6010f2 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -1761,7 +1761,12 @@ class FiroWallet extends CoinServiceAPI { balances.add( (lelantusBalance + utxosValue + _unconfirmedLelantusBalance) * price); - balances.add(utxosValue); + int availableSats = + utxos.satoshiBalance - utxos.satoshiBalanceUnconfirmed; + if (availableSats < 0) { + availableSats = 0; + } + balances.add(Format.satoshisToAmount(availableSats)); Logging.instance.log("balances $balances", level: LogLevel.Info); await DB.instance.put( @@ -2782,6 +2787,7 @@ class FiroWallet extends CoinServiceAPI { Decimal currentPrice = await firoPrice; final List> outputArray = []; int satoshiBalance = 0; + int satoshiBalancePending = 0; for (int i = 0; i < utxoData.length; i++) { for (int j = 0; j < utxoData[i].length; j++) { @@ -2795,16 +2801,22 @@ class FiroWallet extends CoinServiceAPI { ); final Map tx = {}; + final int confirmations = txn["confirmations"] as int? ?? 0; + final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; + if (!confirmed) { + satoshiBalancePending += value; + } tx["txid"] = txn["txid"]; tx["vout"] = utxoData[i][j]["tx_pos"]; tx["value"] = value; tx["status"] = {}; + tx["status"]["confirmed"] = confirmed; + tx["status"]["confirmations"] = confirmations; tx["status"]["confirmed"] = txn["confirmations"] == null ? false : txn["confirmations"] > 0; - tx["status"]["confirmations"] = - txn["confirmations"] == null ? 0 : txn["confirmations"]!; + tx["status"]["block_height"] = txn["height"]; tx["status"]["block_hash"] = txn["blockhash"]; tx["status"]["block_time"] = txn["blocktime"]; @@ -2832,6 +2844,7 @@ class FiroWallet extends CoinServiceAPI { .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) .toString(), "outputArray": outputArray, + "unconfirmed": satoshiBalancePending, }; final dataModel = UtxoData.fromJson(result); diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index be910d78e..756e3d8f7 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -37,10 +37,30 @@ class _TransactionCardState extends ConsumerState { if (coin == Coin.epicCash && _transaction.slateId == null) { return "Restored Funds"; } + + if (_transaction.subType == "mint") { + // if (type == "Received") { + if (_transaction.confirmedStatus) { + return "Anonymized"; + } else { + return "Anonymizing"; + } + // } else if (type == "Sent") { + // if (_transaction.confirmedStatus) { + // return "Sent MINT"; + // } else { + // return "Sending MINT"; + // } + // } else { + // return type; + // } + } + if (type == "Received") { - if (_transaction.isMinting) { - return "Minting"; - } else if (_transaction.confirmedStatus) { + // if (_transaction.isMinting) { + // return "Minting"; + // } else + if (_transaction.confirmedStatus) { return "Received"; } else { return "Receiving";