From 5bd6996ae462a352978214a22de48aea226d50bf Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sun, 2 Jun 2024 12:47:24 +0200 Subject: [PATCH] feat(core/ui): show success screens between shares This commit also adds minor improvements to reset flow. [no changelog] --- .../flow/confirm_reset_create.rs | 4 +- .../ui/model_mercury/flow/show_share_words.rs | 3 +- .../embed/rust/src/ui/model_mercury/layout.rs | 9 ++- core/mocks/generated/trezorui2.pyi | 2 +- .../apps/management/reset_device/layout.py | 55 ++------------ .../src/trezor/ui/layouts/mercury/__init__.py | 8 ++- .../src/trezor/ui/layouts/mercury/recovery.py | 2 +- core/src/trezor/ui/layouts/mercury/reset.py | 72 +++++++++++++++---- core/src/trezor/ui/layouts/tr/reset.py | 46 +++++++++++- core/src/trezor/ui/layouts/tt/reset.py | 48 ++++++++++++- core/translations/signatures.json | 6 +- python/src/trezorlib/debuglink.py | 23 +++--- tests/common.py | 18 +++-- tests/input_flows.py | 8 ++- tests/input_flows_helpers.py | 6 +- 15 files changed, 210 insertions(+), 100 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs index c4df045075..e02ba7c28e 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs @@ -97,9 +97,11 @@ impl ConfirmResetCreate { .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)) .one_button_request(ButtonRequestCode::ResetDevice.with_type("setup_device")); + // FIXME: TR::reset__cancel_create_wallet should be used but Button text on + // multiple lines not supported yet let content_menu = Frame::left_aligned( "".into(), - VerticalMenu::empty().danger(theme::ICON_CANCEL, "Cancel".into()), // TODO: use TR + VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::buttons__cancel.into()), ) .with_cancel_button() .with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) diff --git a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs index 0b4f74b11c..e016aec4e7 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs @@ -112,13 +112,12 @@ impl ShowShareWords { .map(|_| Some(FlowMsg::Confirmed)); let content_check_backup_intro = Frame::left_aligned( - TR::reset__check_backup_title.into(), + TR::reset__check_wallet_backup_title.into(), SwipeContent::new(Paragraphs::new(Paragraph::new( &theme::TEXT_MAIN_GREY_LIGHT, TR::reset__check_backup_instructions, ))), ) - .with_subtitle(TR::words__instructions.into()) .with_footer(TR::instructions__swipe_up.into(), None) .with_swipe(SwipeDirection::Up, SwipeSettings::default()) .map(|_| Some(FlowMsg::Confirmed)); diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 2b821713bf..fb1581e3fe 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -788,10 +788,15 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let description: Option = kwargs + .get(Qstr::MP_QSTR_description)? + .try_into_option()? + .and_then(|desc: TString| if desc.is_empty() { None } else { Some(desc) }); + let content = StatusScreen::new_success(); let obj = LayoutObj::new(SwipeUpScreen::new( Frame::left_aligned(title, SwipeContent::new(content).with_normal_attach(None)) - .with_footer(TR::instructions__swipe_up.into(), None) + .with_footer(TR::instructions__swipe_up.into(), description) .with_swipe(SwipeDirection::Up, SwipeSettings::default()), ))?; Ok(obj.into()) @@ -1572,7 +1577,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// allow_cancel: bool = False, /// time_ms: int = 0, /// ) -> LayoutObj[UiResult]: - /// """Success modal. No buttons shown when `button` is empty string.""" + /// """Success screen. Description is used in the footer.""" Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), /// def show_info( diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index e680d2a87a..caf2c398ef 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -278,7 +278,7 @@ def show_success( allow_cancel: bool = False, time_ms: int = 0, ) -> LayoutObj[UiResult]: - """Success modal. No buttons shown when `button` is empty string.""" + """Success screen. Description is used in the footer.""" # rust/src/ui/model_mercury/layout.rs diff --git a/core/src/apps/management/reset_device/layout.py b/core/src/apps/management/reset_device/layout.py index 41c2e3051e..f2958290be 100644 --- a/core/src/apps/management/reset_device/layout.py +++ b/core/src/apps/management/reset_device/layout.py @@ -69,17 +69,20 @@ async def _share_words_confirmed( Return true if the words are confirmed successfully. """ - # TODO: confirm_action("Select the words bla bla") + from trezor.ui.layouts.reset import ( + show_share_confirmation_success, + show_share_confirmation_failure, + ) if await _do_confirm_share_words(share_index, share_words, group_index): - await _show_confirmation_success( + await show_share_confirmation_success( share_index, num_of_shares, group_index, ) return True else: - await _show_confirmation_failure() + await show_share_confirmation_failure() return False @@ -105,52 +108,6 @@ async def _do_confirm_share_words( return True -async def _show_confirmation_success( - share_index: int | None = None, - num_of_shares: int | None = None, - group_index: int | None = None, -) -> None: - if share_index is None or num_of_shares is None: - # it is a BIP39 or a 1-of-1 SLIP39 backup - subheader = TR.reset__finished_verifying_wallet_backup - text = "" - - elif share_index == num_of_shares - 1: - if group_index is None: - subheader = TR.reset__finished_verifying_shares - else: - subheader = TR.reset__finished_verifying_group_template.format( - group_index + 1 - ) - text = "" - - else: - if group_index is None: - subheader = TR.reset__share_checked_successfully_template.format( - share_index + 1 - ) - text = TR.reset__continue_with_share_template.format(share_index + 2) - else: - subheader = TR.reset__group_share_checked_successfully_template.format( - group_index + 1, share_index + 1 - ) - text = TR.reset__continue_with_next_share - - return await show_success("success_recovery", text, subheader) - - -async def _show_confirmation_failure() -> None: - from trezor.ui.layouts.reset import show_reset_warning - - await show_reset_warning( - "warning_backup_check", - TR.words__please_check_again, - TR.reset__wrong_word_selected, - TR.buttons__check_again, - ButtonRequestType.ResetDevice, - ) - - async def show_backup_warning() -> None: from trezor.ui.layouts.reset import show_warning_backup diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index ad8f5c6c8f..3d85acff81 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -369,7 +369,11 @@ async def confirm_reset_device(_title: str, recovery: bool = False) -> None: async def prompt_backup() -> bool: # TODO: should we move this to `flow_prompt_backup`? await interact( - RustLayout(trezorui2.show_success(title=TR.backup__new_wallet_created)), + RustLayout( + trezorui2.show_success( + title=TR.backup__new_wallet_created, description=None + ) + ), "backup_device", ButtonRequestType.ResetDevice, ) @@ -562,7 +566,7 @@ async def show_success( RustLayout( trezorui2.show_success( title=content, - description="", + description=subheader, ) ), br_type, diff --git a/core/src/trezor/ui/layouts/mercury/recovery.py b/core/src/trezor/ui/layouts/mercury/recovery.py index 89d411b7b8..e7ab841ba0 100644 --- a/core/src/trezor/ui/layouts/mercury/recovery.py +++ b/core/src/trezor/ui/layouts/mercury/recovery.py @@ -123,7 +123,7 @@ async def continue_recovery( if show_info: # Show this just one-time - description = TR.recovery__only_first_n_letters + description = TR.recovery__enter_each_word else: description = subtext or "" diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 661bcc1eb5..dcfd456881 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType from trezor.wire import ActionCancelled from ..common import interact -from . import RustLayout, raise_if_not_confirmed +from . import RustLayout, raise_if_not_confirmed, show_success if TYPE_CHECKING: pass @@ -57,11 +57,11 @@ async def select_word( group_index: int | None = None, ) -> str: if share_index is None: - description: str = TR.reset__check_wallet_backup_title + title: str = TR.reset__check_wallet_backup_title elif group_index is None: - description: str = TR.reset__check_share_title_template.format(share_index + 1) + title: str = TR.reset__check_share_title_template.format(share_index + 1) else: - description: str = TR.reset__check_group_share_title_template.format( + title: str = TR.reset__check_group_share_title_template.format( group_index + 1, share_index + 1 ) @@ -74,10 +74,10 @@ async def select_word( result = await RustLayout( trezorui2.select_word( - title=TR.reset__select_word_x_of_y_template.format( + title=title, + description=TR.reset__select_word_x_of_y_template.format( checked_index + 1, count ), - description=description, words=(words[0], words[1], words[2]), ) ) @@ -316,12 +316,9 @@ async def show_warning_backup() -> None: async def show_success_backup() -> None: - from . import show_success - await show_success( "success_backup", - TR.reset__use_your_backup, - TR.reset__your_backup_is_done, + TR.backup__title_backup_completed, ) @@ -332,15 +329,14 @@ async def show_reset_warning( button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, ) -> None: - button = button or TR.buttons__try_again # def_arg await raise_if_not_confirmed( interact( RustLayout( trezorui2.show_warning( - title=content or TR.words__warning, - description="", - value=subheader or "", - button=button, + title=subheader or "", + description=content, + value="", + button="", allow_cancel=False, ) ), @@ -348,3 +344,49 @@ async def show_reset_warning( br_code, ) ) + + +async def show_share_confirmation_success( + share_index: int | None = None, + num_of_shares: int | None = None, + group_index: int | None = None, +) -> None: + if share_index is None or num_of_shares is None: + # it is a BIP39 or a 1-of-1 SLIP39 backup + # mercury UI shows only final wallet backup confirmation screen later + return + + # TODO: super-shamir copy not done + if share_index == num_of_shares - 1: + title = TR.reset__share_completed_template.format(share_index + 1) + if group_index is None: + footer_description = "" + else: + footer_description = TR.reset__finished_verifying_group_template.format( + group_index + 1 + ) + else: + if group_index is None: + title = TR.reset__share_completed_template.format(share_index + 1) + footer_description = ( + TR.instructions__shares_continue_with_x_template.format(share_index + 2) + ) + else: + title = TR.reset__continue_with_next_share + footer_description = ( + TR.reset__group_share_checked_successfully_template.format( + group_index + 1, share_index + 1 + ) + ) + + return await show_success("success_recovery", title, subheader=footer_description) + + +async def show_share_confirmation_failure() -> None: + await show_reset_warning( + "warning_backup_check", + TR.words__try_again, + TR.reset__incorrect_word_selected, + "", + ButtonRequestType.ResetDevice, + ) diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index 6dcc593195..2d17232430 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType from trezor.wire import ActionCancelled from ..common import interact -from . import RustLayout, confirm_action, show_warning +from . import RustLayout, confirm_action, show_warning, show_success CONFIRMED = trezorui2.CONFIRMED # global_import_cache @@ -291,3 +291,47 @@ async def show_reset_warning( button, br_code=br_code, ) + + +async def show_share_confirmation_success( + share_index: int | None = None, + num_of_shares: int | None = None, + group_index: int | None = None, +) -> None: + if share_index is None or num_of_shares is None: + # it is a BIP39 or a 1-of-1 SLIP39 backup + subheader = TR.reset__finished_verifying_wallet_backup + text = "" + + elif share_index == num_of_shares - 1: + if group_index is None: + subheader = TR.reset__finished_verifying_shares + else: + subheader = TR.reset__finished_verifying_group_template.format( + group_index + 1 + ) + text = "" + + else: + if group_index is None: + subheader = TR.reset__share_checked_successfully_template.format( + share_index + 1 + ) + text = TR.reset__continue_with_share_template.format(share_index + 2) + else: + subheader = TR.reset__group_share_checked_successfully_template.format( + group_index + 1, share_index + 1 + ) + text = TR.reset__continue_with_next_share + + return await show_success("success_recovery", text, subheader) + + +async def show_share_confirmation_failure() -> None: + await show_reset_warning( + "warning_backup_check", + TR.words__please_check_again, + TR.reset__wrong_word_selected, + TR.buttons__check_again, + ButtonRequestType.ResetDevice, + ) diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index bdb88043dc..53c51bbc4f 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType from trezor.wire import ActionCancelled from ..common import interact -from . import RustLayout, raise_if_not_confirmed +from . import RustLayout, raise_if_not_confirmed, show_success CONFIRMED = trezorui2.CONFIRMED # global_import_cache @@ -326,8 +326,6 @@ async def show_warning_backup() -> None: async def show_success_backup() -> None: - from . import show_success - await show_success( "success_backup", TR.reset__use_your_backup, @@ -357,3 +355,47 @@ async def show_reset_warning( br_code, ) ) + + +async def show_share_confirmation_success( + share_index: int | None = None, + num_of_shares: int | None = None, + group_index: int | None = None, +) -> None: + if share_index is None or num_of_shares is None: + # it is a BIP39 or a 1-of-1 SLIP39 backup + subheader = TR.reset__finished_verifying_wallet_backup + text = "" + + elif share_index == num_of_shares - 1: + if group_index is None: + subheader = TR.reset__finished_verifying_shares + else: + subheader = TR.reset__finished_verifying_group_template.format( + group_index + 1 + ) + text = "" + + else: + if group_index is None: + subheader = TR.reset__share_checked_successfully_template.format( + share_index + 1 + ) + text = TR.reset__continue_with_share_template.format(share_index + 2) + else: + subheader = TR.reset__group_share_checked_successfully_template.format( + group_index + 1, share_index + 1 + ) + text = TR.reset__continue_with_next_share + + return await show_success("success_recovery", text, subheader) + + +async def show_share_confirmation_failure() -> None: + await show_reset_warning( + "warning_backup_check", + TR.words__please_check_again, + TR.reset__wrong_word_selected, + TR.buttons__check_again, + ButtonRequestType.ResetDevice, + ) diff --git a/core/translations/signatures.json b/core/translations/signatures.json index cc45d0657e..32d271486a 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "57561824d2bed19054d5570b0181b5907c7ae5546e4d6c2e589a727898a69237", - "datetime": "2024-05-31T09:09:13.124759", - "commit": "6000eaf8280116d0cc947aa8d2aceb568dca56f8" + "merkle_root": "7a115e582a5f5f09b1850946030762360f55e516a60cf960dffc9ba174c2e4d2", + "datetime": "2024-06-02T11:07:12.183601", + "commit": "66496206ccbe9583203fbfebf8c9222e8c6379b8" }, "history": [ { diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index f1fa328b8c..a0d23bc8f6 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -171,27 +171,32 @@ class LayoutContent(UnstructuredJSONReader): return visible + def _get_str_or_dict_text(self, key: str) -> str: + value = self.find_unique_value_by_key(key, "") + if isinstance(value, dict): + return value["text"] + return value + def title(self) -> str: - """Getting text that is displayed as a title.""" + """Getting text that is displayed as a title and potentially subtitle.""" # There could be possibly subtitle as well title_parts: List[str] = [] - def _get_str_or_dict_text(key: str) -> str: - value = self.find_unique_value_by_key(key, "") - if isinstance(value, dict): - return value["text"] - return value - - title = _get_str_or_dict_text("title") + title = self._get_str_or_dict_text("title") if title: title_parts.append(title) - subtitle = _get_str_or_dict_text("subtitle") + subtitle = self.subtitle() if subtitle: title_parts.append(subtitle) return "\n".join(title_parts) + def subtitle(self) -> str: + """Getting text that is displayed as a subtitle.""" + subtitle = self._get_str_or_dict_text("subtitle") + return subtitle + def text_content(self) -> str: """What is on the screen, in one long string, so content can be asserted regardless of newlines. Also getting rid of possible ellipsis. diff --git a/tests/common.py b/tests/common.py index 5011d91172..6502faf591 100644 --- a/tests/common.py +++ b/tests/common.py @@ -277,13 +277,21 @@ def check_share( Given the mnemonic word list, proceed with the backup check: three rounds of `Select word X of Y` choices. """ + re_num_of_word = r"\d+" for _ in range(3): - if debug.model is models.T2B1: - # T2B1 has the instruction in the title - word_pos_match = re.search(r"\d+", debug.wait_layout().title()) + if debug.model is models.T2T1: + # T2T1 has position as the first number in the text + word_pos_match = re.search( + re_num_of_word, debug.wait_layout().text_content() + ) + elif debug.model is models.T2B1: + # other models have the instruction in the title/subtitle + word_pos_match = re.search(re_num_of_word, debug.wait_layout().title()) + elif debug.model is models.T3T1: + word_pos_match = re.search(re_num_of_word, debug.wait_layout().subtitle()) else: - # Other models has position as the first number in the text - word_pos_match = re.search(r"\d+", debug.wait_layout().text_content()) + word_pos_match = None + assert word_pos_match is not None word_pos = int(word_pos_match.group(0)) diff --git a/tests/input_flows.py b/tests/input_flows.py index 5d630658ba..aba5d4d08b 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -1255,9 +1255,11 @@ def get_mnemonic_and_confirm_success( # mnemonic phrases mnemonic = yield from read_and_confirm_mnemonic(debug) - br = yield # confirm recovery seed check - assert br.code == B.Success - debug.press_yes() + is_slip39 = len(mnemonic.split()) in (20, 33) + if debug.model in (models.T2T1, models.T2B1) or is_slip39: + br = yield # confirm recovery share check + assert br.code == B.Success + debug.press_yes() br = yield # confirm success assert br.code == B.Success diff --git a/tests/input_flows_helpers.py b/tests/input_flows_helpers.py index 343b363f61..5135549e0b 100644 --- a/tests/input_flows_helpers.py +++ b/tests/input_flows_helpers.py @@ -93,7 +93,7 @@ class RecoveryFlow: def enter_your_backup(self) -> BRGeneratorType: yield if self.debug.model is models.T3T1: - TR.assert_in(self._text_content(), "recovery__only_first_n_letters") + TR.assert_in(self._text_content(), "recovery__enter_each_word") else: TR.assert_in(self._text_content(), "recovery__enter_backup") is_dry_run = any( @@ -110,7 +110,7 @@ class RecoveryFlow: yield TR.assert_in_multiple( self._text_content(), - ["recovery__enter_any_share", "recovery__only_first_n_letters"], + ["recovery__enter_any_share", "recovery__enter_each_word"], ) is_dry_run = any( title in self.debug.wait_layout().title().lower() @@ -127,7 +127,7 @@ class RecoveryFlow: if self.client.model is models.T2B1: TR.assert_in(self._text_content(), "recovery__num_of_words") elif self.client.model is models.T3T1: - TR.assert_in(self._text_content(), "recovery__only_first_n_letters") + TR.assert_in(self._text_content(), "recovery__enter_each_word") else: TR.assert_in(self._text_content(), "recovery__enter_any_share") self.debug.press_no()