From 663ada81233abf0046b45fa43dc16fce41b95d40 Mon Sep 17 00:00:00 2001 From: alvroble <50918598+alvroble@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:21:19 +0200 Subject: [PATCH] Option MICROSD_TOAST_TIMER_FOREVER refactor - Prevent escaping the RemoveMicroSDWarningView by launching SettingsEntryUpdateSelectionView in a blocking mode. If the tracked setting remains unchanged, Back/normal navigation returns to the blocking view (RemoveMicroSDWarningView). - Only 2 ways of escaping this mode: 1) physically removing MicroSD and 2) modifying the setting - Add test tests/test_flows.py::TestFlowTest::test_remove_microsd_blocking covering: --- src/seedsigner/views/settings_views.py | 23 +++++++++--- src/seedsigner/views/view.py | 26 ++++++++++---- tests/test_flows.py | 50 +++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index b6fa7899..cbf66e08 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 d7e0c26a..24d7de8e 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 2faafcfa..e520de48 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