Option MICROSD_TOAST_TIMER_FOREVER refactor
What changed, and why it matters
This commit changes how SeedSigner handles a warning screen that appears when a MicroSD card is inserted but the device is set to never show reminders about it. Previously, users could simply dismiss the warning and continue using the device with the MicroSD still inserted. After this change, the warning becomes harder to bypass: users must either physically remove the MicroSD card or change the setting that suppresses the reminders. The change is framed by the project as a refactor of a settings option, not as a security fix.
Treat this as a hardening or UX-safety change rather than an urgent vulnerability patch. Review whether the original dismiss behavior was considered a security issue by the project, and verify the new blocking flow does not introduce unintended lockout or navigation bugs. No immediate incident response is indicated by the commit content alone.
Security signals we found
UI flow change that removes a one-click dismiss path from a hardware-state warning
Blocking navigation pattern introduced to prevent bypass of a MicroSD insertion warning
Warning screen now tied to a physical security action (removing MicroSD) or a settings change
No mention of CVE, vulnerability, exploit, or security advisory in commit message or diff
Evidence from the diff
The patch refactors RemoveMicroSDWarningView so that its ‘Dismiss’ button becomes a ‘Settings’ button. Pressing Settings launches SettingsEntryUpdateSelectionView in a new blocking mode. In blocking mode, if the user backs out without changing the MICROSD_TOAST_TIMER setting, the app returns to RemoveMicroSDWarningView. The only ways to leave the warning are: (1) physically remove the MicroSD and press Continue, or (2) change the setting away from MICROSD_TOAST_TIMER_FOREVER. SettingsEntryUpdateSelectionView now accepts blocking_view and unblocking_view arguments and enforces this behavior in its run() method. A new test verifies the three exit paths.
Changed components
src/seedsigner/views/view.pysrc/seedsigner/views/settings_views.pytests/test_flows.pyInspect captured patch +87 / −12
diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py
index b6fa789..cbf66e0 100644
--- a/src/seedsigner/views/settings_views.py
+++ b/src/seedsigner/views/settings_views.py
@@ -150,11 +150,15 @@ class SettingsEntryUpdateSelectionView(View):
Handles changes to all selection-type settings (Multiselect, SELECT_1,
Enabled/Disabled, etc).
"""
- def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None):
+ def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None, blocking_view: View = None, unblocking_view: View = None):
super().__init__()
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
self.selected_button = selected_button
self.parent_initial_scroll = parent_initial_scroll
+ # If the setting remains unchanged, navigation should return to blocking_view (if set)
+ self.blocking_view = blocking_view
+ # unblocking_view is an optional target to navigate to once the setting actually changes.
+ self.unblocking_view = unblocking_view
def run(self):
@@ -200,6 +204,8 @@ class SettingsEntryUpdateSelectionView(View):
)
if ret_value == RET_CODE__BACK_BUTTON:
+ if self.blocking_view:
+ return Destination(self.blocking_view, clear_history=True)
return settings_menu_view_destination
value = self.settings_entry.get_selection_option_value(ret_value)
@@ -219,8 +225,7 @@ class SettingsEntryUpdateSelectionView(View):
else:
# All other types are single selects (e.g. Enabled/Disabled, SELECT_1)
- if value == initial_value:
- # No change, return to menu
+ if value == initial_value and not self.blocking_view:
return settings_menu_view_destination
else:
updated_value = value
@@ -238,11 +243,21 @@ class SettingsEntryUpdateSelectionView(View):
if destination:
return destination
+
+ # If this selection view was opened from a blocking flow (e.g. RemoveMicroSDWarningView),
+ # prevent navigation away until the setting actually changes. If it hasn't changed,
+ # return to the blocking view so it can re-evaluate the state.
+ if self.blocking_view:
+ current_value = self.settings.get_value(self.settings_entry.attr_name)
+ if current_value == initial_value:
+ return Destination(self.blocking_view, clear_history=True)
+ elif self.unblocking_view:
+ return Destination(self.unblocking_view, clear_history=True)
# All selects stay in place; re-initialize where in the list we left off
self.selected_button = ret_value
- return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True)
+ return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button, blocking_view=self.blocking_view, unblocking_view=self.unblocking_view), skip_current_view=True)
diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py
index d7e0c26..24d7de8 100644
--- a/src/seedsigner/views/view.py
+++ b/src/seedsigner/views/view.py
@@ -412,10 +412,10 @@ class OptionDisabledView(View):
class RemoveMicroSDWarningView(View):
CONTINUE = ButtonOption("Continue")
- DISMISS = ButtonOption("Dismiss")
+ SETTINGS = ButtonOption("Settings")
def run(self):
- button_data = [self.CONTINUE, self.DISMISS]
+ button_data = [self.CONTINUE, self.SETTINGS]
selected_menu_num = self.run_screen(
WarningScreen,
title=_("Action Required"),
@@ -426,8 +426,20 @@ class RemoveMicroSDWarningView(View):
button_data=button_data,
)
- from seedsigner.hardware.microsd import MicroSD
- if button_data[selected_menu_num] == self.CONTINUE and MicroSD.get_instance().is_inserted:
- return Destination(RemoveMicroSDWarningView, clear_history=True)
- else:
- return Destination(MainMenuView, clear_history=True)
+ if button_data[selected_menu_num] == self.CONTINUE:
+ from seedsigner.hardware.microsd import MicroSD
+ if not MicroSD.get_instance().is_inserted:
+ return Destination(MainMenuView, clear_history=True)
+ else:
+ return Destination(RemoveMicroSDWarningView, clear_history=True)
+
+ elif button_data[selected_menu_num] == self.SETTINGS:
+ from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
+ return Destination(
+ SettingsEntryUpdateSelectionView,
+ view_args=dict(
+ attr_name=SettingsConstants.SETTING__MICROSD_TOAST_TIMER,
+ blocking_view=RemoveMicroSDWarningView,
+ unblocking_view=MainMenuView
+ )
+ )
\ No newline at end of file
diff --git a/tests/test_flows.py b/tests/test_flows.py
index 2faafcf..e520de4 100644
--- a/tests/test_flows.py
+++ b/tests/test_flows.py
@@ -9,8 +9,12 @@ from seedsigner.models.seed import Seed
from seedsigner.views import scan_views
from seedsigner.views.psbt_views import PSBTSelectSeedView
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedsMenuView
-from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, View
+from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, RemoveMicroSDWarningView, MainMenuView, View
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
+from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
+from seedsigner.models.settings_definition import SettingsDefinition
+from seedsigner.models.settings import SettingsConstants
+from seedsigner.hardware.microsd import MicroSD
@@ -192,3 +196,47 @@ class TestFlowTest(FlowTest):
FlowStep(MainMenuView), # Need a next Destination to force the first step to run
])
+ def test_remove_microsd_blocking(self):
+ """
+ Verifies three related behaviors:
+
+ 1) If the RemoveMicroSDWarningView launches the SettingsEntryUpdateSelectionView
+ and the user presses Back without changing the tracked setting, the flow
+ returns to RemoveMicroSDWarningView (the blocking condition remains).
+ 2) If the user changes the tracked setting while in the settings entry, the
+ flow unblocks and navigates to MainMenuView.
+ 3) If the MicroSD is physically removed and the user presses Continue on the
+ warning, the flow proceeds to MainMenuView.
+ """
+ controller = Controller.get_instance()
+
+ settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__MICROSD_TOAST_TIMER)
+ controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FOREVER)
+
+ # There are only two ways of exiting RemoveMicroSDWarningView when SETTING__MICROSD_TOAST_TIMER -> MICROSD_TOAST_TIMER_FOREVER
+ self.run_sequence([
+ FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.SETTINGS),
+ FlowStep(SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
+ FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.SETTINGS),
+ # 1) Modifying the setting
+ FlowStep(SettingsEntryUpdateSelectionView, screen_return_value=0),
+ FlowStep(MainMenuView)
+ ])
+
+ self.reset_controller()
+ controller = Controller.get_instance()
+
+ settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__MICROSD_TOAST_TIMER)
+ controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FOREVER)
+
+ # 2) Removing the MicroSD card and pressing CONTINUE
+ self.mock_microsd.is_inserted = False
+ assert MicroSD.get_instance().is_inserted is False
+
+ self.run_sequence([
+ FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.CONTINUE),
+ FlowStep(MainMenuView)
+ ])
+
+ # Restore the setting for the controller
+ controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS)
\ No newline at end of file
Why this scored 37/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.