disable buy option of spark names in view only wallet (and hide names option in non spark firo view only wallets)
What changed, and why it matters
This commit changes the Stack Wallet app so that users with a 'view-only' wallet can no longer see buttons to buy or renew Spark names (a Firo cryptocurrency feature). It also makes sure the 'Names' menu appears only for the correct wallet types. The change is a UI restriction, not a server-side security fix, and appears aimed at preventing users from accidentally trying actions their wallet cannot perform.
Treat as a minor hardening/UI consistency change. If a security advisory is planned, it should clarify whether any actual unauthorized transaction or name operation was possible before this patch, because the diff alone only shows UI-level gating. Review whether the underlying buy/renew logic enforces the same view-only restrictions independently of the UI.
Security signals we found
UI guard added to disable a purchase/renew action for view-only wallets
View-only wallet state used to gate sensitive on-chain actions
No server-side or cryptographic changes visible in the diff
Evidence from the diff
The patch adds isViewOnly checks in three UI files. In buy_spark_name_option_widget.dart, the ‘Buy name’ button is disabled and its onPressed is set to null when the wallet is view-only. In spark_name_details.dart, the ‘Renew’ button is hidden when the wallet is view-only. In wallet_view.dart and desktop_wallet_features.dart, the ‘Names’ navigation item is shown only for full Spark wallets or Spark-specific view-only wallets, hiding it for non-Spark view-only wallets. The diff is mostly formatting/whitespace changes plus these guard conditions.
Changed components
lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dartlib/pages/spark_names/sub_widgets/spark_name_details.dartlib/pages/wallet_view/wallet_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dartInspect captured patch +204 / −203
diff --git a/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart
index 61c35fe..7a2d179 100644
--- a/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart
+++ b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart
@@ -295,6 +295,9 @@ class _NameCard extends ConsumerWidget {
? STextStyles.w500_16(context)
: STextStyles.w500_12(context));
+ final _isViewOnlyWallet =
+ (ref.read(pWallets).getWallet(walletId) as SparkInterface).isViewOnly;
+
return RoundedWhiteContainer(
padding: EdgeInsets.all(Util.isDesktop ? 24 : 16),
child: IntrinsicHeight(
@@ -318,59 +321,63 @@ class _NameCard extends ConsumerWidget {
children: [
PrimaryButton(
label: "Buy name",
- enabled: isAvailable,
+ enabled: !_isViewOnlyWallet && isAvailable,
buttonHeight: Util.isDesktop
? ButtonHeight.m
: ButtonHeight.l,
width: Util.isDesktop ? 140 : 120,
- onPressed: () async {
- if (context.mounted) {
- if (Util.isDesktop) {
- await showDialog<void>(
- context: context,
- builder: (context) => SDialog(
- child: SizedBox(
- width: 580,
- child: Column(
- children: [
- Row(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(
- left: 32,
+ onPressed: _isViewOnlyWallet
+ ? null
+ : () async {
+ if (context.mounted) {
+ if (Util.isDesktop) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => SDialog(
+ child: SizedBox(
+ width: 580,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 32,
+ ),
+ child: Text(
+ "Buy name",
+ style: STextStyles.desktopH3(
+ context,
+ ),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
),
- child: Text(
- "Buy name",
- style: STextStyles.desktopH3(context),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ ),
+ child: BuySparkNameView(
+ walletId: walletId,
+ name: name,
+ ),
),
- ),
- const DesktopDialogCloseButton(),
- ],
- ),
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- ),
- child: BuySparkNameView(
- walletId: walletId,
- name: name,
+ ],
),
),
- ],
- ),
- ),
- ),
- );
- } else {
- await Navigator.of(context).pushNamed(
- BuySparkNameView.routeName,
- arguments: (walletId: walletId, name: name),
- );
- }
- }
- },
+ ),
+ );
+ } else {
+ await Navigator.of(context).pushNamed(
+ BuySparkNameView.routeName,
+ arguments: (walletId: walletId, name: name),
+ );
+ }
+ }
+ },
),
],
),
diff --git a/lib/pages/spark_names/sub_widgets/spark_name_details.dart b/lib/pages/spark_names/sub_widgets/spark_name_details.dart
index 372f748..6a5c75a 100644
--- a/lib/pages/spark_names/sub_widgets/spark_name_details.dart
+++ b/lib/pages/spark_names/sub_widgets/spark_name_details.dart
@@ -5,10 +5,12 @@ import '../../../db/drift/database.dart';
import '../../../models/isar/models/isar_models.dart';
import '../../../providers/db/drift_provider.dart';
import '../../../providers/db/main_db_provider.dart';
+import '../../../providers/global/wallets_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@@ -45,6 +47,8 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
late Stream<SparkName?> _nameStream;
late SparkName name;
+ late final bool _isViewOnlyWallet;
+
Stream<AddressLabel?>? _labelStream;
AddressLabel? label;
@@ -81,37 +85,36 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
- builder:
- (context) => SDialog(
- child: SizedBox(
- width: 580,
- child: Column(
+ builder: (context) => SDialog(
+ child: SizedBox(
+ width: 580,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "Renew name",
- style: STextStyles.desktopH3(context),
- ),
- ),
- const DesktopDialogCloseButton(),
- ],
- ),
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: BuySparkNameView(
- walletId: widget.walletId,
- name: name.name,
- nameToRenew: name,
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Renew name",
+ style: STextStyles.desktopH3(context),
),
),
+ const DesktopDialogCloseButton(),
],
),
- ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: BuySparkNameView(
+ walletId: widget.walletId,
+ name: name.name,
+ nameToRenew: name,
+ ),
+ ),
+ ],
),
+ ),
+ ),
);
} else {
await Navigator.of(context).pushNamed(
@@ -133,6 +136,10 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
super.initState();
name = widget.name;
+ _isViewOnlyWallet =
+ (ref.read(pWallets).getWallet(widget.walletId) as SparkInterface)
+ .isViewOnly;
+
label = ref
.read(mainDBProvider)
.getAddressLabelSync(widget.walletId, name.address);
@@ -143,9 +150,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
final db = ref.read(pDrift(widget.walletId));
- _nameStream =
- (db.select(db.sparkNames)
- ..where((e) => e.name.equals(name.name))).watchSingleOrNull();
+ _nameStream = (db.select(
+ db.sparkNames,
+ )..where((e) => e.name.equals(name.name))).watchSingleOrNull();
}
@override
@@ -159,38 +166,37 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
return ConditionalParent(
condition: !Util.isDesktop,
- builder:
- (child) => Background(
- child: Scaffold(
- backgroundColor: Colors.transparent,
- appBar: AppBar(
- backgroundColor: Colors.transparent,
- // Theme.of(context).extension<StackColors>()!.background,
- leading: const AppBarBackButton(),
- title: Text(
- "Spark name details",
- style: STextStyles.navBarTitle(context),
- ),
- ),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight,
- ),
- child: IntrinsicHeight(child: child),
- ),
+ builder: (child) => Background(
+ child: Scaffold(
+ backgroundColor: Colors.transparent,
+ appBar: AppBar(
+ backgroundColor: Colors.transparent,
+ // Theme.of(context).extension<StackColors>()!.background,
+ leading: const AppBarBackButton(),
+ title: Text(
+ "Spark name details",
+ style: STextStyles.navBarTitle(context),
+ ),
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight,
),
- );
- },
- ),
- ),
+ child: IntrinsicHeight(child: child),
+ ),
+ ),
+ );
+ },
),
),
+ ),
+ ),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) {
@@ -221,10 +227,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
child: RoundedContainer(
padding: EdgeInsets.zero,
color: Colors.transparent,
- borderColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
child: child,
),
),
@@ -244,19 +249,17 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
children: [
RoundedContainer(
padding: const EdgeInsets.all(12),
- color:
- Util.isDesktop
- ? Colors.transparent
- : Theme.of(context).extension<StackColors>()!.popupBG,
+ color: Util.isDesktop
+ ? Colors.transparent
+ : Theme.of(context).extension<StackColors>()!.popupBG,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SelectableText(
name.name,
- style:
- Util.isDesktop
- ? STextStyles.pageTitleH2(context)
- : STextStyles.w500_14(context),
+ style: Util.isDesktop
+ ? STextStyles.pageTitleH2(context)
+ : STextStyles.w500_14(context),
),
],
),
@@ -264,14 +267,12 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
const _Div(),
RoundedContainer(
- padding:
- Util.isDesktop
- ? const EdgeInsets.all(16)
- : const EdgeInsets.all(12),
- color:
- Util.isDesktop
- ? Colors.transparent
- : Theme.of(context).extension<StackColors>()!.popupBG,
+ padding: Util.isDesktop
+ ? const EdgeInsets.all(16)
+ : const EdgeInsets.all(12),
+ color: Util.isDesktop
+ ? Colors.transparent
+ : Theme.of(context).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -282,10 +283,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
Text(
"Address",
style: STextStyles.w500_14(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle1,
),
),
Util.isDesktop
@@ -309,74 +309,69 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
return (label != null && label!.value.isNotEmpty)
? Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const _Div(),
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const _Div(),
- RoundedContainer(
- padding:
- Util.isDesktop
- ? const EdgeInsets.all(16)
- : const EdgeInsets.all(12),
- color:
- Util.isDesktop
- ? Colors.transparent
- : Theme.of(
+ RoundedContainer(
+ padding: Util.isDesktop
+ ? const EdgeInsets.all(16)
+ : const EdgeInsets.all(12),
+ color: Util.isDesktop
+ ? Colors.transparent
+ : Theme.of(
context,
).extension<StackColors>()!.popupBG,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- children: [
- Text(
- "Address label",
- style: STextStyles.w500_14(
- context,
- ).copyWith(
- color:
- Theme.of(context)
- .extension<StackColors>()!
- .textSubtitle1,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Address label",
+ style: STextStyles.w500_14(context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textSubtitle1,
+ ),
),
- ),
- Util.isDesktop
- ? tvd.IconCopyButton(
- data: label!.value,
- )
- : SimpleCopyButton(
- data: label!.value,
- ),
- ],
- ),
- const SizedBox(height: 4),
- SelectableText(
- label!.value,
- style: STextStyles.w500_14(context),
- ),
- ],
+ Util.isDesktop
+ ? tvd.IconCopyButton(
+ data: label!.value,
+ )
+ : SimpleCopyButton(
+ data: label!.value,
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ SelectableText(
+ label!.value,
+ style: STextStyles.w500_14(context),
+ ),
+ ],
+ ),
),
- ),
- ],
- )
+ ],
+ )
: const SizedBox(width: 0, height: 0);
},
),
const _Div(),
RoundedContainer(
- padding:
- Util.isDesktop
- ? const EdgeInsets.all(16)
- : const EdgeInsets.all(12),
- color:
- Util.isDesktop
- ? Colors.transparent
- : Theme.of(context).extension<StackColors>()!.popupBG,
+ padding: Util.isDesktop
+ ? const EdgeInsets.all(16)
+ : const EdgeInsets.all(12),
+ color: Util.isDesktop
+ ? Colors.transparent
+ : Theme.of(context).extension<StackColors>()!.popupBG,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -387,10 +382,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
Text(
"Expiry",
style: STextStyles.w500_14(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle1,
),
),
const SizedBox(height: 4),
@@ -402,11 +396,12 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
),
],
),
- if (remaining < _remainingMagic)
+ if (remaining < _remainingMagic && !_isViewOnlyWallet)
PrimaryButton(
label: "Renew",
- buttonHeight:
- Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l,
+ buttonHeight: Util.isDesktop
+ ? ButtonHeight.xs
+ : ButtonHeight.l,
onPressed: _renew,
),
],
@@ -414,14 +409,12 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
),
const _Div(),
RoundedContainer(
- padding:
- Util.isDesktop
- ? const EdgeInsets.all(16)
- : const EdgeInsets.all(12),
- color:
- Util.isDesktop
- ? Colors.transparent
- : Theme.of(context).extension<StackColors>()!.popupBG,
+ padding: Util.isDesktop
+ ? const EdgeInsets.all(16)
+ : const EdgeInsets.all(12),
+ color: Util.isDesktop
+ ? Colors.transparent
+ : Theme.of(context).extension<StackColors>()!.popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -429,10 +422,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
Text(
"Additional info",
style: STextStyles.w500_14(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle1,
),
),
const SizedBox(height: 4),
diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart
index c3c6ff4..b3a3338 100644
--- a/lib/pages/wallet_view/wallet_view.dart
+++ b/lib/pages/wallet_view/wallet_view.dart
@@ -1082,7 +1082,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
icon: const BuyNavIcon(),
onTap: () => _onBuyPressed(context),
),
- if (wallet is SparkInterface)
+ if (wallet is SparkInterface ||
+ (viewOnly && wallet.viewOnlyType == .spark))
WalletNavigationBarItemData(
label: "Names",
icon: const PaynymNavIcon(),
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 dd8869c..63366f2 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
@@ -462,6 +462,7 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
) {
final coin = wallet.info.coin;
final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
+ final isSparkViewOnly = isViewOnly && wallet.viewOnlyType == .spark;
return [
if (!isViewOnly &&
@@ -473,7 +474,7 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
_onAnonymizeAllPressed,
),
- if (wallet is SparkInterface)
+ if (wallet is SparkInterface && !isViewOnly || isSparkViewOnly)
(WalletFeature.sparkNames, Assets.svg.robotHead, _onSparkNamesPressed),
if (!isViewOnly &&
Why this scored 29/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.