UX changes to require a selection for multiselect settings
What changed, and why it matters
This commit adds a user-experience guard so that settings where multiple options can be toggled now require at least one option to remain selected. Previously, a user could back out of such a setting with nothing selected, which could leave the device in an unintended or non-functional configuration. The fix shows a warning and returns the user to the setting until they pick at least one option.
No urgent action required. Treat as a normal UX hardening commit. Reviewers may verify that all TYPE__MULTISELECT settings entries genuinely require at least one selection and that the warning flow does not create an unescapable loop.
Security signals we found
Input validation / enforced minimum selection for multiselect settings
New warning view to prevent accidental empty configuration
No cryptographic, network, or authentication changes
Evidence from the diff
The change introduces SettingsSelectionRequiredWarningView in src/seedsigner/views/settings_views.py. When the user presses BACK to leave a TYPE__MULTISELECT settings entry, the code now checks the captured initial_value (which holds the current selections). If no options are selected, it routes to a warning screen instead of exiting, and the warning screen sends the user back to the same settings entry. Tests and screenshot generation are updated accordingly. There is no cryptographic, network, or privilege-related change.
Changed components
src/seedsigner/views/settings_views.pytests/test_flows_settings.pytests/screenshot_generator/generator.pyInspect captured patch +62 / −5
diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py
index cbf66e0..56141c2 100644
--- a/src/seedsigner/views/settings_views.py
+++ b/src/seedsigner/views/settings_views.py
@@ -204,6 +204,13 @@ class SettingsEntryUpdateSelectionView(View):
)
if ret_value == RET_CODE__BACK_BUTTON:
+ if self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT:
+ # After the user finishes toggling multiselect options, initial_value will
+ # have their final selections when they hit BACK to exit. All current
+ # multiselect settings require at least one option to be selected.
+ if not initial_value:
+ return Destination(SettingsSelectionRequiredWarningView, view_args={"attr_name": self.settings_entry.attr_name})
+
if self.blocking_view:
return Destination(self.blocking_view, clear_history=True)
return settings_menu_view_destination
@@ -261,6 +268,37 @@ class SettingsEntryUpdateSelectionView(View):
+class SettingsSelectionRequiredWarningView(View):
+ def __init__(self, attr_name: str):
+ super().__init__()
+ self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
+
+
+ def run(self):
+ from seedsigner.gui.screens.screen import WarningScreen
+
+ # TRANSLATOR_NOTE: Title of a warning dialog when configuring a setting that requires at least one option to be selected.
+ title = _("Selection Required")
+
+ # TRANSLATOR_NOTE: The name of the setting being configured (e.g. "Script types") will be inserted.
+ text = _("At least one option must be selected for \"{}\".").format(self.settings_entry.display_name)
+
+ # TRANSLATOR_NOTE: Text for the button that returns the user to the setting configuration screen.
+ button_text = _("Return to setting")
+
+ self.run_screen(
+ WarningScreen,
+ title=title,
+ status_headline=None,
+ text=text,
+ button_data=[ButtonOption(button_text)],
+ show_back_button=False,
+ )
+
+ return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name))
+
+
+
class SettingsIngestSettingsQRView(View):
def __init__(self, data: str):
from seedsigner.hardware.microsd import MicroSD
diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py
index 673086f..6965419 100644
--- a/tests/screenshot_generator/generator.py
+++ b/tests/screenshot_generator/generator.py
@@ -427,6 +427,7 @@ def generate_screenshots(locale):
ScreenshotConfig(settings_views.DonateView),
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
+ ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)),
],
"Misc Error Views": [
ScreenshotConfig(NotYetImplementedView),
diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py
index 75fc530..ff73a3d 100644
--- a/tests/test_flows_settings.py
+++ b/tests/test_flows_settings.py
@@ -37,17 +37,35 @@ class TestSettingsFlows(FlowTest):
def test_multiselect(self):
- """ Multiselect Settings options should stay in-place; requires BACK to exit. """
+ """
+ Multiselect Settings options should stay in-place; requires BACK to exit. If no
+ selections are made, route to the warning screen and return the user to the
+ settings entry until at least one option is selected.
+ """
# Which option are we testing?
- settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__XPUB_QR_FORMAT)
+ settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES)
+
+ # Enable all options to start
+ self.settings.set_value(settings_entry.attr_name, [option[0] for option in settings_entry.selection_options])
+
+ # Sanity check, we only expect two options for this setting
+ assert len(settings_entry.selection_options) == 2
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.ADVANCED),
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
- FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option
- FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
- FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
+ FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # deselect first option
+ FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # deselect second option
+ FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select second option
+ FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # deselect second option
+ FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit
+
+ # Both options were deselected, should route to the warning screen
+ FlowStep(settings_views.SettingsSelectionRequiredWarningView),
+ FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select first option
+
+ # Now we can exit
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit
FlowStep(settings_views.SettingsMenuView),
])
Why this scored 23/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.