add option to choose between spark and transparent addresses during swap when selecting choose from stack to fill in an address
What changed, and why it matters
This commit updates the wallet's exchange and buy flows so that when a user picks one of their own wallets to receive a swap, they can now choose between a private 'Spark' address and a regular 'Transparent' address for Firo. Previously the app only offered one default address type. The change is a user-facing feature improvement, not a security fix, and the diff does not show any vulnerability being patched.
No security action required. Treat as a normal feature commit. If reviewing further, verify that Spark address generation and balance display logic correctly handle null addresses and that the exchange partner supports Spark addresses before the swap is initiated.
Security signals we found
UI feature addition: Spark vs Transparent address selection
Return type change from walletId to structured address data
No input sanitization, authentication, or cryptographic changes observed
No vendor disclosure of security relevance
Evidence from the diff
The patch replaces ChooseFromStackView with ChooseAddressFromStackView and DesktopChooseFromStack with DesktopChooseAddressFromStack. The new views return a richer record/Tuple2 containing walletId/address/walletName instead of only a walletId, and callers in buy_form.dart, step_2_view.dart, and desktop_step_2.dart now use the returned address directly. For Firo wallets the UI exposes separate Spark and Transparent rows; Spark addresses are fetched via SparkInterface.getCurrentReceivingSparkAddress() with a fallback to generateNextSparkAddress(saveToDB: true). WalletInfoRowBalance gains an optional balanceType filter (public/private) so the displayed balance matches the selected address type. No cryptographic, input-validation, or access-control changes are visible.
Changed components
lib/pages/buy_view/buy_form.dartlib/pages/exchange_view/choose_address_from_stack_view.dartlib/pages/exchange_view/exchange_step_views/step_2_view.dartlib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dartlib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dartlib/route_generator.dartlib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dartInspect captured patch +1039 / −727
diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart
index 4791104..93b6440 100644
--- a/lib/pages/buy_view/buy_form.dart
+++ b/lib/pages/buy_view/buy_form.dart
@@ -57,7 +57,7 @@ import '../../widgets/stack_dialog.dart';
import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import '../address_book_views/address_book_view.dart';
-import '../exchange_view/choose_from_stack_view.dart';
+import '../exchange_view/choose_address_from_stack_view.dart';
import 'buy_quote_preview.dart';
import 'sub_widgets/crypto_selection_view.dart';
import 'sub_widgets/fiat_selection_view.dart';
@@ -1172,14 +1172,19 @@ class _BuyFormState extends ConsumerState<BuyForm> {
);
Navigator.of(context)
.pushNamed(
- ChooseFromStackView.routeName,
+ ChooseAddressFromStackView.routeName,
arguments: coin,
)
.then((value) async {
- if (value is String) {
+ if (value
+ is ({
+ String walletId,
+ String address,
+ String walletName,
+ })) {
final wallet = ref
.read(pWallets)
- .getWallet(value);
+ .getWallet(value.walletId);
// _toController.text = manager.walletName;
// model.recipientAddress =
diff --git a/lib/pages/exchange_view/choose_address_from_stack_view.dart b/lib/pages/exchange_view/choose_address_from_stack_view.dart
new file mode 100644
index 0000000..c17c58b
--- /dev/null
+++ b/lib/pages/exchange_view/choose_address_from_stack_view.dart
@@ -0,0 +1,344 @@
+/*
+ * 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 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import '../../providers/providers.dart';
+import '../../themes/stack_colors.dart';
+import '../../utilities/assets.dart';
+import '../../utilities/constants.dart';
+import '../../utilities/show_loading.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/wallet/wallet_mixin_interfaces/spark_interface.dart';
+import '../../widgets/background.dart';
+import '../../widgets/custom_buttons/app_bar_icon_button.dart';
+import '../../widgets/rounded_white_container.dart';
+import '../../widgets/stack_dialog.dart';
+import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart';
+import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
+
+class ChooseAddressFromStackView extends ConsumerStatefulWidget {
+ const ChooseAddressFromStackView({super.key, required this.coin});
+
+ final CryptoCurrency coin;
+
+ static const String routeName = "/chooseFromStack";
+
+ @override
+ ConsumerState<ChooseAddressFromStackView> createState() =>
+ _ChooseFromStackViewState();
+}
+
+class _ChooseFromStackViewState
+ extends ConsumerState<ChooseAddressFromStackView> {
+ late final CryptoCurrency coin;
+
+ @override
+ void initState() {
+ coin = widget.coin;
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final walletIds = ref
+ .watch(pWallets)
+ .wallets
+ .where((e) => e.info.coin == coin)
+ .map((e) => e.walletId)
+ .toList();
+
+ return Background(
+ child: Scaffold(
+ backgroundColor: Theme.of(context).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: const AppBarBackButton(),
+ title: Text(
+ "Choose your ${coin.ticker.toUpperCase()} wallet",
+ style: STextStyles.navBarTitle(context),
+ ),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: walletIds.isEmpty
+ ? Column(
+ children: [
+ RoundedWhiteContainer(
+ child: Center(
+ child: Text(
+ "No ${coin.ticker.toUpperCase()} wallets",
+ style: STextStyles.itemSubtitle(context),
+ ),
+ ),
+ ),
+ ],
+ )
+ : ListView.builder(
+ itemCount: walletIds.length,
+ itemBuilder: (context, index) => Padding(
+ padding: const EdgeInsets.symmetric(vertical: 5.0),
+ child: _WalletAddressSelectCard(
+ walletId: walletIds[index],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _WalletAddressSelectCard extends ConsumerStatefulWidget {
+ const _WalletAddressSelectCard({required this.walletId});
+
+ final String walletId;
+
+ @override
+ ConsumerState<_WalletAddressSelectCard> createState() =>
+ _WalletAddressSelectCardState();
+}
+
+class _WalletAddressSelectCardState
+ extends ConsumerState<_WalletAddressSelectCard> {
+ @override
+ Widget build(BuildContext context) {
+ final coin = ref.watch(pWalletCoin(widget.walletId));
+
+ if (coin is! Firo) {
+ return RawMaterialButton(
+ splashColor: Theme.of(context).extension<StackColors>()!.highlight,
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ padding: const EdgeInsets.all(0),
+ elevation: 0,
+ onPressed: () async {
+ final wallet = ref.read(pWallets).getWallet(widget.walletId);
+
+ final data = (
+ walletId: widget.walletId,
+ address:
+ (await wallet.getCurrentReceivingAddress())?.value ??
+ wallet.info.cachedReceivingAddress,
+ walletName: wallet.info.name,
+ );
+
+ if (context.mounted) {
+ Navigator.of(context).pop(data);
+ }
+ },
+ child: RoundedWhiteContainer(
+ child: Row(
+ children: [
+ WalletInfoCoinIcon(coin: coin),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ ref.watch(pWalletName(widget.walletId)),
+ style: STextStyles.titleBold12(context),
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 2),
+ WalletInfoRowBalance(walletId: widget.walletId),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ return RoundedWhiteContainer(
+ child: Column(
+ mainAxisSize: .min,
+ crossAxisAlignment: .start,
+ children: [
+ Row(
+ children: [
+ WalletInfoCoinIcon(coin: coin),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ ref.watch(pWalletName(widget.walletId)),
+ style: STextStyles.titleBold12(context),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ RawMaterialButton(
+ splashColor: Theme.of(context).extension<StackColors>()!.highlight,
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ padding: const EdgeInsets.all(0),
+ elevation: 0,
+ onPressed: () async {
+ Future<String?> _future() async {
+ final wallet =
+ ref.read(pWallets).getWallet(widget.walletId)
+ as SparkInterface;
+ final sparkAddress = await wallet
+ .getCurrentReceivingSparkAddress();
+ if (sparkAddress != null) {
+ return sparkAddress.value;
+ }
+
+ return (await wallet.generateNextSparkAddress(
+ saveToDB: true,
+ )).value;
+ }
+
+ Exception? ex;
+ final sparkAddress = await showLoading(
+ context: context,
+ message: "Fetching Spark address",
+ rootNavigator: Util.isDesktop,
+ delay: const Duration(milliseconds: 1200),
+ whileFutureAlt: _future,
+ onException: (e) => ex = e,
+ );
+
+ if (context.mounted) {
+ if (ex != null) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => StackOkDialog(
+ title: "Error",
+ message: ex
+ .toString()
+ .replaceFirst("Exception:", "")
+ .trim(),
+ ),
+ );
+ } else {
+ Navigator.of(context).pop((
+ walletId: widget.walletId,
+ address: sparkAddress,
+ walletName:
+ "${ref.read(pWalletName(widget.walletId))} (Spark)",
+ ));
+ }
+ }
+ },
+ child: Row(
+ crossAxisAlignment: .center,
+ mainAxisAlignment: .spaceBetween,
+ children: [
+ Column(
+ mainAxisSize: .min,
+ crossAxisAlignment: .start,
+ children: [
+ Text("Spark address", style: STextStyles.w500_12(context)),
+ const SizedBox(height: 2),
+ WalletInfoRowBalance(
+ walletId: widget.walletId,
+ balanceType: .private,
+ ),
+ ],
+ ),
+ SizedBox(
+ width: 25,
+ height: 25,
+ child: SvgPicture.asset(
+ Assets.svg.chevronRight,
+ colorFilter: ColorFilter.mode(
+ Theme.of(context).extension<StackColors>()!.textDark,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 8),
+ RawMaterialButton(
+ splashColor: Theme.of(context).extension<StackColors>()!.highlight,
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ padding: const EdgeInsets.all(0),
+ elevation: 0,
+ onPressed: () async {
+ final wallet = ref.read(pWallets).getWallet(widget.walletId);
+
+ final data = (
+ walletId: widget.walletId,
+ address:
+ (await wallet.getCurrentReceivingAddress())?.value ??
+ wallet.info.cachedReceivingAddress,
+ walletName: "${wallet.info.name} (Transparent)",
+ );
+
+ if (context.mounted) {
+ Navigator.of(context).pop(data);
+ }
+ },
+
+ child: Row(
+ crossAxisAlignment: .center,
+ mainAxisAlignment: .spaceBetween,
+ children: [
+ Column(
+ mainAxisSize: .min,
+ crossAxisAlignment: .start,
+ children: [
+ Text(
+ "Transparent address",
+ style: STextStyles.w500_12(context),
+ ),
+ const SizedBox(height: 2),
+ WalletInfoRowBalance(
+ walletId: widget.walletId,
+ balanceType: .public,
+ ),
+ ],
+ ),
+ SizedBox(
+ width: 25,
+ height: 25,
+ child: SvgPicture.asset(
+ Assets.svg.chevronRight,
+ colorFilter: ColorFilter.mode(
+ Theme.of(context).extension<StackColors>()!.textDark,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart
deleted file mode 100644
index d4cc11d..0000000
--- a/lib/pages/exchange_view/choose_from_stack_view.dart
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-
-import '../../providers/providers.dart';
-import '../../themes/stack_colors.dart';
-import '../../utilities/constants.dart';
-import '../../utilities/text_styles.dart';
-import '../../wallets/crypto_currency/crypto_currency.dart';
-import '../../wallets/isar/providers/wallet_info_provider.dart';
-import '../../widgets/background.dart';
-import '../../widgets/custom_buttons/app_bar_icon_button.dart';
-import '../../widgets/rounded_white_container.dart';
-import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart';
-import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
-
-class ChooseFromStackView extends ConsumerStatefulWidget {
- const ChooseFromStackView({super.key, required this.coin});
-
- final CryptoCurrency coin;
-
- static const String routeName = "/chooseFromStack";
-
- @override
- ConsumerState<ChooseFromStackView> createState() =>
- _ChooseFromStackViewState();
-}
-
-class _ChooseFromStackViewState extends ConsumerState<ChooseFromStackView> {
- late final CryptoCurrency coin;
-
- @override
- void initState() {
- coin = widget.coin;
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- final walletIds =
- ref
- .watch(pWallets)
- .wallets
- .where((e) => e.info.coin == coin)
- .map((e) => e.walletId)
- .toList();
-
- return Background(
- child: Scaffold(
- backgroundColor: Theme.of(context).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: const AppBarBackButton(),
- title: Text(
- "Choose your ${coin.ticker.toUpperCase()} wallet",
- style: STextStyles.navBarTitle(context),
- ),
- ),
- body: SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child:
- walletIds.isEmpty
- ? Column(
- children: [
- RoundedWhiteContainer(
- child: Center(
- child: Text(
- "No ${coin.ticker.toUpperCase()} wallets",
- style: STextStyles.itemSubtitle(context),
- ),
- ),
- ),
- ],
- )
- : ListView.builder(
- itemCount: walletIds.length,
- itemBuilder: (context, index) {
- final walletId = walletIds[index];
-
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 5.0),
- child: RawMaterialButton(
- splashColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.highlight,
- materialTapTargetSize:
- MaterialTapTargetSize.shrinkWrap,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- ),
- padding: const EdgeInsets.all(0),
- // color: Theme.of(context).extension<StackColors>()!.popupBG,
- elevation: 0,
- onPressed: () async {
- if (mounted) {
- Navigator.of(context).pop(walletId);
- }
- },
- child: RoundedWhiteContainer(
- // color: Colors.transparent,
- child: Row(
- children: [
- WalletInfoCoinIcon(coin: coin),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- Text(
- ref.watch(pWalletName(walletId)),
- style: STextStyles.titleBold12(
- context,
- ),
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 2),
- WalletInfoRowBalance(
- walletId: walletIds[index],
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- ),
- );
- },
- ),
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart
index 3bead41..1b2fa42 100644
--- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart
+++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart
@@ -35,7 +35,7 @@ import '../../../widgets/stack_text_field.dart';
import '../../../widgets/textfield_icon_button.dart';
import '../../address_book_views/address_book_view.dart';
import '../../address_book_views/subviews/contact_popup.dart';
-import '../choose_from_stack_view.dart';
+import '../choose_address_from_stack_view.dart';
import '../sub_widgets/step_row.dart';
import 'step_3_view.dart';
@@ -299,24 +299,21 @@ class _Step2ViewState extends ConsumerState<Step2View> {
Navigator.of(context)
.pushNamed(
- ChooseFromStackView.routeName,
+ ChooseAddressFromStackView
+ .routeName,
arguments: coin,
)
.then((value) async {
- if (value is String) {
- final wallet = ref
- .read(pWallets)
- .getWallet(value);
-
+ if (value
+ is ({
+ String walletId,
+ String address,
+ String walletName,
+ })) {
_toController.text =
- wallet.info.name;
+ value.walletName;
model.recipientAddress =
- (await wallet
- .getCurrentReceivingAddress())
- ?.value ??
- wallet
- .info
- .cachedReceivingAddress;
+ value.address;
setState(() {
enableNext =
@@ -571,21 +568,21 @@ class _Step2ViewState extends ConsumerState<Step2View> {
Navigator.of(context)
.pushNamed(
- ChooseFromStackView.routeName,
+ ChooseAddressFromStackView
+ .routeName,
arguments: coin,
)
.then((value) async {
- if (value is String) {
- final wallet = ref
- .read(pWallets)
- .getWallet(value);
-
+ if (value
+ is ({
+ String walletId,
+ String address,
+ String walletName,
+ })) {
_refundController.text =
- wallet.info.name;
+ value.walletName;
model.refundAddress =
- (await wallet
- .getCurrentReceivingAddress())!
- .value;
+ value.address;
}
setState(() {
enableNext =
diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart
index c7ba641..46038a5 100644
--- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart
+++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart
@@ -31,7 +31,7 @@ import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.dart';
import '../../../my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart';
-import '../../subwidgets/desktop_choose_from_stack.dart';
+import '../../subwidgets/desktop_choose_address_from_stack.dart';
import '../step_scaffold.dart';
class DesktopStep2 extends ConsumerStatefulWidget {
@@ -59,23 +59,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
void selectRecipientAddressFromStack() async {
try {
- final coin =
- AppConfig.getCryptoCurrencyForTicker(
- ref.read(desktopExchangeModelProvider)!.receiveTicker,
- )!;
+ final coin = AppConfig.getCryptoCurrencyForTicker(
+ ref.read(desktopExchangeModelProvider)!.receiveTicker,
+ )!;
final info = await showDialog<Tuple2<String, String>?>(
context: context,
barrierColor: Colors.transparent,
- builder:
- (context) => DesktopDialog(
- maxWidth: 720,
- maxHeight: 670,
- child: Padding(
- padding: const EdgeInsets.all(32),
- child: DesktopChooseFromStack(coin: coin),
- ),
- ),
+ builder: (context) => DesktopDialog(
+ maxWidth: 720,
+ maxHeight: 670,
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: DesktopChooseAddressFromStack(coin: coin),
+ ),
+ ),
);
if (info is Tuple2<String, String>) {
@@ -91,23 +89,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
void selectRefundAddressFromStack() async {
try {
- final coin =
- AppConfig.getCryptoCurrencyForTicker(
- ref.read(desktopExchangeModelProvider)!.sendTicker,
- )!;
+ final coin = AppConfig.getCryptoCurrencyForTicker(
+ ref.read(desktopExchangeModelProvider)!.sendTicker,
+ )!;
final info = await showDialog<Tuple2<String, String>?>(
context: context,
barrierColor: Colors.transparent,
- builder:
- (context) => DesktopDialog(
- maxWidth: 720,
- maxHeight: 670,
- child: Padding(
- padding: const EdgeInsets.all(32),
- child: DesktopChooseFromStack(coin: coin),
- ),
- ),
+ builder: (context) => DesktopDialog(
+ maxWidth: 720,
+ maxHeight: 670,
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: DesktopChooseAddressFromStack(coin: coin),
+ ),
+ ),
);
if (info is Tuple2<String, String>) {
_refundController.text = info.item1;
@@ -127,30 +123,29 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
final entry = await showDialog<ContactAddressEntry?>(
context: context,
barrierColor: Colors.transparent,
- builder:
- (context) => DesktopDialog(
- maxWidth: 720,
- maxHeight: 670,
- child: Column(
- mainAxisSize: MainAxisSize.min,
+ builder: (context) => DesktopDialog(
+ maxWidth: 720,
+ maxHeight: 670,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "Address book",
- style: STextStyles.desktopH3(context),
- ),
- ),
- const DesktopDialogCloseButton(),
- ],
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Address book",
+ style: STextStyles.desktopH3(context),
+ ),
),
- Expanded(child: AddressBookAddressChooser(coin: coin)),
+ const DesktopDialogCloseButton(),
],
),
- ),
+ Expanded(child: AddressBookAddressChooser(coin: coin)),
+ ],
+ ),
+ ),
);
if (entry != null) {
@@ -168,30 +163,29 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
final entry = await showDialog<ContactAddressEntry?>(
context: context,
barrierColor: Colors.transparent,
- builder:
- (context) => DesktopDialog(
- maxWidth: 720,
- maxHeight: 670,
- child: Column(
- mainAxisSize: MainAxisSize.min,
+ builder: (context) => DesktopDialog(
+ maxWidth: 720,
+ maxHeight: 670,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "Address book",
- style: STextStyles.desktopH3(context),
- ),
- ),
- const DesktopDialogCloseButton(),
- ],
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Address book",
+ style: STextStyles.desktopH3(context),
+ ),
),
- Expanded(child: AddressBookAddressChooser(coin: coin)),
+ const DesktopDialogCloseButton(),
],
),
- ),
+ Expanded(child: AddressBookAddressChooser(coin: coin)),
+ ],
+ ),
+ ),
);
if (entry != null) {
@@ -234,12 +228,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
if (tuple != null) {
if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() ==
tuple.item2.ticker.toLowerCase()) {
- _toController.text =
- ref
- .read(pWallets)
- .getWallet(tuple.item1)
- .info
- .cachedReceivingAddress;
+ _toController.text = ref
+ .read(pWallets)
+ .getWallet(tuple.item1)
+ .info
+ .cachedReceivingAddress;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(desktopExchangeModelProvider)!.recipientAddress =
@@ -249,12 +242,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
if (doesRefundAddress &&
ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
tuple.item2.ticker.toUpperCase()) {
- _refundController.text =
- ref
- .read(pWallets)
- .getWallet(tuple.item1)
- .info
- .cachedReceivingAddress;
+ _refundController.text = ref
+ .read(pWallets)
+ .getWallet(tuple.item1)
+ .info
+ .cachedReceivingAddress;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(desktopExchangeModelProvider)!.refundAddress =
_refundController.text;
@@ -300,10 +292,9 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
Text(
"Recipient Wallet",
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveSearchIconRight,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveSearchIconRight,
),
),
if (AppConfig.isStackCoin(
@@ -347,81 +338,82 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_toController.text;
widget.enableNextChanged.call(_next());
},
- decoration: standardInputDecoration(
- "Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address",
- _toFocusNode,
- context,
- desktopMed: true,
- ).copyWith(
- contentPadding: const EdgeInsets.only(
- left: 16,
- top: 6,
- bottom: 8,
- right: 5,
- ),
- suffixIcon: Padding(
- padding:
- _toController.text.isEmpty
+ decoration:
+ standardInputDecoration(
+ "Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address",
+ _toFocusNode,
+ context,
+ desktopMed: true,
+ ).copyWith(
+ contentPadding: const EdgeInsets.only(
+ left: 16,
+ top: 6,
+ bottom: 8,
+ right: 5,
+ ),
+ suffixIcon: Padding(
+ padding: _toController.text.isEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
- child: UnconstrainedBox(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- _toController.text.isNotEmpty
- ? TextFieldIconButton(
- key: const Key(
- "sendViewClearAddressFieldButtonKey",
- ),
- onTap: () {
- _toController.text = "";
- ref
- .read(desktopExchangeModelProvider)!
- .recipientAddress = _toController.text;
- widget.enableNextChanged.call(_next());
- },
- child: const XIcon(),
- )
- : TextFieldIconButton(
- key: const Key(
- "sendViewPasteAddressFieldButtonKey",
- ),
- onTap: () async {
- final ClipboardData? data = await clipboard
- .getData(Clipboard.kTextPlain);
- if (data?.text != null &&
- data!.text!.isNotEmpty) {
- final content = data.text!.trim();
- _toController.text = content;
- ref
- .read(desktopExchangeModelProvider)!
- .recipientAddress = _toController.text;
- widget.enableNextChanged.call(_next());
- }
- },
- child:
- _toController.text.isEmpty
- ? const ClipboardIcon()
- : const XIcon(),
- ),
- if (_toController.text.isEmpty &&
- AppConfig.isStackCoin(
- ref.watch(
- desktopExchangeModelProvider.select(
- (value) => value!.receiveTicker,
- ),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _toController.text.isNotEmpty
+ ? TextFieldIconButton(
+ key: const Key(
+ "sendViewClearAddressFieldButtonKey",
+ ),
+ onTap: () {
+ _toController.text = "";
+ ref
+ .read(desktopExchangeModelProvider)!
+ .recipientAddress =
+ _toController.text;
+ widget.enableNextChanged.call(_next());
+ },
+ child: const XIcon(),
+ )
+ : TextFieldIconButton(
+ key: const Key(
+ "sendViewPasteAddressFieldButtonKey",
+ ),
+ onTap: () async {
+ final ClipboardData? data = await clipboard
+ .getData(Clipboard.kTextPlain);
+ if (data?.text != null &&
+ data!.text!.isNotEmpty) {
+ final content = data.text!.trim();
+ _toController.text = content;
+ ref
+ .read(desktopExchangeModelProvider)!
+ .recipientAddress = _toController
+ .text;
+ widget.enableNextChanged.call(_next());
+ }
+ },
+ child: _toController.text.isEmpty
+ ? const ClipboardIcon()
+ : const XIcon(),
+ ),
+ if (_toController.text.isEmpty &&
+ AppConfig.isStackCoin(
+ ref.watch(
+ desktopExchangeModelProvider.select(
+ (value) => value!.receiveTicker,
+ ),
+ ),
+ ))
+ TextFieldIconButton(
+ key: const Key("sendViewAddressBookButtonKey"),
+ onTap: selectRecipientFromAddressBook,
+ child: const AddressBookIcon(),
),
- ))
- TextFieldIconButton(
- key: const Key("sendViewAddressBookButtonKey"),
- onTap: selectRecipientFromAddressBook,
- child: const AddressBookIcon(),
- ),
- ],
+ ],
+ ),
+ ),
),
),
- ),
- ),
),
),
const SizedBox(height: 10),
@@ -440,10 +432,9 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
Text(
"Refund Wallet (required)",
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
- color:
- Theme.of(context)
- .extension<StackColors>()!
- .textFieldActiveSearchIconRight,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveSearchIconRight,
),
),
if (AppConfig.isStackCoin(
@@ -487,84 +478,87 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_refundController.text;
widget.enableNextChanged.call(_next());
},
- decoration: standardInputDecoration(
- "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address",
- _refundFocusNode,
- context,
- desktopMed: true,
- ).copyWith(
- contentPadding: const EdgeInsets.only(
- left: 16,
- top: 6,
- bottom: 8,
- right: 5,
- ),
- suffixIcon: Padding(
- padding:
- _refundController.text.isEmpty
+ decoration:
+ standardInputDecoration(
+ "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address",
+ _refundFocusNode,
+ context,
+ desktopMed: true,
+ ).copyWith(
+ contentPadding: const EdgeInsets.only(
+ left: 16,
+ top: 6,
+ bottom: 8,
+ right: 5,
+ ),
+ suffixIcon: Padding(
+ padding: _refundController.text.isEmpty
? const EdgeInsets.only(right: 16)
: const EdgeInsets.only(right: 0),
- child: UnconstrainedBox(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- _refundController.text.isNotEmpty
- ? TextFieldIconButton(
- key: const Key(
- "sendViewClearAddressFieldButtonKey",
- ),
- onTap: () {
- _refundController.text = "";
- ref
- .read(desktopExchangeModelProvider)!
- .refundAddress = _refundController.text;
-
- widget.enableNextChanged.call(_next());
- },
- child: const XIcon(),
- )
- : TextFieldIconButton(
- key: const Key(
- "sendViewPasteAddressFieldButtonKey",
- ),
- onTap: () async {
- final ClipboardData? data = await clipboard
- .getData(Clipboard.kTextPlain);
- if (data?.text != null &&
- data!.text!.isNotEmpty) {
- final content = data.text!.trim();
-
- _refundController.text = content;
- ref
- .read(desktopExchangeModelProvider)!
- .refundAddress = _refundController.text;
-
- widget.enableNextChanged.call(_next());
- }
- },
- child:
- _refundController.text.isEmpty
- ? const ClipboardIcon()
- : const XIcon(),
- ),
- if (_refundController.text.isEmpty &&
- AppConfig.isStackCoin(
- ref.watch(
- desktopExchangeModelProvider.select(
- (value) => value!.sendTicker,
- ),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _refundController.text.isNotEmpty
+ ? TextFieldIconButton(
+ key: const Key(
+ "sendViewClearAddressFieldButtonKey",
+ ),
+ onTap: () {
+ _refundController.text = "";
+ ref
+ .read(desktopExchangeModelProvider)!
+ .refundAddress = _refundController
+ .text;
+
+ widget.enableNextChanged.call(_next());
+ },
+ child: const XIcon(),
+ )
+ : TextFieldIconButton(
+ key: const Key(
+ "sendViewPasteAddressFieldButtonKey",
+ ),
+ onTap: () async {
+ final ClipboardData? data =
+ await clipboard.getData(
+ Clipboard.kTextPlain,
+ );
+ if (data?.text != null &&
+ data!.text!.isNotEmpty) {
+ final content = data.text!.trim();
+
+ _refundController.text = content;
+ ref
+ .read(desktopExchangeModelProvider)!
+ .refundAddress = _refundController
+ .text;
+
+ widget.enableNextChanged.call(_next());
+ }
+ },
+ child: _refundController.text.isEmpty
+ ? const ClipboardIcon()
+ : const XIcon(),
+ ),
+ if (_refundController.text.isEmpty &&
+ AppConfig.isStackCoin(
+ ref.watch(
+ desktopExchangeModelProvider.select(
+ (value) => value!.sendTicker,
+ ),
+ ),
+ ))
+ TextFieldIconButton(
+ key: const Key("sendViewAddressBookButtonKey"),
+ onTap: selectRefundFromAddressBook,
+ child: const AddressBookIcon(),
),
- ))
- TextFieldIconButton(
- key: const Key("sendViewAddressBookButtonKey"),
- onTap: selectRefundFromAddressBook,
- child: const AddressBookIcon(),
- ),
- ],
+ ],
+ ),
+ ),
),
),
- ),
- ),
),
),
if (doesRefundAddress) const SizedBox(height: 10),
@@ -572,7 +566,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
RoundedWhiteContainer(
borderColor: Theme.of(context).extension<StackColors>()!.background,
child: Text(
- "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.",
+ "In case something goes wrong during the exchange, we might need "
+ "a refund address so we can return your coins back to you.",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart
new file mode 100644
index 0000000..8eaf949
--- /dev/null
+++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart
@@ -0,0 +1,426 @@
+/*
+ * 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 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:tuple/tuple.dart';
+
+import '../../../app_config.dart';
+import '../../../providers/providers.dart';
+import '../../../providers/wallet/public_private_balance_state_provider.dart';
+import '../../../themes/stack_colors.dart';
+import '../../../utilities/amount/amount_formatter.dart';
+import '../../../utilities/assets.dart';
+import '../../../utilities/constants.dart';
+import '../../../utilities/show_loading.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/wallet/wallet_mixin_interfaces/spark_interface.dart';
+import '../../../widgets/custom_buttons/blue_text_button.dart';
+import '../../../widgets/desktop/secondary_button.dart';
+import '../../../widgets/icon_widgets/x_icon.dart';
+import '../../../widgets/rounded_white_container.dart';
+import '../../../widgets/stack_dialog.dart';
+import '../../../widgets/stack_text_field.dart';
+import '../../../widgets/textfield_icon_button.dart';
+import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
+
+class DesktopChooseAddressFromStack extends ConsumerStatefulWidget {
+ const DesktopChooseAddressFromStack({super.key, required this.coin});
+
+ final CryptoCurrency coin;
+
+ @override
+ ConsumerState<DesktopChooseAddressFromStack> createState() =>
+ _DesktopChooseFromStackState();
+}
+
+class _DesktopChooseFromStackState
+ extends ConsumerState<DesktopChooseAddressFromStack> {
+ late final TextEditingController _searchController;
+ late final FocusNode searchFieldFocusNode;
+
+ String _searchTerm = "";
+
+ List<String> filter(List<String> walletIds, String searchTerm) {
+ if (searchTerm.isEmpty) {
+ return walletIds;
+ }
+
+ final List<String> result = [];
+ for (final walletId in walletIds) {
+ final name = ref.read(pWalletName(walletId));
+
+ if (name.toLowerCase().contains(searchTerm.toLowerCase())) {
+ result.add(walletId);
+ }
+ }
+
+ return result;
+ }
+
+ @override
+ void initState() {
+ searchFieldFocusNode = FocusNode();
+ _searchController = TextEditingController();
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ searchFieldFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Choose from ${AppConfig.prefix}",
+ style: STextStyles.desktopH3(context),
+ ),
+ const SizedBox(height: 28),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ autocorrect: false,
+ enableSuggestions: false,
+ controller: _searchController,
+ focusNode: searchFieldFocusNode,
+ onChanged: (value) {
+ setState(() {
+ _searchTerm = value;
+ });
+ },
+ style: STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ height: 1.8,
+ ),
+ decoration:
+ standardInputDecoration(
+ "Search",
+ searchFieldFocusNode,
+ context,
+ desktopMed: true,
+ ).copyWith(
+ prefixIcon: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 18,
+ ),
+ child: SvgPicture.asset(
+ Assets.svg.search,
+ width: 20,
+ height: 20,
+ ),
+ ),
+ suffixIcon: _searchController.text.isNotEmpty
+ ? Padding(
+ padding: const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ children: [
+ TextFieldIconButton(
+ child: const XIcon(),
+ onTap: () async {
+ setState(() {
+ _searchController.text = "";
+ _searchTerm = "";
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ )
+ : null,
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+ Flexible(
+ child: Builder(
+ builder: (context) {
+ final wallets = ref
+ .watch(pWallets)
+ .wallets
+ .where((e) => e.info.coin == widget.coin);
+
+ if (wallets.isEmpty) {
+ return Column(
+ children: [
+ RoundedWhiteContainer(
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ child: Center(
+ child: Text(
+ "No ${widget.coin.ticker.toUpperCase()} wallets",
+ style: STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ List<String> walletIds = wallets.map((e) => e.walletId).toList();
+
+ walletIds = filter(walletIds, _searchTerm);
+
+ return ListView.separated(
+ primary: false,
+ itemCount: walletIds.length,
+ separatorBuilder: (_, __) => const SizedBox(height: 5),
+ itemBuilder: (context, index) =>
+ _WalletRow(walletId: walletIds[index]),
+ );
+ },
+ ),
+ ),
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ const Spacer(),
+ const SizedBox(width: 16),
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ buttonHeight: ButtonHeight.l,
+ onPressed: Navigator.of(context).pop,
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
+
+class _BalanceDisplay extends ConsumerWidget {
+ const _BalanceDisplay({super.key, required this.walletId, this.balanceType});
+
+ final String walletId;
+ final BalanceType? balanceType;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final coin = ref.watch(pWalletCoin(walletId));
+ final total = balanceType == BalanceType.public
+ ? ref.watch(pWalletBalance(walletId)).total
+ : balanceType == BalanceType.private
+ ? ref.watch(pWalletBalanceSecondary(walletId)).total +
+ ref.watch(pWalletBalanceTertiary(walletId)).total
+ : ref.watch(pWalletBalance(walletId)).total +
+ ref.watch(pWalletBalanceSecondary(walletId)).total +
+ ref.watch(pWalletBalanceTertiary(walletId)).total;
+
+ return Text(
+ ref.watch(pAmountFormatter(coin)).format(total),
+ style: STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
+ ),
+ textAlign: TextAlign.right,
+ );
+ }
+}
+
+class _WalletRow extends ConsumerWidget {
+ const _WalletRow({super.key, required this.walletId});
+
+ final String walletId;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final coin = ref.watch(pWalletCoin(walletId));
+
+ if (coin is! Firo) {
+ return RoundedWhiteContainer(
+ borderColor: Theme.of(context).extension<StackColors>()!.background,
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
+ child: Row(
+ children: [
+ Row(
+ children: [
+ WalletInfoCoinIcon(coin: coin),
+ const SizedBox(width: 12),
+ Text(
+ ref.watch(pWalletName(walletId)),
+ style: STextStyles.desktopTextExtraExtraSmall(context)
+ .copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textDark,
+ ),
+ ),
+ ],
+ ),
+ const Spacer(),
+ _BalanceDisplay(walletId: walletId),
+ const SizedBox(width: 80),
+ CustomTextButton(
+ text: "Select wallet",
+ onTap: () async {
+ final wallet = ref.read(pWallets).getWallet(walletId);
+ final address =
+ (await wallet.getCurrentReceivingAddress())?.value ??
+ wallet.info.cachedReceivingAddress;
+
+ if (context.mounted) {
+ Navigator.of(context).pop(Tuple2(wallet.info.name, address));
+ }
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ return RoundedWhiteContainer(
+ borderColor: Theme.of(context).extension<StackColors>()!.background,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
+ child: Column(
+ mainAxisSize: .min,
+ crossAxisAlignment: .start,
+ children: [
+ Row(
+ children: [
+ WalletInfoCoinIcon(coin: coin, size: 32),
+ const SizedBox(width: 12),
+ Text(
+ ref.watch(pWalletName(walletId)),
+ style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
+ color: Theme.of(context).extension<StackColors>()!.textDark,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ const SizedBox(
+ width: 12 + 32, // space + size of WalletInfoCoinIcon
+ ),
+ Text(
+ "Spark",
+ style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
+ color: Theme.of(context).extension<StackColors>()!.textDark,
+ ),
+ ),
+ const Spacer(),
+ _BalanceDisplay(walletId: walletId, balanceType: .private),
+ const SizedBox(width: 80),
+ CustomTextButton(
+ text: "Select wallet",
+ onTap: () async {
+ Future<String?> _future() async {
+ final wallet =
+ ref.read(pWallets).getWallet(walletId)
+ as SparkInterface;
+
+ final sparkAddress = await wallet
+ .getCurrentReceivingSparkAddress();
+ if (sparkAddress != null) {
+ return sparkAddress.value;
+ }
+
+ return (await wallet.generateNextSparkAddress(
+ saveToDB: true,
+ )).value;
+ }
+
+ Exception? ex;
+ final sparkAddress = await showLoading(
+ context: context,
+ message: "Fetching Spark address",
+ rootNavigator: Util.isDesktop,
+ delay: const Duration(milliseconds: 1200),
+ whileFutureAlt: _future,
+ onException: (e) => ex = e,
+ );
+
+ if (context.mounted) {
+ if (ex != null) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => StackOkDialog(
+ title: "Error",
+ message: ex
+ .toString()
+ .replaceFirst("Exception:", "")
+ .trim(),
+ maxWidth: 400,
+ desktopPopRootNavigator: true,
+ ),
+ );
+ } else {
+ Navigator.of(context).pop(
+ sparkAddress == null
+ ? null
+ : Tuple2(
+ "${ref.read(pWalletName(walletId))} (Spark)",
+ sparkAddress,
+ ),
+ );
+ }
+ }
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ const SizedBox(
+ width: 12 + 32, // space + size of WalletInfoCoinIcon
+ ),
+ Text(
+ "Transparent",
+ style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
+ color: Theme.of(context).extension<StackColors>()!.textDark,
+ ),
+ ),
+ const Spacer(),
+ _BalanceDisplay(walletId: walletId, balanceType: .public),
+ const SizedBox(width: 80),
+ CustomTextButton(
+ text: "Select wallet",
+ onTap: () async {
+ final wallet = ref.read(pWallets).getWallet(walletId);
+ final address =
+ (await wallet.getCurrentReceivingAddress())?.value ??
+ wallet.info.cachedReceivingAddress;
+
+ if (context.mounted) {
+ Navigator.of(
+ context,
+ ).pop(Tuple2("${wallet.info.name} (Transparent)", address));
+ }
+ },
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart
deleted file mode 100644
index 1dec08f..0000000
--- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * 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 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_svg/svg.dart';
-import 'package:tuple/tuple.dart';
-
-import '../../../app_config.dart';
-import '../../../providers/providers.dart';
-import '../../../themes/stack_colors.dart';
-import '../../../utilities/amount/amount.dart';
-import '../../../utilities/amount/amount_formatter.dart';
-import '../../../utilities/assets.dart';
-import '../../../utilities/constants.dart';
-import '../../../utilities/text_styles.dart';
-import '../../../wallets/crypto_currency/crypto_currency.dart';
-import '../../../wallets/isar/providers/wallet_info_provider.dart';
-import '../../../widgets/custom_buttons/blue_text_button.dart';
-import '../../../widgets/desktop/secondary_button.dart';
-import '../../../widgets/icon_widgets/x_icon.dart';
-import '../../../widgets/rounded_white_container.dart';
-import '../../../widgets/stack_text_field.dart';
-import '../../../widgets/textfield_icon_button.dart';
-import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
-
-class DesktopChooseFromStack extends ConsumerStatefulWidget {
- const DesktopChooseFromStack({
- super.key,
- required this.coin,
- });
-
- final CryptoCurrency coin;
-
- @override
- ConsumerState<DesktopChooseFromStack> createState() =>
- _DesktopChooseFromStackState();
-}
-
-class _DesktopChooseFromStackState
- extends ConsumerState<DesktopChooseFromStack> {
- late final TextEditingController _searchController;
- late final FocusNode searchFieldFocusNode;
-
- String _searchTerm = "";
-
- List<String> filter(List<String> walletIds, String searchTerm) {
- if (searchTerm.isEmpty) {
- return walletIds;
- }
-
- final List<String> result = [];
- for (final walletId in walletIds) {
- final name = ref.read(pWalletName(walletId));
-
- if (name.toLowerCase().contains(searchTerm.toLowerCase())) {
- result.add(walletId);
- }
- }
-
- return result;
- }
-
- @override
- void initState() {
- searchFieldFocusNode = FocusNode();
- _searchController = TextEditingController();
- super.initState();
- }
-
- @override
- void dispose() {
- _searchController.dispose();
- searchFieldFocusNode.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Choose from ${AppConfig.prefix}",
- style: STextStyles.desktopH3(context),
- ),
- const SizedBox(
- height: 28,
- ),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- child: TextField(
- autocorrect: false,
- enableSuggestions: false,
- controller: _searchController,
- focusNode: searchFieldFocusNode,
- onChanged: (value) {
- setState(() {
- _searchTerm = value;
- });
- },
- style: STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldActiveText,
- height: 1.8,
- ),
- decoration: standardInputDecoration(
- "Search",
- searchFieldFocusNode,
- context,
- desktopMed: true,
- ).copyWith(
- prefixIcon: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 18,
- ),
- child: SvgPicture.asset(
- Assets.svg.search,
- width: 20,
- height: 20,
- ),
- ),
- suffixIcon: _searchController.text.isNotEmpty
- ? Padding(
- padding: const EdgeInsets.only(right: 0),
- child: UnconstrainedBox(
- child: Row(
- children: [
- TextFieldIconButton(
- child: const XIcon(),
- onTap: () async {
- setState(() {
- _searchController.text = "";
- _searchTerm = "";
- });
- },
- ),
- ],
- ),
- ),
- )
- : null,
- ),
- ),
- ),
- const SizedBox(
- height: 16,
- ),
- Flexible(
- child: Builder(
- builder: (context) {
- final wallets = ref
- .watch(pWallets)
- .wallets
- .where((e) => e.info.coin == widget.coin);
-
- if (wallets.isEmpty) {
- return Column(
- children: [
- RoundedWhiteContainer(
- borderColor: Theme.of(context)
- .extension<StackColors>()!
- .background,
- child: Center(
- child: Text(
- "No ${widget.coin.ticker.toUpperCase()} wallets",
- style:
- STextStyles.desktopTextExtraExtraSmall(context),
- ),
- ),
- ),
- ],
- );
- }
-
- List<String> walletIds = wallets.map((e) => e.walletId).toList();
-
- walletIds = filter(walletIds, _searchTerm);
-
- return ListView.separated(
- primary: false,
- itemCount: walletIds.length,
- separatorBuilder: (_, __) => const SizedBox(
- height: 5,
- ),
- itemBuilder: (context, index) {
- final wallet = ref.watch(
- pWallets
- .select((value) => value.getWallet(walletIds[index])),
- );
-
- return RoundedWhiteContainer(
- borderColor:
- Theme.of(context).extension<StackColors>()!.background,
- padding: const EdgeInsets.symmetric(
- horizontal: 20,
- vertical: 14,
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Row(
- children: [
- WalletInfoCoinIcon(coin: widget.coin),
- const SizedBox(
- width: 12,
- ),
- Text(
- wallet.info.name,
- style: STextStyles.desktopTextExtraExtraSmall(
- context,
- ).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textDark,
- ),
- ),
- ],
- ),
- const Spacer(),
- _BalanceDisplay(
- walletId: walletIds[index],
- ),
- const SizedBox(
- width: 80,
- ),
- CustomTextButton(
- text: "Select wallet",
- onTap: () async {
- final address =
- (await wallet.getCurrentReceivingAddress())
- ?.value ??
- wallet.info.cachedReceivingAddress;
-
- if (mounted) {
- Navigator.of(context).pop(
- Tuple2(
- wallet.info.name,
- address,
- ),
- );
- }
- },
- ),
- ],
- ),
- );
- },
- );
- },
- ),
- ),
- const SizedBox(
- height: 20,
- ),
- Row(
- children: [
- const Spacer(),
- const SizedBox(
- width: 16,
- ),
- Expanded(
- child: SecondaryButton(
- label: "Cancel",
- buttonHeight: ButtonHeight.l,
- onPressed: Navigator.of(context).pop,
- ),
- ),
- ],
- ),
- ],
- );
- }
-}
-
-class _BalanceDisplay extends ConsumerWidget {
- const _BalanceDisplay({
- super.key,
- required this.walletId,
- });
-
- final String walletId;
-
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final coin = ref.watch(pWalletCoin(walletId));
- Amount total = ref.watch(pWalletBalance(walletId)).total;
- if (coin is Firo) {
- total += ref.watch(pWalletBalanceSecondary(walletId)).total;
- total += ref.watch(pWalletBalanceTertiary(walletId)).total;
- }
-
- return Text(
- ref.watch(pAmountFormatter(coin)).format(total),
- style: STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
- ),
- textAlign: TextAlign.right,
- );
- }
-}
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index e6a24ec..21c20a0 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -64,7 +64,7 @@ import 'pages/churning/churning_view.dart';
import 'pages/coin_control/coin_control_view.dart';
import 'pages/coin_control/utxo_details_view.dart';
import 'pages/epic_finalize_view/epic_finalize_view.dart';
-import 'pages/exchange_view/choose_from_stack_view.dart';
+import 'pages/exchange_view/choose_address_from_stack_view.dart';
import 'pages/exchange_view/edit_trade_note_view.dart';
import 'pages/exchange_view/exchange_step_views/step_1_view.dart';
import 'pages/exchange_view/exchange_step_views/step_2_view.dart';
@@ -2104,11 +2104,11 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
- case ChooseFromStackView.routeName:
+ case ChooseAddressFromStackView.routeName:
if (args is CryptoCurrency) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
- builder: (_) => ChooseFromStackView(coin: args),
+ builder: (_) => ChooseAddressFromStackView(coin: args),
settings: RouteSettings(name: settings.name),
);
}
diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart
index ef81c10..a1ec772 100644
--- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart
+++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart
@@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../db/isar/main_db.dart';
import '../../../models/isar/models/contract.dart';
+import '../../../providers/wallet/public_private_balance_state_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/amount/amount_formatter.dart';
@@ -28,10 +29,12 @@ class WalletInfoRowBalance extends ConsumerWidget {
super.key,
required this.walletId,
this.contractAddress,
+ this.balanceType,
});
final String walletId;
final String? contractAddress;
+ final BalanceType? balanceType;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -41,10 +44,13 @@ class WalletInfoRowBalance extends ConsumerWidget {
Contract? contract;
if (contractAddress == null) {
- totalBalance =
- info.cachedBalance.total +
- info.cachedBalanceSecondary.total +
- info.cachedBalanceTertiary.total;
+ totalBalance = balanceType == BalanceType.private
+ ? info.cachedBalanceSecondary.total + info.cachedBalanceTertiary.total
+ : balanceType == BalanceType.public
+ ? info.cachedBalance.total
+ : info.cachedBalance.total +
+ info.cachedBalanceSecondary.total +
+ info.cachedBalanceTertiary.total;
contract = null;
} else {
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.