diff --git a/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs b/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs index fbce08f018..ba2b43bb9b 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs @@ -97,13 +97,9 @@ pub extern "C" fn new_prompt_backup(n_args: usize, args: *const Obj, kwargs: *mu impl PromptBackup { fn new_obj(_args: &[Obj], _kwargs: &Map) -> Result { let title: TString = TR::backup__title_create_wallet_backup.into(); - let par_array: [Paragraph<'static>; 1] = [Paragraph::new( - &theme::TEXT_MAIN_GREY_LIGHT, - // FIXME: should be "contains X words" but the mnemonic/shares are not yet generated at - // this point. We might need to merge the PromptBackup and ShowShareWords flows - TString::from_str("Your wallet backup contains words in a specific order."), - )]; - let paragraphs = Paragraphs::new(par_array); + let text_intro: TString = TR::backup__it_should_be_backed_up.into(); + + let paragraphs = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, text_intro)); let content_intro = Frame::left_aligned(title, SwipeContent::new(paragraphs)) .with_menu_button() .with_footer(TR::instructions__swipe_up.into(), None) diff --git a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs index df940c9d81..55ffad8a20 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs @@ -52,7 +52,6 @@ impl FlowState for RequestNumber { (RequestNumber::Menu, FlowMsg::Choice(0)) => { Decision::Goto(RequestNumber::Info, SwipeDirection::Left) } - (RequestNumber::Menu, FlowMsg::Choice(1)) => Decision::Return(FlowMsg::Cancelled), (RequestNumber::Menu, FlowMsg::Cancelled) => { Decision::Goto(RequestNumber::Number, SwipeDirection::Right) } @@ -119,9 +118,7 @@ impl RequestNumber { let content_menu = Frame::left_aligned( "".into(), - VerticalMenu::empty() - .item(theme::ICON_CHEVRON_RIGHT, TR::buttons__more_info.into()) - .danger(theme::ICON_CANCEL, TR::backup__title_skip.into()), + VerticalMenu::empty().item(theme::ICON_CHEVRON_RIGHT, TR::buttons__more_info.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 96e0bd3174..37337020f2 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 @@ -79,6 +79,10 @@ impl ShowShareWords { let subtitle: TString = kwargs.get(Qstr::MP_QSTR_subtitle)?.try_into()?; let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let share_words_vec: Vec = util::iter_into_vec(share_words_obj)?; + 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 text_info: Obj = kwargs.get(Qstr::MP_QSTR_text_info)?; let text_confirm: TString = kwargs.get(Qstr::MP_QSTR_text_confirm)?.try_into()?; let nwords = share_words_vec.len(); @@ -98,7 +102,7 @@ impl ShowShareWords { ), ) .with_subtitle(TR::words__instructions.into()) - .with_footer(TR::instructions__swipe_up.into(), None) + .with_footer(TR::instructions__swipe_up.into(), description) .with_swipe(SwipeDirection::Up, SwipeSettings::default()) .map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed)) .one_button_request(ButtonRequestCode::ResetDevice.with_type("share_words")) diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 48a22a6565..7ea0b5d2a0 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -1688,6 +1688,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// subtitle: str, /// words: Iterable[str], + /// description: str, /// text_info: Iterable[str], /// text_confirm: str, /// ) -> LayoutObj[UiResult]: diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 8318286ab4..8a50f68eaf 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -402,6 +402,7 @@ def flow_show_share_words( title: str, subtitle: str, words: Iterable[str], + description: str, text_info: Iterable[str], text_confirm: str, ) -> LayoutObj[UiResult]: diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 4cf51a3db0..94974dae24 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -126,13 +126,21 @@ async def reset_device(msg: ResetDevice) -> Success: return Success(message="Initialized") +async def _backup_bip39(mnemonic: str) -> None: + words = mnemonic.split() + await layout.show_backup_intro(single_share=True, num_of_words=len(words)) + await layout.show_and_confirm_single_share(words) + + async def _backup_slip39_single( encrypted_master_secret: bytes, extendable: bool ) -> None: mnemonics = _get_slip39_mnemonics(encrypted_master_secret, 1, ((1, 1),), extendable) + words = mnemonics[0][0].split() # for a single 1-of-1 group, we use the same layouts as for BIP39 - await layout.show_and_confirm_mnemonic(mnemonics[0][0]) + await layout.show_backup_intro(single_share=True, num_of_words=len(words)) + await layout.show_and_confirm_single_share(words) async def _backup_slip39_basic( @@ -140,6 +148,8 @@ async def _backup_slip39_basic( ) -> None: group_threshold = 1 + await layout.show_backup_intro(single_share=False) + # get number of shares await layout.slip39_show_checklist(0, advanced=False) share_count = await layout.slip39_prompt_number_of_shares() @@ -165,6 +175,9 @@ async def _backup_slip39_basic( async def _backup_slip39_advanced( encrypted_master_secret: bytes, extendable: bool ) -> None: + + await layout.show_backup_intro(single_share=False) + # get number of groups await layout.slip39_show_checklist(0, advanced=True) groups_count = await layout.slip39_advanced_prompt_number_of_groups() @@ -287,4 +300,4 @@ async def backup_seed(backup_type: BackupType, mnemonic_secret: bytes) -> None: else: await _backup_slip39_basic(mnemonic_secret, extendable) else: - await layout.show_and_confirm_mnemonic(mnemonic_secret.decode()) + await _backup_bip39(mnemonic_secret.decode()) diff --git a/core/src/apps/management/reset_device/layout.py b/core/src/apps/management/reset_device/layout.py index f2958290be..8376d038e3 100644 --- a/core/src/apps/management/reset_device/layout.py +++ b/core/src/apps/management/reset_device/layout.py @@ -3,7 +3,6 @@ from typing import Sequence from trezor import TR from trezor.enums import ButtonRequestType -from trezor.ui.layouts import show_success from trezor.ui.layouts.reset import ( # noqa: F401 show_share_words, slip39_advanced_prompt_group_threshold, @@ -70,8 +69,8 @@ async def _share_words_confirmed( Return true if the words are confirmed successfully. """ from trezor.ui.layouts.reset import ( - show_share_confirmation_success, show_share_confirmation_failure, + show_share_confirmation_success, ) if await _do_confirm_share_words(share_index, share_words, group_index): @@ -108,6 +107,14 @@ async def _do_confirm_share_words( return True +async def show_backup_intro( + single_share: bool, num_of_words: int | None = None +) -> None: + from trezor.ui.layouts.reset import show_intro_backup + + await show_intro_backup(single_share, num_of_words) + + async def show_backup_warning() -> None: from trezor.ui.layouts.reset import show_warning_backup @@ -124,12 +131,10 @@ async def show_backup_success() -> None: # === -async def show_and_confirm_mnemonic(mnemonic: str) -> None: +async def show_and_confirm_single_share(words: Sequence[str]) -> None: # warn user about mnemonic safety await show_backup_warning() - words = mnemonic.split() - while True: # display paginated mnemonic on the screen await show_share_words(words) diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index 5c057ae5e7..e74cc2deb4 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -382,7 +382,6 @@ async def prompt_backup() -> bool: "backup_device", ButtonRequestType.ResetDevice, ) - return result is CONFIRMED diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index ef5de17cdb..e3cc9b0ae2 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -32,12 +32,14 @@ async def show_share_words( group_index + 1, share_index + 1 ) words_count = len(share_words) + description = "" text_info = [TR.reset__write_down_words_template.format(words_count)] if words_count == 20 and share_index is None: # 1-of-1 SLIP39: inform the user about repeated words text_info.append(TR.reset__words_may_repeat) if share_index == 0: # regular SLIP39, 1st share + description = TR.instructions__shares_start_with_1 text_info.append(TR.reset__repeat_for_all_shares) text_confirm = TR.reset__words_written_down_template.format(words_count) @@ -46,6 +48,7 @@ async def show_share_words( title=title, subtitle=subtitle, words=share_words, + description=description, text_info=text_info, text_confirm=text_confirm, ) @@ -304,6 +307,24 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) +async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: + if single_share: + assert num_of_words is not None + description = TR.backup__info_single_share_backup.format(num_of_words) + else: + description = TR.backup__info_multi_share_backup + + await interact( + RustLayout( + trezorui2.show_info( + title=TR.backup__title_create_wallet_backup, description=description + ) + ), + "backup_warning", + ButtonRequestType.ResetDevice, + ) + + async def show_warning_backup() -> None: result = await interact( RustLayout( diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index 2d17232430..13ca822af7 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, show_success +from . import RustLayout, confirm_action, show_success, show_warning CONFIRMED = trezorui2.CONFIRMED # global_import_cache @@ -254,6 +254,23 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) +async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: + if single_share: + assert num_of_words is not None + description = TR.backup__info_single_share_backup.format(num_of_words) + else: + description = TR.backup__info_multi_share_backup + + await confirm_action( + "backup_warning", + title=TR.backup__title_backup_wallet, + verb=TR.buttons__continue, + description=description, + verb_cancel=None, + br_code=ButtonRequestType.ResetDevice, + ) + + async def show_warning_backup() -> None: await show_warning( "backup_warning", diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index 53c51bbc4f..5794c27792 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -309,6 +309,27 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) +async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: + if single_share: + assert num_of_words is not None + description = TR.backup__info_single_share_backup.format(num_of_words) + else: + description = TR.backup__info_multi_share_backup + + await interact( + RustLayout( + trezorui2.show_info( + title="", + button=TR.buttons__continue, + description=description, + allow_cancel=False, + ) + ), + "backup_warning", + ButtonRequestType.ResetDevice, + ) + + async def show_warning_backup() -> None: result = await interact( RustLayout( diff --git a/tests/input_flows.py b/tests/input_flows.py index f2b7babb18..56a7e201bf 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -1310,7 +1310,7 @@ class InputFlowBip39ResetBackup(InputFlowBase): # 1. Confirm Reset x3 # 2. Backup your seed # 3. Confirm warning - yield from click_through(self.debug, screens=4, code=B.ResetDevice) + yield from click_through(self.debug, screens=5, code=B.ResetDevice) # mnemonic phrases and rest self.mnemonic = yield from get_mnemonic_and_confirm_success(self.debug) diff --git a/tests/persistence_tests/test_shamir_persistence.py b/tests/persistence_tests/test_shamir_persistence.py index aa958d49e7..bfe3f5b4f5 100644 --- a/tests/persistence_tests/test_shamir_persistence.py +++ b/tests/persistence_tests/test_shamir_persistence.py @@ -138,7 +138,7 @@ def test_recovery_on_old_wallet(core_emulator: Emulator): words = first_share.split(" ") # start entering first share - assert "the first 2-4 letters" in debug.read_layout().text_content() + assert "Enter each word of your wallet backup" in debug.read_layout().text_content() debug.press_yes() assert debug.wait_layout().main_component() == "MnemonicKeyboard" @@ -176,7 +176,7 @@ def test_recovery_multiple_resets(core_emulator: Emulator): def enter_shares_with_restarts(debug: DebugLink) -> None: shares = MNEMONIC_SLIP39_ADVANCED_20 layout = debug.read_layout() - expected_text = "the first 2-4 letters" + expected_text = "Enter each word of your wallet backup" remaining = len(shares) for share in shares: assert expected_text in layout.text_content()