diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index fc78c19a9e..8e3c111cf0 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -532,6 +532,7 @@ static void _librust_qstrs(void) { MP_QSTR_reset__set_it_to_count_template; MP_QSTR_reset__share_checked_successfully_template; MP_QSTR_reset__share_completed_template; + MP_QSTR_reset__share_words_first; MP_QSTR_reset__share_words_title; MP_QSTR_reset__slip39_checklist_more_info_threshold; MP_QSTR_reset__slip39_checklist_more_info_threshold_example_template; @@ -651,7 +652,7 @@ static void _librust_qstrs(void) { MP_QSTR_show_progress_coinjoin; MP_QSTR_show_remaining_shares; MP_QSTR_show_share_words; - MP_QSTR_show_share_words_delizia; + MP_QSTR_show_share_words_extended; MP_QSTR_show_simple; MP_QSTR_show_success; MP_QSTR_show_wait_text; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index f8182f3f5f..0605435da5 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -855,7 +855,7 @@ pub enum TranslatedString { reset__recovery_share_title_template = 567, // "Recovery share #{0}" reset__required_number_of_groups = 568, // "The required number of groups for recovery." reset__select_correct_word = 569, // "Select the correct word for each position." - reset__select_word_template = 570, // "Select {0} word" + reset__select_word_template = 570, // {"Bolt": "Select {0} word", "Caesar": "Select {0} word", "Delizia": "Select {0} word", "Eckhart": "Select word #{0} from your wallet backup"} reset__select_word_x_of_y_template = 571, // "Select word {0} of {1}:" reset__set_it_to_count_template = 572, // "Set it to {0} and you will need " reset__share_checked_successfully_template = 573, // "Share #{0} checked successfully." @@ -1339,7 +1339,7 @@ pub enum TranslatedString { reset__repeat_for_all_shares = 938, // "Repeat for all shares." homescreen__settings_subtitle = 939, // "Settings" homescreen__settings_title = 940, // "Homescreen" - reset__the_word_is_repeated = 941, // "The word is repeated" + reset__the_word_is_repeated = 941, // {"Bolt": "The word is repeated", "Caesar": "The word is repeated", "Delizia": "The word is repeated", "Eckhart": "The word appears multiple times in the backup."} tutorial__title_lets_begin = 942, // "Let's begin" tutorial__did_you_know = 943, // "Did you know?" tutorial__first_wallet = 944, // "The Trezor Model One, created in 2013,\nwas the world's first hardware wallet." @@ -1383,6 +1383,7 @@ pub enum TranslatedString { #[cfg(feature = "universal_fw")] ethereum__unknown_contract_address_short = 974, // "Unknown contract address." instructions__keep_holding = 975, // "Keep holding" + reset__share_words_first = 976, // "Write down the first word from the backup." } impl TranslatedString { @@ -2234,7 +2235,14 @@ impl TranslatedString { Self::reset__recovery_share_title_template => "Recovery share #{0}", Self::reset__required_number_of_groups => "The required number of groups for recovery.", Self::reset__select_correct_word => "Select the correct word for each position.", + #[cfg(feature = "layout_bolt")] Self::reset__select_word_template => "Select {0} word", + #[cfg(feature = "layout_caesar")] + Self::reset__select_word_template => "Select {0} word", + #[cfg(feature = "layout_delizia")] + Self::reset__select_word_template => "Select {0} word", + #[cfg(feature = "layout_eckhart")] + Self::reset__select_word_template => "Select word #{0} from your wallet backup", Self::reset__select_word_x_of_y_template => "Select word {0} of {1}:", Self::reset__set_it_to_count_template => "Set it to {0} and you will need ", Self::reset__share_checked_successfully_template => "Share #{0} checked successfully.", @@ -2718,7 +2726,14 @@ impl TranslatedString { Self::reset__repeat_for_all_shares => "Repeat for all shares.", Self::homescreen__settings_subtitle => "Settings", Self::homescreen__settings_title => "Homescreen", + #[cfg(feature = "layout_bolt")] Self::reset__the_word_is_repeated => "The word is repeated", + #[cfg(feature = "layout_caesar")] + Self::reset__the_word_is_repeated => "The word is repeated", + #[cfg(feature = "layout_delizia")] + Self::reset__the_word_is_repeated => "The word is repeated", + #[cfg(feature = "layout_eckhart")] + Self::reset__the_word_is_repeated => "The word appears multiple times in the backup.", Self::tutorial__title_lets_begin => "Let's begin", Self::tutorial__did_you_know => "Did you know?", Self::tutorial__first_wallet => "The Trezor Model One, created in 2013,\nwas the world's first hardware wallet.", @@ -2762,6 +2777,7 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Self::ethereum__unknown_contract_address_short => "Unknown contract address.", Self::instructions__keep_holding => "Keep holding", + Self::reset__share_words_first => "Write down the first word from the backup.", } } @@ -4140,6 +4156,7 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Qstr::MP_QSTR_ethereum__unknown_contract_address_short => Some(Self::ethereum__unknown_contract_address_short), Qstr::MP_QSTR_instructions__keep_holding => Some(Self::instructions__keep_holding), + Qstr::MP_QSTR_reset__share_words_first => Some(Self::reset__share_words_first), _ => None, } } diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs index e280a43a58..c20ede2ac2 100644 --- a/core/embed/rust/src/ui/api/firmware_micropython.rs +++ b/core/embed/rust/src/ui/api/firmware_micropython.rs @@ -878,7 +878,7 @@ extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_show_share_words_delizia( +extern "C" fn new_show_share_words_extended( n_args: usize, args: *const Obj, kwargs: *mut Map, @@ -898,7 +898,7 @@ extern "C" fn new_show_share_words_delizia( let words: Vec = util::iter_into_vec(words)?; - let layout = ModelUI::show_share_words_delizia( + let layout = ModelUI::show_share_words_extended( words, subtitle, instructions, @@ -1589,7 +1589,7 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// """Show mnemonic for backup.""" Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), - /// def show_share_words_delizia( + /// def show_share_words_extended( /// *, /// words: Iterable[str], /// subtitle: str | None, @@ -1599,7 +1599,7 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// ) -> LayoutObj[UiResult]: /// """Show mnemonic for wallet backup preceded by an instruction screen and followed by a /// confirmation screen.""" - Qstr::MP_QSTR_show_share_words_delizia => obj_fn_kw!(0, new_show_share_words_delizia).as_obj(), + Qstr::MP_QSTR_show_share_words_extended => obj_fn_kw!(0, new_show_share_words_extended).as_obj(), /// def show_simple( /// *, diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs index be68f5d192..8ab1850100 100644 --- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs @@ -1013,7 +1013,7 @@ impl FirmwareUI for UIBolt { Ok(layout) } - fn show_share_words_delizia( + fn show_share_words_extended( _words: heapless::Vec, 33>, _subtitle: Option>, _instructions: Obj, diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs index d6f8393e24..43901b7cab 100644 --- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs @@ -1133,7 +1133,7 @@ impl FirmwareUI for UICaesar { Ok(layout) } - fn show_share_words_delizia( + fn show_share_words_extended( _words: heapless::Vec, 33>, _subtitle: Option>, _instructions: Obj, diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs index 80b25adb44..a392c4affa 100644 --- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs @@ -996,11 +996,11 @@ impl FirmwareUI for UIDelizia { _title: Option>, ) -> Result { Err::, Error>(Error::ValueError( - c"use show_share_words_delizia instead", + c"use show_share_words_extended instead", )) } - fn show_share_words_delizia( + fn show_share_words_extended( words: heapless::Vec, 33>, subtitle: Option>, instructions: Obj, diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs index f5b943f508..a6f4b43a64 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs @@ -1,3 +1,5 @@ pub mod eckhart_swipe_flow_test; +pub mod show_share_words; pub use eckhart_swipe_flow_test::new_eckhart_swipe_flow; +pub use show_share_words::new_show_share_words_flow; diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs b/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs new file mode 100644 index 0000000000..fbf582beb9 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/flow/show_share_words.rs @@ -0,0 +1,111 @@ +use crate::{ + error, + strutil::TString, + translations::TR, + ui::{ + component::{ + text::{ + op::OpTextLayout, + paragraphs::{Paragraph, ParagraphSource}, + }, + ComponentExt, FormattedText, + }, + flow::{ + base::{Decision, DecisionBuilder as _}, + FlowController, FlowMsg, SwipeFlow, + }, + geometry::{Direction, LinearPlacement}, + }, +}; + +use heapless::Vec; + +use super::super::{ + component::{ + ActionBar, Button, Header, ShareWordsScreen, ShareWordsScreenMsg, TextScreen, TextScreenMsg, + }, + fonts, theme, +}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ShowShareWords { + Instruction, + ShareWords, + Confirm, +} + +impl FlowController for ShowShareWords { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, direction: Direction) -> Decision { + match (self, direction) { + _ => self.do_nothing(), + } + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Instruction, FlowMsg::Cancelled) => self.return_msg(FlowMsg::Cancelled), + (Self::Instruction, FlowMsg::Confirmed) => Self::ShareWords.goto(), + (Self::ShareWords, FlowMsg::Cancelled) => Self::Instruction.goto(), + (Self::ShareWords, FlowMsg::Confirmed) => Self::Confirm.goto(), + (Self::Confirm, FlowMsg::Cancelled) => Self::ShareWords.goto(), + (Self::Confirm, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed), + _ => self.do_nothing(), + } + } +} + +pub fn new_show_share_words_flow( + words: Vec, 33>, + _subtitle: TString<'static>, + instruction: Paragraph<'static>, + text_confirm: TString<'static>, +) -> Result { + let instruction = TextScreen::new( + instruction + .into_paragraphs() + .with_placement(LinearPlacement::vertical()), + ) + .with_header(Header::new(TR::reset__recovery_wallet_backup_title.into())) + .with_action_bar(ActionBar::new_double( + Button::with_icon(theme::ICON_CHEVRON_UP), + Button::with_text(TR::buttons__continue.into()), + )) + .map(|msg| match msg { + TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled), + TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), + _ => Some(FlowMsg::Cancelled), + }); + + let share_words = ShareWordsScreen::new(words).map(|msg| match msg { + ShareWordsScreenMsg::Cancelled => Some(FlowMsg::Cancelled), + ShareWordsScreenMsg::Confirmed => Some(FlowMsg::Confirmed), + }); + + let op_confirm = + OpTextLayout::new(theme::TEXT_NORMAL).text(text_confirm, fonts::FONT_SATOSHI_REGULAR_38); + + let confirm = TextScreen::new(FormattedText::new(op_confirm)) + .with_header(Header::new(TR::reset__recovery_wallet_backup_title.into())) + .with_action_bar(ActionBar::new_double( + Button::with_icon(theme::ICON_CHEVRON_LEFT), + Button::with_text(TR::buttons__hold_to_confirm.into()) + .styled(theme::button_confirm()) + .with_long_press(theme::CONFIRM_HOLD_DURATION), + )) + .map(|msg| match msg { + TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled), + TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), + TextScreenMsg::Menu => Some(FlowMsg::Cancelled), + }); + + let res = SwipeFlow::new(&ShowShareWords::Instruction)? + .with_page(&ShowShareWords::Instruction, instruction)? + .with_page(&ShowShareWords::ShareWords, share_words)? + .with_page(&ShowShareWords::Confirm, confirm)?; + Ok(res) +} diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index b1bd638cc2..6e436ca8ce 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -25,7 +25,7 @@ use crate::{ use super::{ component::{ActionBar, Button, Header, HeaderMsg, Hint, TextScreen}, - fonts, theme, UIEckhart, + flow, fonts, theme, UIEckhart, }; impl FirmwareUI for UIEckhart { @@ -553,17 +553,33 @@ impl FirmwareUI for UIEckhart { _words: heapless::Vec, 33>, _title: Option>, ) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + Err::, Error>(Error::ValueError( + c"use show_share_words_extended instead", + )) } - fn show_share_words_delizia( - _words: heapless::Vec, 33>, - _subtitle: Option>, - _instructions: Obj, + fn show_share_words_extended( + words: heapless::Vec, 33>, + subtitle: Option>, + instructions: Obj, + // Irrelevant for Eckhart because the footer is dynamic _text_footer: Option>, - _text_confirm: TString<'static>, + text_confirm: TString<'static>, ) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + // TODO: add support for multiple instructions + let instruction: TString = IterBuf::new() + .try_iterate(instructions)? + .next() + .unwrap() + .try_into()?; + + let flow = flow::show_share_words::new_show_share_words_flow( + words, + subtitle.unwrap_or(TString::empty()), + Paragraph::new(&theme::TEXT_REGULAR, instruction), + text_confirm, + )?; + Ok(flow) } fn show_remaining_shares(_pages_iterable: Obj) -> Result { diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs index 054dd90ae7..927213041f 100644 --- a/core/embed/rust/src/ui/ui_firmware.rs +++ b/core/embed/rust/src/ui/ui_firmware.rs @@ -340,8 +340,8 @@ pub trait FirmwareUI { ) -> Result; // TODO: merge with `show_share_words` instead of having specific version for - // Delizia UI - fn show_share_words_delizia( + // Delizia/Eckhart UI + fn show_share_words_extended( words: Vec, 33>, subtitle: Option>, instructions: Obj, // TODO: replace Obj diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index 876ecfbb63..5dbefcd2ae 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -603,7 +603,7 @@ def show_share_words( # rust/src/ui/api/firmware_micropython.rs -def show_share_words_delizia( +def show_share_words_extended( *, words: Iterable[str], subtitle: str | None, diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 99590e0309..76bba5b497 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -665,6 +665,7 @@ class TR: reset__set_it_to_count_template: str = "Set it to {0} and you will need " reset__share_checked_successfully_template: str = "Share #{0} checked successfully." reset__share_completed_template: str = "Share #{0} completed" + reset__share_words_first: str = "Write down the first word from the backup." reset__share_words_title: str = "Standard backup" reset__slip39_checklist_more_info_threshold: str = "The threshold sets the minumum number of shares needed to recover your wallet." reset__slip39_checklist_more_info_threshold_example_template: str = "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet." diff --git a/core/src/trezor/ui/layouts/delizia/reset.py b/core/src/trezor/ui/layouts/delizia/reset.py index ec65910b6d..c7e616228d 100644 --- a/core/src/trezor/ui/layouts/delizia/reset.py +++ b/core/src/trezor/ui/layouts/delizia/reset.py @@ -38,7 +38,7 @@ def show_share_words( text_confirm = TR.reset__words_written_down_template.format(words_count) return raise_if_not_confirmed( - trezorui_api.show_share_words_delizia( + trezorui_api.show_share_words_extended( words=share_words, subtitle=subtitle, instructions=instructions, diff --git a/core/src/trezor/ui/layouts/eckhart/reset.py b/core/src/trezor/ui/layouts/eckhart/reset.py index 2430cc6ac5..459a44ec14 100644 --- a/core/src/trezor/ui/layouts/eckhart/reset.py +++ b/core/src/trezor/ui/layouts/eckhart/reset.py @@ -16,8 +16,31 @@ def show_share_words( share_index: int | None = None, group_index: int | None = None, ) -> Awaitable[None]: - # FIXME: not implemented - raise NotImplemented + if share_index is None: + subtitle = None + elif group_index is None: + subtitle = TR.reset__recovery_share_title_template.format(share_index + 1) + else: + subtitle = TR.reset__group_share_title_template.format( + group_index + 1, share_index + 1 + ) + words_count = len(share_words) + description = None + # Eckhart currently has only one instruction, other are shown in the hint area + instructions = [TR.reset__write_down_words_template.format(words_count)] + assert len(instructions) == 1 + text_confirm = TR.reset__words_written_down_template.format(words_count) + + return raise_if_not_confirmed( + trezorui_api.show_share_words_extended( + words=share_words, + subtitle=subtitle, + instructions=instructions, + text_footer=description, + text_confirm=text_confirm, + ), + None, + ) async def select_word( @@ -46,9 +69,7 @@ async def select_word( result = await interact( trezorui_api.select_word( title=title, - description=TR.reset__select_word_x_of_y_template.format( - checked_index + 1, count - ), + description=TR.reset__select_word_template.format(checked_index + 1), words=(words[0], words[1], words[2]), ), None, diff --git a/core/translations/en.json b/core/translations/en.json index b00b3531e1..1a5263944f 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -662,12 +662,18 @@ "reset__required_number_of_groups": "The required number of groups for recovery.", "reset__select_correct_word": "Select the correct word for each position.", "reset__select_threshold": "Select the minimum shares required to recover your wallet.", - "reset__select_word_template": "Select {0} word", + "reset__select_word_template": { + "Bolt": "Select {0} word", + "Caesar": "Select {0} word", + "Delizia": "Select {0} word", + "Eckhart": "Select word #{0} from your wallet backup" + }, "reset__select_word_x_of_y_template": "Select word {0} of {1}:", "reset__set_it_to_count_template": "Set it to {0} and you will need ", "reset__share_checked_successfully_template": "Share #{0} checked successfully.", "reset__share_completed_template": "Share #{0} completed", "reset__share_words_title": "Standard backup", + "reset__share_words_first": "Write down the first word from the backup.", "reset__slip39_checklist_more_info_threshold": "The threshold sets the minumum number of shares needed to recover your wallet.", "reset__slip39_checklist_more_info_threshold_example_template": "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet.", "reset__slip39_checklist_num_groups": "Number of groups", @@ -684,7 +690,12 @@ "reset__slip39_checklist_write_down": "Write down and check all shares", "reset__slip39_checklist_write_down_recovery": "Write down & check all wallet backup shares", "reset__the_threshold_sets_the_number_of_shares": "The threshold sets the number of shares ", - "reset__the_word_is_repeated": "The word is repeated", + "reset__the_word_is_repeated": { + "Bolt": "The word is repeated", + "Caesar": "The word is repeated", + "Delizia": "The word is repeated", + "Eckhart": "The word appears multiple times in the backup." + }, "reset__threshold_info": "= minimum number of unique word lists used for recovery.", "reset__title_backup_is_done": "Backup is done", "reset__title_create_wallet": "Create wallet", diff --git a/core/translations/order.json b/core/translations/order.json index 648cdcfe01..10cd796855 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -974,5 +974,6 @@ "972": "ethereum__interaction_contract", "973": "misc__enable_labeling", "974": "ethereum__unknown_contract_address_short", - "975": "instructions__keep_holding" + "975": "instructions__keep_holding", + "976": "reset__share_words_first" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index f5f26e0279..70066928b7 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "43c08e81d71c1c28d77b1650fc96b0dfcd473fde0c922717e5588baeeb581bd3", - "datetime": "2025-02-14T16:12:57.065880", - "commit": "3dabb94653e04856efc89d07c67b7e6f0c587f8c" + "merkle_root": "31454a46346717afd55e29f2a23f6cf64e4e23679af127d774895aa3a700c764", + "datetime": "2025-02-18T22:33:29.769368", + "commit": "778d5265dc258c1b0b74e8433acbd08d090b8746" }, "history": [ {