From 6182fbd792ba463a26169a41aade88aebb456ef8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 21 May 2026 16:07:04 -0600 Subject: [PATCH 1/4] fix: bandaid fix for race condition when checkElectrumAdapter is called concurrently. The architecture of ElectrumXClient and ClientManager needs to be revisited and refactored in a safer way --- lib/electrumx_rpc/electrumx_client.dart | 188 ++++++++++++------------ 1 file changed, 97 insertions(+), 91 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index abcb01296..94b650b10 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -117,6 +117,8 @@ class ElectrumXClient { final Mutex _torConnectingLock = Mutex(); bool _requireMutex = false; + final _adapterMutex = Mutex(); + ElectrumXClient({ required String host, required int port, @@ -219,111 +221,115 @@ class ElectrumXClient { } Future checkElectrumAdapter() async { - ({InternetAddress host, int port})? proxyInfo; - - if (AppConfig.hasFeature(AppFeature.tor)) { - // If we're supposed to use Tor... - if (_prefs.useTor) { - // But Tor isn't running... - if (_torService.status != TorConnectionStatus.connected) { - // And the killswitch isn't set... - if (!_prefs.torKillSwitch) { - // Then we'll just proceed and connect to ElectrumX through - // clearnet at the bottom of this function. - Logging.instance.w( - "Tor preference set but Tor is not enabled, killswitch not set," - " connecting to Electrum adapter through clearnet", - ); + await _adapterMutex.protect(() async { + ({InternetAddress host, int port})? proxyInfo; + + if (AppConfig.hasFeature(AppFeature.tor)) { + // If we're supposed to use Tor... + if (_prefs.useTor) { + // But Tor isn't running... + if (_torService.status != TorConnectionStatus.connected) { + // And the killswitch isn't set... + if (!_prefs.torKillSwitch) { + // Then we'll just proceed and connect to ElectrumX through + // clearnet at the bottom of this function. + Logging.instance.w( + "Tor preference set but Tor is not enabled, killswitch not set," + " connecting to Electrum adapter through clearnet", + ); + } else { + // ... But if the killswitch is set, then we throw an exception. + throw Exception( + "Tor preference and killswitch set but Tor is not enabled, " + "not connecting to Electrum adapter", + ); + // TODO [prio=low]: Try to start Tor. + } } else { - // ... But if the killswitch is set, then we throw an exception. - throw Exception( - "Tor preference and killswitch set but Tor is not enabled, " - "not connecting to Electrum adapter", + // Get the proxy info from the TorService. + proxyInfo = _torService.getProxyInfo(); + } + + if (netType == TorPlainNetworkOption.clear) { + _electrumAdapterChannel = null; + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, ); - // TODO [prio=low]: Try to start Tor. } } else { - // Get the proxy info from the TorService. - proxyInfo = _torService.getProxyInfo(); - } - - if (netType == TorPlainNetworkOption.clear) { - _electrumAdapterChannel = null; - await ClientManager.sharedInstance.remove( - cryptoCurrency: cryptoCurrency, - ); - } - } else { - if (netType == TorPlainNetworkOption.tor) { - _electrumAdapterChannel = null; - await ClientManager.sharedInstance.remove( - cryptoCurrency: cryptoCurrency, - ); + if (netType == TorPlainNetworkOption.tor) { + _electrumAdapterChannel = null; + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + ); + } } } - } - // If the current ElectrumAdapterClient is closed, create a new one. - if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) { - _electrumAdapterChannel = null; - await ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency); - } - - final String useHost; - final int usePort; - final bool useUseSSL; - - if (currentFailoverIndex == -1) { - useHost = host; - usePort = port; - useUseSSL = useSSL; - } else { - _electrumAdapterChannel = null; - await ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency); - useHost = _failovers[currentFailoverIndex].address; - usePort = _failovers[currentFailoverIndex].port; - useUseSSL = _failovers[currentFailoverIndex].useSSL; - } + // If the current ElectrumAdapterClient is closed, create a new one. + if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) { + _electrumAdapterChannel = null; + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + ); + } - _electrumAdapterChannel ??= await electrum_adapter.connect( - useHost, - port: usePort, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, - acceptUnverified: false, - useSSL: useUseSSL, - proxyInfo: proxyInfo, - ); + final String useHost; + final int usePort; + final bool useUseSSL; - if (getElectrumAdapter() == null) { - final ElectrumClient newClient; - if (cryptoCurrency is Firo) { - newClient = FiroElectrumClient( - _electrumAdapterChannel!, - useHost, - usePort, - useUseSSL, - proxyInfo, - ); + if (currentFailoverIndex == -1) { + useHost = host; + usePort = port; + useUseSSL = useSSL; } else { - newClient = ElectrumClient( - _electrumAdapterChannel!, - useHost, - usePort, - useUseSSL, - proxyInfo, + _electrumAdapterChannel = null; + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, ); + useHost = _failovers[currentFailoverIndex].address; + usePort = _failovers[currentFailoverIndex].port; + useUseSSL = _failovers[currentFailoverIndex].useSSL; } - await newClient.request('server.version'); - - await ClientManager.sharedInstance.addClient( - newClient, - cryptoCurrency: cryptoCurrency, - netType: netType, + _electrumAdapterChannel ??= await electrum_adapter.connect( + useHost, + port: usePort, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, + acceptUnverified: false, + useSSL: useUseSSL, + proxyInfo: proxyInfo, ); - } - return; + if (getElectrumAdapter() == null) { + final ElectrumClient newClient; + if (cryptoCurrency is Firo) { + newClient = FiroElectrumClient( + _electrumAdapterChannel!, + useHost, + usePort, + useUseSSL, + proxyInfo, + ); + } else { + newClient = ElectrumClient( + _electrumAdapterChannel!, + useHost, + usePort, + useUseSSL, + proxyInfo, + ); + } + + await newClient.request('server.version'); + + await ClientManager.sharedInstance.addClient( + newClient, + cryptoCurrency: cryptoCurrency, + netType: netType, + ); + } + }); } /// Send raw rpc command From 6e9c01ed60c2951800f0aaec7b30acc3edc0c7db Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 22 May 2026 09:40:20 -0600 Subject: [PATCH 2/4] handle here instead of https://github.com/cypherstack/stack_wallet/pull/1345 due to conflicts that I don't want to deal with --- crypto_plugins/flutter_libmwc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmwc b/crypto_plugins/flutter_libmwc index cb444bf93..c8db22aed 160000 --- a/crypto_plugins/flutter_libmwc +++ b/crypto_plugins/flutter_libmwc @@ -1 +1 @@ -Subproject commit cb444bf93c4c6e5305a3fb94641f42d908c199e9 +Subproject commit c8db22aed2c50aa1e95dfc532abb0a4961c543d7 From 75733fe49efeda7221758f76bb555723b4b4a175 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 22 May 2026 09:56:43 -0600 Subject: [PATCH 3/4] hide/disable coin control view for salvium as the underlying library doesn't fully support it --- .../send_view/frost_ms/frost_send_view.dart | 201 +++++++++--------- lib/pages/send_view/send_view.dart | 8 +- lib/pages/wallet_view/wallet_view.dart | 2 + .../wallet_view/sub_widgets/desktop_send.dart | 8 +- .../sub_widgets/desktop_wallet_features.dart | 2 + 5 files changed, 116 insertions(+), 105 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 4b1014158..59bdc843e 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -33,6 +33,7 @@ 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/bitcoin_frost_wallet.dart'; +import '../../../wallets/wallet/impl/salvium_wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; @@ -164,10 +165,9 @@ class _FrostSendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -231,6 +231,7 @@ class _FrostSendViewState extends ConsumerState { final showCoinControl = wallet is CoinControlInterface && + wallet is! SalviumWallet && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, @@ -242,59 +243,56 @@ class _FrostSendViewState extends ConsumerState { return ConditionalParent( condition: !Util.isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Send ${coin.ticker}", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: child, - ), - ), + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, ), - ); - }, - ), - ), + ), + ), + ); + }, ), ), + ), + ), child: ConditionalParent( condition: Util.isDesktop, - builder: - (child) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - child: child, - ), + builder: (child) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: child, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -375,11 +373,10 @@ class _FrostSendViewState extends ConsumerState { for (int i = 0; i < recipientWidgetIndexes.length; i++) ConditionalParent( condition: recipientWidgetIndexes.length > 1, - builder: - (child) => Padding( - padding: const EdgeInsets.only(top: 8), - child: child, - ), + builder: (child) => Padding( + padding: const EdgeInsets.only(top: 8), + child: child, + ), child: Recipient( key: Key("recipientKey_${recipientWidgetIndexes[i]}"), index: recipientWidgetIndexes[i], @@ -388,21 +385,21 @@ class _FrostSendViewState extends ConsumerState { onChanged: () { _validateRecipientFormStates(); }, - remove: - i == 0 && recipientWidgetIndexes.length == 1 - ? null - : () { - ref - .read( - pRecipient( - recipientWidgetIndexes[i], - ).notifier, - ) - .state = null; - recipientWidgetIndexes.removeAt(i); - setState(() {}); - _validateRecipientFormStates(); - }, + remove: i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + ref + .read( + pRecipient( + recipientWidgetIndexes[i], + ).notifier, + ) + .state = + null; + recipientWidgetIndexes.removeAt(i); + setState(() {}); + _validateRecipientFormStates(); + }, addAnotherRecipientTapped: () { // used for tracking recipient forms _greatestWidgetIndex++; @@ -443,17 +440,15 @@ class _FrostSendViewState extends ConsumerState { Text( "Coin control", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), CustomTextButton( - text: - selectedUTXOs.isEmpty - ? "Select coins" - : "Selected coins (${selectedUTXOs.length})", + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", onTap: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -506,32 +501,32 @@ class _FrostSendViewState extends ConsumerState { focusNode: _noteFocusNode, style: STextStyles.field(context), onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: - noteController.text.isNotEmpty + decoration: + standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - noteController.text = ""; - }); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), ), ), const SizedBox(height: 12), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 94b5663c8..a2dd4f483 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -57,6 +57,7 @@ import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; +import '../../wallets/wallet/impl/salvium_wallet.dart'; import '../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; @@ -813,7 +814,9 @@ class _SendViewState extends ConsumerState { .enableCoinControl; if (coin is! Ethereum && - !(wallet is CoinControlInterface && coinControlEnabled) || + !(wallet is CoinControlInterface && + wallet is! SalviumWallet && + coinControlEnabled) || (wallet is CoinControlInterface && coinControlEnabled && selectedUTXOs.isEmpty)) { @@ -915,6 +918,7 @@ class _SendViewState extends ConsumerState { feeRateType: feeRate, utxos: (wallet is CoinControlInterface && + wallet is! SalviumWallet && coinControlEnabled && selectedUTXOs.isNotEmpty) ? selectedUTXOs @@ -1037,6 +1041,7 @@ class _SendViewState extends ConsumerState { ethEIP1559Fee: ethFee, utxos: (wallet is CoinControlInterface && + wallet is! SalviumWallet && coinControlEnabled && selectedUTXOs.isNotEmpty) ? selectedUTXOs @@ -1405,6 +1410,7 @@ class _SendViewState extends ConsumerState { ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && + ref.watch(pWallets).getWallet(walletId) is! SalviumWallet && (showPrivateBalance ? balType == BalanceType.public : true); final isExchangeAddress = ref.watch(pIsExchangeAddress); diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 83a4d6e8f..0dc8d171c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -54,6 +54,7 @@ import '../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/impl/salvium_wallet.dart'; import '../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; @@ -1172,6 +1173,7 @@ class _WalletViewState extends ConsumerState { }, ), if (wallet is CoinControlInterface && + wallet is! SalviumWallet && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index b8dc85f4d..efc02f628 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -56,6 +56,7 @@ import '../../../../wallets/models/tx_data.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; +import '../../../../wallets/wallet/impl/salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; @@ -458,7 +459,9 @@ class _DesktopSendState extends ConsumerState { .read(prefsChangeNotifierProvider) .enableCoinControl; - if (!(wallet is CoinControlInterface && coinControlEnabled) || + if (!(wallet is CoinControlInterface && + wallet is! SalviumWallet && + coinControlEnabled) || (coinControlEnabled && ref.read(desktopUseUTXOs).isEmpty)) { // confirm send all if (amount == availableBalance) { @@ -597,6 +600,7 @@ class _DesktopSendState extends ConsumerState { feeRateType: feeRate, utxos: (wallet is CoinControlInterface && + wallet is! SalviumWallet && coinControlEnabled && ref.read(pDesktopUseUTXOs).isNotEmpty) ? ref.read(pDesktopUseUTXOs) @@ -724,6 +728,7 @@ class _DesktopSendState extends ConsumerState { : null, utxos: (wallet is CoinControlInterface && + wallet is! SalviumWallet && coinControlEnabled && ref.read(pDesktopUseUTXOs).isNotEmpty) ? ref.read(pDesktopUseUTXOs) @@ -1351,6 +1356,7 @@ class _DesktopSendState extends ConsumerState { ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && + ref.watch(pWallets).getWallet(walletId) is! SalviumWallet && (showPrivateBalance ? balType == BalanceType.public : true); return Column( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index a9458bfd9..ca0a2ae0c 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -44,6 +44,7 @@ import '../../../../wallets/crypto_currency/coins/firo.dart'; import '../../../../wallets/wallet/impl/bitcoin_wallet.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../../wallets/wallet/impl/salvium_wallet.dart'; import '../../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet.dart' show Wallet; @@ -561,6 +562,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { prefsChangeNotifierProvider.select((value) => value.enableExchange), ), (wallet is CoinControlInterface && + wallet is! SalviumWallet && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, From fd8bb87b65c363e74a560072d67487b3330e332f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 22 May 2026 11:05:51 -0600 Subject: [PATCH 4/4] hide/disable shopinbit/cakepay based on app features flags --- .../global_settings_view.dart | 56 ++++++++------- lib/pages/wallet_view/wallet_view.dart | 31 ++++---- lib/pages_desktop_specific/desktop_menu.dart | 25 ++++--- .../services/desktop_services_view.dart | 70 ++++++++++++------- .../settings/desktop_settings_view.dart | 2 +- .../settings/settings_menu.dart | 2 +- 6 files changed, 105 insertions(+), 81 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/global_settings_view.dart b/lib/pages/settings_views/global_settings_view/global_settings_view.dart index 40b53198c..729709f81 100644 --- a/lib/pages/settings_views/global_settings_view/global_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/global_settings_view.dart @@ -247,33 +247,37 @@ class GlobalSettingsView extends StatelessWidget { ); }, ), - Consumer( - builder: (_, ref, __) { - final familiarity = ref.watch( - prefsChangeNotifierProvider.select( - (v) => v.familiarity, - ), - ); - if (familiarity < 6) { - return const SizedBox.shrink(); - } - return Column( - children: [ - const SizedBox(height: 8), - SettingsListButton( - iconAssetName: Assets.svg.key, - iconSize: 16, - title: "ShopinBit", - onPressed: () { - Navigator.of(context).pushNamed( - ShopInBitSettingsView.routeName, - ); - }, + if (AppConfig.hasFeature( + AppFeature.shopinBit, + )) + Consumer( + builder: (_, ref, __) { + final familiarity = ref.watch( + prefsChangeNotifierProvider.select( + (v) => v.familiarity, ), - ], - ); - }, - ), + ); + if (familiarity < 6) { + return const SizedBox.shrink(); + } + return Column( + children: [ + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.key, + iconSize: 16, + title: "ShopinBit", + onPressed: () { + Navigator.of(context).pushNamed( + ShopInBitSettingsView + .routeName, + ); + }, + ), + ], + ); + }, + ), const SizedBox(height: 8), SettingsListButton( iconAssetName: Assets.svg.questionMessage, diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 0dc8d171c..04c0888d5 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -1348,7 +1348,7 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (!viewOnly) + if (!viewOnly && AppConfig.hasFeature(.shopinBit)) WalletNavigationBarItemData( label: "Services", icon: SvgPicture.asset( @@ -1365,21 +1365,22 @@ class _WalletViewState extends ConsumerState { ).pushNamed(ServicesView.routeName); }, ), - WalletNavigationBarItemData( - label: "Gift cards", - icon: CreditCardIcon( - height: 20, - width: 20, - color: Theme.of( - context, - ).extension()!.bottomNavIconIcon, + if (AppConfig.hasFeature(.shopinBit)) + WalletNavigationBarItemData( + label: "Gift cards", + icon: CreditCardIcon( + height: 20, + width: 20, + color: Theme.of( + context, + ).extension()!.bottomNavIconIcon, + ), + onTap: () { + Navigator.of( + context, + ).pushNamed(GiftCardsView.routeName); + }, ), - onTap: () { - Navigator.of( - context, - ).pushNamed(GiftCardsView.routeName); - }, - ), ], ), ), diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index 5ffe149a1..7602ca532 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -223,17 +223,20 @@ class _DesktopMenuState extends ConsumerState { isExpandedInitially: !_isMinimized, ), ], - const SizedBox(height: 2), - DesktopMenuItem( - key: const ValueKey('services'), - duration: duration, - icon: const DesktopServicesIcon(), - label: "Services", - value: DesktopMenuItemId.services, - onChanged: updateSelectedMenuItem, - controller: controllers[3], - isExpandedInitially: !_isMinimized, - ), + if (AppConfig.hasFeature(.shopinBit) || + AppConfig.hasFeature(.cakePay)) ...[ + const SizedBox(height: 2), + DesktopMenuItem( + key: const ValueKey('services'), + duration: duration, + icon: const DesktopServicesIcon(), + label: "Services", + value: DesktopMenuItemId.services, + onChanged: updateSelectedMenuItem, + controller: controllers[3], + isExpandedInitially: !_isMinimized, + ), + ], const SizedBox(height: 2), DesktopMenuItem( key: const ValueKey('notifications'), diff --git a/lib/pages_desktop_specific/services/desktop_services_view.dart b/lib/pages_desktop_specific/services/desktop_services_view.dart index f94f70883..7a24d94ae 100644 --- a/lib/pages_desktop_specific/services/desktop_services_view.dart +++ b/lib/pages_desktop_specific/services/desktop_services_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../app_config.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; @@ -12,7 +13,22 @@ import '../settings/settings_menu_item.dart'; import 'cakepay/desktop_gift_cards_view.dart'; import 'shopin_bit/desktop_shopinbit_view.dart'; -final selectedServicesMenuItemStateProvider = StateProvider((_) => 0); +final _selectedServicesMenuItemStateProvider = StateProvider<_MenuItem?>( + (_) => _labels.firstOrNull, +); + +enum _MenuItem { + shopinBit("Services"), + cakePay("Gift Cards"); + + final String value; + const _MenuItem(this.value); +} + +final _labels = [ + if (AppConfig.hasFeature(.shopinBit)) _MenuItem.shopinBit, + if (AppConfig.hasFeature(.cakePay)) _MenuItem.cakePay, +]; class DesktopServicesView extends ConsumerStatefulWidget { const DesktopServicesView({super.key}); @@ -25,22 +41,22 @@ class DesktopServicesView extends ConsumerStatefulWidget { } class _DesktopServicesViewState extends ConsumerState { - final List _labels = const ["Services", "Gift Cards"]; - @override Widget build(BuildContext context) { - final List contentViews = [ - const Navigator( - key: Key("servicesShopInBitDesktopKey"), - onGenerateRoute: RouteGenerator.generateRoute, - initialRoute: DesktopShopInBitView.routeName, - ), - const Navigator( - key: Key("servicesGiftCardsDesktopKey"), - onGenerateRoute: RouteGenerator.generateRoute, - initialRoute: DesktopGiftCardsView.routeName, - ), - ]; + final Map<_MenuItem, Widget> contentViews = { + if (AppConfig.hasFeature(.shopinBit)) + .shopinBit: const Navigator( + key: Key("servicesShopInBitDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopShopInBitView.routeName, + ), + if (AppConfig.hasFeature(.cakePay)) + .cakePay: const Navigator( + key: Key("servicesGiftCardsDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopGiftCardsView.routeName, + ), + }; return DesktopScaffold( background: Theme.of(context).extension()!.background, @@ -68,12 +84,11 @@ class _DesktopServicesViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - for (int i = 0; i < _labels.length; i++) - Column( + ..._labels.map( + (label) => Column( mainAxisSize: MainAxisSize.min, children: [ - if (i > 0) const SizedBox(height: 2), - SettingsMenuItem( + SettingsMenuItem<_MenuItem?>( icon: SvgPicture.asset( Assets.svg.polygon, width: 11, @@ -81,28 +96,28 @@ class _DesktopServicesViewState extends ConsumerState { color: ref .watch( - selectedServicesMenuItemStateProvider + _selectedServicesMenuItemStateProvider .state, ) .state == - i + label ? Theme.of(context) .extension()! .accentColorBlue : Colors.transparent, ), - label: _labels[i], - value: i, + label: label.value, + value: label, group: ref .watch( - selectedServicesMenuItemStateProvider + _selectedServicesMenuItemStateProvider .state, ) .state, onChanged: (newValue) => ref .read( - selectedServicesMenuItemStateProvider + _selectedServicesMenuItemStateProvider .state, ) .state = @@ -110,6 +125,7 @@ class _DesktopServicesViewState extends ConsumerState { ), ], ), + ), ], ), ), @@ -121,8 +137,8 @@ class _DesktopServicesViewState extends ConsumerState { Expanded( child: contentViews[ref - .watch(selectedServicesMenuItemStateProvider.state) - .state], + .watch(_selectedServicesMenuItemStateProvider.state) + .state]!, ), ], ), diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart index ee2c423b3..65569890d 100644 --- a/lib/pages_desktop_specific/settings/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/settings/desktop_settings_view.dart @@ -94,7 +94,7 @@ class _DesktopSettingsViewState extends ConsumerState { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: AdvancedSettings.routeName, ), //advanced - if (familiarity >= 6) + if (AppConfig.hasFeature(.shopinBit) && familiarity >= 6) const Navigator( key: Key("settingsShopInBitDesktopKey"), onGenerateRoute: RouteGenerator.generateRoute, diff --git a/lib/pages_desktop_specific/settings/settings_menu.dart b/lib/pages_desktop_specific/settings/settings_menu.dart index a7f5129c1..be49fc2a2 100644 --- a/lib/pages_desktop_specific/settings/settings_menu.dart +++ b/lib/pages_desktop_specific/settings/settings_menu.dart @@ -46,7 +46,7 @@ class _SettingsMenuState extends ConsumerState { "Syncing preferences", if (AppConfig.hasFeature(AppFeature.themeSelection)) "Appearance", "Advanced", - if (familiarity >= 6) "ShopinBit", + if (AppConfig.hasFeature(.shopinBit) && familiarity >= 6) "ShopinBit", ]; return Column(