From b5df4f25464149dbd31fb75a3b5dc30f18ceba58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20Biz=C4=83u?= Date: Fri, 18 Oct 2024 12:25:56 +0200 Subject: [PATCH] feat(core/ethereum): new ETH contract flow --- core/.changelog.d/4251.fixed | 1 + core/embed/rust/librust_qstr.h | 11 +- .../generated/translated_string.rs | 34 +- core/embed/rust/src/ui/component/paginated.rs | 3 + core/embed/rust/src/ui/flow/page.rs | 11 + .../src/ui/model_mercury/component/frame.rs | 5 + .../ui/model_mercury/flow/confirm_action.rs | 224 ++++++++---- .../rust/src/ui/model_mercury/flow/mod.rs | 5 +- .../embed/rust/src/ui/model_mercury/layout.rs | 175 ++++++--- .../ui/model_tr/component/changing_text.rs | 2 +- .../rust/src/ui/model_tr/component/page.rs | 69 +++- core/embed/rust/src/ui/model_tr/layout.rs | 82 ++++- .../rust/src/ui/model_tt/component/page.rs | 12 + core/embed/rust/src/ui/model_tt/layout.rs | 33 +- core/mocks/generated/trezorui2.pyi | 27 +- core/mocks/trezortranslate_keys.pyi | 9 +- core/src/apps/ethereum/layout.py | 42 ++- core/src/apps/ethereum/sign_tx.py | 11 +- core/src/apps/misc/get_ecdh_session_key.py | 5 +- core/src/apps/nem/multisig/layout.py | 6 +- core/src/apps/stellar/layout.py | 4 +- core/src/apps/stellar/operations/layout.py | 16 +- core/src/apps/tezos/layout.py | 18 +- .../src/trezor/ui/layouts/mercury/__init__.py | 176 +++++---- core/src/trezor/ui/layouts/tr/__init__.py | 98 ++++- core/src/trezor/ui/layouts/tr/recovery.py | 2 +- core/src/trezor/ui/layouts/tr/reset.py | 2 +- core/src/trezor/ui/layouts/tt/__init__.py | 89 +++-- core/tools/translations/rules.json | 2 +- core/translations/cs.json | 3 +- core/translations/de.json | 7 +- core/translations/en.json | 9 +- core/translations/es.json | 3 +- core/translations/fr.json | 3 +- core/translations/it.json | 3 +- core/translations/order.json | 9 +- core/translations/pt.json | 3 +- core/translations/signatures.json | 6 +- core/translations/tr.json | 3 +- tests/input_flows_helpers.py | 14 +- tests/ui_tests/fixtures.json | 336 +++++++++--------- 41 files changed, 1076 insertions(+), 497 deletions(-) create mode 100644 core/.changelog.d/4251.fixed diff --git a/core/.changelog.d/4251.fixed b/core/.changelog.d/4251.fixed new file mode 100644 index 00000000000..e9e128b5aa4 --- /dev/null +++ b/core/.changelog.d/4251.fixed @@ -0,0 +1 @@ +New EVM call contract flow. diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 7ac29af8f4e..741883517cf 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -166,6 +166,7 @@ static void _librust_qstrs(void) { MP_QSTR_buttons__try_again; MP_QSTR_buttons__turn_off; MP_QSTR_buttons__turn_on; + MP_QSTR_buttons__view_all_data; MP_QSTR_can_go_back; MP_QSTR_cancel_arrow; MP_QSTR_cancel_cross; @@ -217,8 +218,10 @@ static void _librust_qstrs(void) { MP_QSTR_debug__loading_seed; MP_QSTR_debug__loading_seed_not_recommended; MP_QSTR_decode; + MP_QSTR_default_cancel; MP_QSTR_deinit; MP_QSTR_description; + MP_QSTR_description_font_green; MP_QSTR_details_title; MP_QSTR_device_name__change_template; MP_QSTR_device_name__title; @@ -347,6 +350,7 @@ static void _librust_qstrs(void) { MP_QSTR_notification; MP_QSTR_notification_level; MP_QSTR_page_count; + MP_QSTR_page_limit; MP_QSTR_pages; MP_QSTR_paint; MP_QSTR_passphrase__access_wallet; @@ -728,6 +732,7 @@ static void _librust_qstrs(void) { MP_QSTR_value; MP_QSTR_verb; MP_QSTR_verb_cancel; + MP_QSTR_verb_info; MP_QSTR_verify; MP_QSTR_version; MP_QSTR_warning; @@ -800,6 +805,7 @@ static void _librust_qstrs(void) { MP_QSTR_words__title_threshold; MP_QSTR_words__try_again; MP_QSTR_words__unknown; + MP_QSTR_words__view_all_data_from_menu; MP_QSTR_words__warning; MP_QSTR_words__writable; MP_QSTR_words__yes; @@ -978,6 +984,7 @@ static void _librust_qstrs(void) { MP_QSTR_ethereum__data_size_template; MP_QSTR_ethereum__gas_limit; MP_QSTR_ethereum__gas_price; + MP_QSTR_ethereum__interaction_contract; MP_QSTR_ethereum__max_gas_price; MP_QSTR_ethereum__name_and_version; MP_QSTR_ethereum__new_contract; @@ -996,13 +1003,15 @@ static void _librust_qstrs(void) { MP_QSTR_ethereum__staking_stake_intro; MP_QSTR_ethereum__staking_unstake; MP_QSTR_ethereum__staking_unstake_intro; - MP_QSTR_ethereum__title_confirm_data; MP_QSTR_ethereum__title_confirm_domain; MP_QSTR_ethereum__title_confirm_message; MP_QSTR_ethereum__title_confirm_struct; MP_QSTR_ethereum__title_confirm_typed_data; + MP_QSTR_ethereum__title_input_data; MP_QSTR_ethereum__title_signing_address; + MP_QSTR_ethereum__token_contract; MP_QSTR_ethereum__units_template; + MP_QSTR_ethereum__unknown_contract_address; MP_QSTR_ethereum__unknown_token; MP_QSTR_ethereum__valid_signature; MP_QSTR_fido__already_registered; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 682f9572d8e..88152d821a7 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -457,7 +457,7 @@ pub enum TranslatedString { #[cfg(feature = "universal_fw")] ethereum__name_and_version = 277, // "Name and version" #[cfg(feature = "universal_fw")] - ethereum__new_contract = 278, // "new contract?" + ethereum__new_contract = 278, // "New contract will be deployed" #[cfg(feature = "universal_fw")] ethereum__no_message_field = 279, // "No message field" #[cfg(feature = "universal_fw")] @@ -473,7 +473,7 @@ pub enum TranslatedString { #[cfg(feature = "universal_fw")] ethereum__sign_eip712 = 285, // "Really sign EIP-712 typed data?" #[cfg(feature = "universal_fw")] - ethereum__title_confirm_data = 286, // "Confirm data" + ethereum__title_input_data = 286, // "Input data" #[cfg(feature = "universal_fw")] ethereum__title_confirm_domain = 287, // "Confirm domain" #[cfg(feature = "universal_fw")] @@ -1371,6 +1371,14 @@ pub enum TranslatedString { fido__title_credential_details = 965, // "Credential details" address__public_key_confirmed = 966, // "Public key confirmed" words__continue_anyway = 967, // "Continue anyway" + #[cfg(feature = "universal_fw")] + ethereum__unknown_contract_address = 968, // "Unknown contract address. Continue only if you know what you are doing." + #[cfg(feature = "universal_fw")] + ethereum__token_contract = 970, // "Token contract" + buttons__view_all_data = 971, // "View all data" + words__view_all_data_from_menu = 972, // "View all data from menu." + #[cfg(feature = "universal_fw")] + ethereum__interaction_contract = 973, // "Interaction contract" } impl TranslatedString { @@ -1822,7 +1830,7 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Self::ethereum__name_and_version => "Name and version", #[cfg(feature = "universal_fw")] - Self::ethereum__new_contract => "new contract?", + Self::ethereum__new_contract => "New contract will be deployed", #[cfg(feature = "universal_fw")] Self::ethereum__no_message_field => "No message field", #[cfg(feature = "universal_fw")] @@ -1838,7 +1846,7 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Self::ethereum__sign_eip712 => "Really sign EIP-712 typed data?", #[cfg(feature = "universal_fw")] - Self::ethereum__title_confirm_data => "Confirm data", + Self::ethereum__title_input_data => "Input data", #[cfg(feature = "universal_fw")] Self::ethereum__title_confirm_domain => "Confirm domain", #[cfg(feature = "universal_fw")] @@ -2736,6 +2744,14 @@ impl TranslatedString { Self::fido__title_credential_details => "Credential details", Self::address__public_key_confirmed => "Public key confirmed", Self::words__continue_anyway => "Continue anyway", + #[cfg(feature = "universal_fw")] + Self::ethereum__unknown_contract_address => "Unknown contract address. Continue only if you know what you are doing.", + #[cfg(feature = "universal_fw")] + Self::ethereum__token_contract => "Token contract", + Self::buttons__view_all_data => "View all data", + Self::words__view_all_data_from_menu => "View all data from menu.", + #[cfg(feature = "universal_fw")] + Self::ethereum__interaction_contract => "Interaction contract", } } @@ -3204,7 +3220,7 @@ impl TranslatedString { #[cfg(feature = "universal_fw")] Qstr::MP_QSTR_ethereum__sign_eip712 => Some(Self::ethereum__sign_eip712), #[cfg(feature = "universal_fw")] - Qstr::MP_QSTR_ethereum__title_confirm_data => Some(Self::ethereum__title_confirm_data), + Qstr::MP_QSTR_ethereum__title_input_data => Some(Self::ethereum__title_input_data), #[cfg(feature = "universal_fw")] Qstr::MP_QSTR_ethereum__title_confirm_domain => Some(Self::ethereum__title_confirm_domain), #[cfg(feature = "universal_fw")] @@ -4102,6 +4118,14 @@ impl TranslatedString { Qstr::MP_QSTR_fido__title_credential_details => Some(Self::fido__title_credential_details), Qstr::MP_QSTR_address__public_key_confirmed => Some(Self::address__public_key_confirmed), Qstr::MP_QSTR_words__continue_anyway => Some(Self::words__continue_anyway), + #[cfg(feature = "universal_fw")] + Qstr::MP_QSTR_ethereum__unknown_contract_address => Some(Self::ethereum__unknown_contract_address), + #[cfg(feature = "universal_fw")] + Qstr::MP_QSTR_ethereum__token_contract => Some(Self::ethereum__token_contract), + Qstr::MP_QSTR_buttons__view_all_data => Some(Self::buttons__view_all_data), + Qstr::MP_QSTR_words__view_all_data_from_menu => Some(Self::words__view_all_data_from_menu), + #[cfg(feature = "universal_fw")] + Qstr::MP_QSTR_ethereum__interaction_contract => Some(Self::ethereum__interaction_contract), _ => None, } } diff --git a/core/embed/rust/src/ui/component/paginated.rs b/core/embed/rust/src/ui/component/paginated.rs index c8c992c8a13..4db93782415 100644 --- a/core/embed/rust/src/ui/component/paginated.rs +++ b/core/embed/rust/src/ui/component/paginated.rs @@ -9,6 +9,9 @@ pub enum PageMsg { /// Cancelled using page controls. Cancelled, + /// Info button pressed + Info, + /// Page component was configured to react to swipes and user swiped left. SwipeLeft, diff --git a/core/embed/rust/src/ui/flow/page.rs b/core/embed/rust/src/ui/flow/page.rs index 476ea1b72a1..e99f6d1ec5d 100644 --- a/core/embed/rust/src/ui/flow/page.rs +++ b/core/embed/rust/src/ui/flow/page.rs @@ -13,6 +13,7 @@ pub struct SwipePage { axis: Axis, pages: usize, current: usize, + limit: Option, } impl SwipePage { @@ -23,6 +24,7 @@ impl SwipePage { axis: Axis::Vertical, pages: 1, current: 0, + limit: None, } } @@ -33,12 +35,18 @@ impl SwipePage { axis: Axis::Horizontal, pages: 1, current: 0, + limit: None, } } pub fn inner(&self) -> &T { &self.inner } + + pub fn with_limit(mut self, limit: Option) -> Self { + self.limit = limit; + self + } } impl Component for SwipePage { @@ -47,6 +55,9 @@ impl Component for SwipePage { fn place(&mut self, bounds: Rect) -> Rect { self.bounds = self.inner.place(bounds); self.pages = self.inner.page_count(); + if let Some(limit) = self.limit { + self.pages = self.pages.min(limit); + } self.bounds } diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index 1e6aa12f484..de90949e916 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -150,6 +150,11 @@ where self.with_button(theme::ICON_MENU, FlowMsg::Info, true) } + pub fn with_danger_menu_button(self) -> Self { + self.with_button(theme::ICON_MENU, FlowMsg::Info, true) + .button_styled(theme::button_warning_high()) + } + pub fn with_warning_low_icon(self) -> Self { self.with_button(theme::ICON_WARNING, FlowMsg::Info, false) .button_styled(theme::button_warning_low()) diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs index 0e2913f3200..aa0e2cc147b 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs @@ -96,6 +96,83 @@ impl FlowController for ConfirmActionSimple { } } +/// Flow similar to ConfirmActionSimple, but having swipe up cancel the flow +/// rather than confirm. To confirm, the user needs to open the menu. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ConfirmActionSimpleDefaultCancel { + Intro, + Menu, +} + +impl FlowController for ConfirmActionSimpleDefaultCancel { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, direction: Direction) -> Decision { + match (self, direction) { + (Self::Intro, Direction::Left) => Self::Menu.swipe(direction), + (Self::Menu, Direction::Right) => Self::Intro.swipe(direction), + (Self::Intro, Direction::Up) => self.return_msg(FlowMsg::Cancelled), + _ => self.do_nothing(), + } + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Intro, FlowMsg::Info) => Self::Menu.goto(), + (Self::Menu, FlowMsg::Cancelled) => Self::Intro.swipe_right(), + (Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled), + (Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Confirmed), + _ => self.do_nothing(), + } + } +} + +pub struct ConfirmActionMenu { + verb_cancel: Option>, + info: bool, + verb_info: Option>, +} + +impl ConfirmActionMenu { + pub fn new( + verb_cancel: Option>, + info: bool, + verb_info: Option>, + ) -> Self { + Self { + verb_cancel, + info, + verb_info, + } + } +} + +pub struct ConfirmActionStrings { + title: TString<'static>, + subtitle: Option>, + verb: Option>, + prompt_screen: Option>, +} + +impl ConfirmActionStrings { + pub fn new( + title: TString<'static>, + subtitle: Option>, + verb: Option>, + prompt_screen: Option>, + ) -> Self { + Self { + title, + subtitle, + verb, + prompt_screen, + } + } +} + #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, new_confirm_action_obj) } @@ -141,35 +218,43 @@ fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result( +fn new_confirm_action_uni( content: T, - title: TString<'static>, - subtitle: Option>, - verb_cancel: Option>, - prompt_screen: Option>, + menu: ConfirmActionMenu, + strings: ConfirmActionStrings, hold: bool, - info: bool, + default_cancel: bool, ) -> Result { - let (prompt_screen, prompt_pages, flow, page) = create_flow(title, prompt_screen, hold); + let (prompt_screen, prompt_pages, flow, page) = + create_flow(strings.title, strings.prompt_screen, hold, default_cancel); - let mut content_intro = Frame::left_aligned(title, content) - .with_menu_button() - .with_footer(TR::instructions__swipe_up.into(), None) + let mut content_intro = Frame::left_aligned(strings.title, content) .with_swipe(Direction::Up, SwipeSettings::default()) .with_swipe(Direction::Left, SwipeSettings::default()) .with_vertical_pages(); - if let Some(subtitle) = subtitle { + if default_cancel { + content_intro = content_intro.title_styled(theme::TEXT_WARNING); + content_intro = content_intro.with_danger_menu_button(); + content_intro = content_intro.with_footer( + TR::instructions__swipe_up.into(), + Some(TR::send__cancel_sign.into()), + ); + } else { + content_intro = content_intro.with_menu_button(); + // TODO: conditionally add the verb to the footer as well? + content_intro = content_intro.with_footer(TR::instructions__swipe_up.into(), None); + } + + if let Some(subtitle) = strings.subtitle { content_intro = content_intro.with_subtitle(subtitle); } @@ -182,13 +267,18 @@ pub fn new_confirm_action_uni( let flow = flow?.with_page(page, content_intro)?; - create_menu_and_confirm(subtitle, verb_cancel, hold, info, prompt_screen, flow) + let flow = create_menu(flow, menu, default_cancel, prompt_screen)?; + + let flow = create_confirm(flow, strings.subtitle, hold, prompt_screen)?; + + Ok(LayoutObj::new(flow)?.into()) } fn create_flow( title: TString<'static>, prompt_screen: Option>, hold: bool, + default_cancel: bool, ) -> ( Option>, usize, @@ -198,52 +288,51 @@ fn create_flow( let prompt_screen = prompt_screen.or_else(|| hold.then_some(title)); let prompt_pages: usize = prompt_screen.is_some().into(); - let flow = if prompt_screen.is_some() { - SwipeFlow::new(&ConfirmAction::Intro) + let (flow, page): (Result, &dyn FlowController) = if prompt_screen.is_some() { + (SwipeFlow::new(&ConfirmAction::Intro), &ConfirmAction::Intro) + } else if default_cancel { + ( + SwipeFlow::new(&ConfirmActionSimpleDefaultCancel::Intro), + &ConfirmActionSimpleDefaultCancel::Intro, + ) } else { - SwipeFlow::new(&ConfirmActionSimple::Intro) - }; - - let page: &dyn FlowController = if prompt_screen.is_some() { - &ConfirmAction::Intro - } else { - &ConfirmActionSimple::Intro + ( + SwipeFlow::new(&ConfirmActionSimple::Intro), + &ConfirmActionSimple::Intro, + ) }; (prompt_screen, prompt_pages, flow, page) } -fn create_menu_and_confirm( - subtitle: Option>, - verb_cancel: Option>, - hold: bool, - info: bool, - prompt_screen: Option>, - flow: SwipeFlow, -) -> Result { - let flow = create_menu(flow, verb_cancel, info, prompt_screen)?; - - let flow = create_confirm(flow, subtitle, hold, prompt_screen)?; - - Ok(LayoutObj::new(flow)?.into()) -} - fn create_menu( flow: SwipeFlow, - verb_cancel: Option>, - info: bool, + menu: ConfirmActionMenu, + default_cancel: bool, prompt_screen: Option>, ) -> Result { - let mut menu_choices = VerticalMenu::empty().danger( - theme::ICON_CANCEL, - verb_cancel.unwrap_or(TR::buttons__cancel.into()), - ); - if info { + let mut menu_choices = VerticalMenu::empty(); + if default_cancel { menu_choices = menu_choices.item( - theme::ICON_CHEVRON_RIGHT, - TR::words__title_information.into(), + theme::ICON_CANCEL, + menu.verb_cancel.unwrap_or(TR::buttons__cancel.into()), + ); + menu_choices = + menu_choices.danger(theme::ICON_CHEVRON_RIGHT, TR::words__continue_anyway.into()); + } else { + menu_choices = menu_choices.danger( + theme::ICON_CANCEL, + menu.verb_cancel.unwrap_or(TR::buttons__cancel.into()), ); + if menu.info { + menu_choices = menu_choices.item( + theme::ICON_CHEVRON_RIGHT, + menu.verb_info + .unwrap_or(TR::words__title_information.into()), + ); + } } + let content_menu = Frame::left_aligned("".into(), menu_choices) .with_cancel_button() .with_swipe(Direction::Right, SwipeSettings::immediate()); @@ -304,20 +393,33 @@ fn create_confirm( #[inline(never)] pub fn new_confirm_action_simple( content: T, - title: TString<'static>, - subtitle: Option>, - verb_cancel: Option>, - prompt_screen: Option>, + menu: ConfirmActionMenu, + strings: ConfirmActionStrings, hold: bool, - info: bool, + page_limit: Option, +) -> Result { + new_confirm_action_uni( + SwipeContent::new(SwipePage::vertical(content).with_limit(page_limit)), + menu, + strings, + hold, + false, + ) +} + +#[inline(never)] +pub fn new_confirm_action_simple_default_cancel( + content: T, + menu: ConfirmActionMenu, + strings: ConfirmActionStrings, + hold: bool, + page_limit: Option, ) -> Result { new_confirm_action_uni( - SwipeContent::new(SwipePage::vertical(content)), - title, - subtitle, - verb_cancel, - prompt_screen, + SwipeContent::new(SwipePage::vertical(content).with_limit(page_limit)), + menu, + strings, hold, - info, + true, ) } diff --git a/core/embed/rust/src/ui/model_mercury/flow/mod.rs b/core/embed/rust/src/ui/model_mercury/flow/mod.rs index 9cf4ff6c914..69f50773576 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/mod.rs @@ -18,7 +18,10 @@ pub mod warning_hi_prio; mod util; -pub use confirm_action::{new_confirm_action, new_confirm_action_simple}; +pub use confirm_action::{ + new_confirm_action, new_confirm_action_simple, new_confirm_action_simple_default_cancel, + ConfirmActionMenu, ConfirmActionStrings, +}; #[cfg(feature = "universal_fw")] pub use confirm_fido::new_confirm_fido; pub use confirm_firmware_update::new_confirm_firmware_update; diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 9908af97361..64212d17198 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -49,7 +49,10 @@ use crate::{ }, model_mercury::{ component::{check_homescreen_format, SwipeContent}, - flow::new_confirm_action_simple, + flow::{ + new_confirm_action_simple, new_confirm_action_simple_default_cancel, + ConfirmActionMenu, ConfirmActionStrings, + }, theme::ICON_BULLET_CHECKMARK, }, }, @@ -237,14 +240,12 @@ extern "C" fn new_confirm_emphasized(n_args: usize, args: *const Obj, kwargs: *m } } - flow::new_confirm_action_simple( + new_confirm_action_simple( FormattedText::new(ops).vertically_centered(), - title, - None, - None, - Some(title), - false, + ConfirmActionMenu::new(None, false, None), + ConfirmActionStrings::new(title, None, None, Some(title)), false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -255,14 +256,18 @@ struct ConfirmBlobParams { subtitle: Option>, data: Obj, description: Option>, + description_font: &'static TextStyle, extra: Option>, verb: Option>, verb_cancel: Option>, + verb_info: Option>, info_button: bool, prompt: bool, hold: bool, chunkify: bool, text_mono: bool, + page_limit: Option, + default_cancel: bool, } impl ConfirmBlobParams { @@ -271,7 +276,7 @@ impl ConfirmBlobParams { data: Obj, description: Option>, verb: Option>, - verb_cancel: Option>, + verb_info: Option>, prompt: bool, hold: bool, ) -> Self { @@ -280,14 +285,18 @@ impl ConfirmBlobParams { subtitle: None, data, description, + description_font: &theme::TEXT_NORMAL, extra: None, verb, - verb_cancel, + verb_cancel: None, + verb_info, info_button: false, prompt, hold, chunkify: false, text_mono: true, + page_limit: None, + default_cancel: false, } } @@ -301,6 +310,11 @@ impl ConfirmBlobParams { self } + fn with_verb_cancel(mut self, verb_cancel: Option>) -> Self { + self.verb_cancel = verb_cancel; + self + } + fn with_info_button(mut self, info_button: bool) -> Self { self.info_button = info_button; self @@ -316,12 +330,27 @@ impl ConfirmBlobParams { self } + fn with_page_limit(mut self, page_limit: Option) -> Self { + self.page_limit = page_limit; + self + } + + fn with_default_cancel(mut self, default_cancel: bool) -> Self { + self.default_cancel = default_cancel; + self + } + + fn with_description_font(mut self, description_font: &'static TextStyle) -> Self { + self.description_font = description_font; + self + } + fn into_flow(self) -> Result { let paragraphs = ConfirmBlob { description: self.description.unwrap_or("".into()), extra: self.extra.unwrap_or("".into()), data: self.data.try_into()?, - description_font: &theme::TEXT_NORMAL, + description_font: self.description_font, extra_font: &theme::TEXT_DEMIBOLD, data_font: if self.chunkify { let data: TString = self.data.try_into()?; @@ -334,14 +363,23 @@ impl ConfirmBlobParams { } .into_paragraphs(); - flow::new_confirm_action_simple( + let build_flow = if self.default_cancel { + new_confirm_action_simple_default_cancel + } else { + new_confirm_action_simple + }; + + build_flow( paragraphs, - self.title, - self.subtitle, - self.verb_cancel, - self.prompt.then_some(self.title), + ConfirmActionMenu::new(self.verb_cancel, self.info_button, self.verb_info), + ConfirmActionStrings::new( + self.title, + self.subtitle, + self.verb, + self.prompt.then_some(self.title), + ), self.hold, - self.info_button, + self.page_limit, ) } } @@ -352,7 +390,17 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?; let description: Option = kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; - let extra: Option = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?; + let description_font_green: bool = + kwargs.get_or(Qstr::MP_QSTR_description_font_green, false)?; + let text_mono: bool = kwargs.get_or(Qstr::MP_QSTR_text_mono, true)?; + let extra: Option = kwargs + .get(Qstr::MP_QSTR_extra) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let subtitle: Option = kwargs + .get(Qstr::MP_QSTR_subtitle) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; let verb: Option = kwargs .get(Qstr::MP_QSTR_verb) .unwrap_or_else(|_| Obj::const_none()) @@ -361,21 +409,44 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map .get(Qstr::MP_QSTR_verb_cancel) .unwrap_or_else(|_| Obj::const_none()) .try_into_option()?; + let verb_info: Option = kwargs + .get(Qstr::MP_QSTR_verb_info) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let info: bool = kwargs.get_or(Qstr::MP_QSTR_info, true)?; let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; let prompt_screen: bool = kwargs.get_or(Qstr::MP_QSTR_prompt_screen, true)?; + let default_cancel: bool = kwargs.get_or(Qstr::MP_QSTR_default_cancel, false)?; + let page_limit: Option = kwargs + .get(Qstr::MP_QSTR_page_limit) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + + let description_font = if description_font_green { + &theme::TEXT_SUB_GREEN_LIME + } else { + &theme::TEXT_NORMAL + }; ConfirmBlobParams::new( title, data, description, verb, - verb_cancel, + verb_info, prompt_screen, hold, ) + .with_description_font(description_font) + .with_text_mono(text_mono) + .with_subtitle(subtitle) + .with_verb_cancel(verb_cancel) .with_extra(extra) + .with_info_button(info) .with_chunkify(chunkify) + .with_default_cancel(default_cancel) + .with_page_limit(page_limit) .into_flow() }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -407,7 +478,13 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut } .into_paragraphs(); - flow::new_confirm_action_simple(paragraphs, title, None, None, None, false, false) + new_confirm_action_simple( + paragraphs, + ConfirmActionMenu::new(None, false, None), + ConfirmActionStrings::new(title, None, None, None), + false, + None, + ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } @@ -425,14 +502,12 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m &theme::TEXT_MONO, )?; - flow::new_confirm_action_simple( + new_confirm_action_simple( paragraphs.into_paragraphs(), - title, - None, - None, - hold.then_some(title), + ConfirmActionMenu::new(None, false, None), + ConfirmActionStrings::new(title, None, None, hold.then_some(title)), hold, - false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -456,12 +531,15 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m new_confirm_action_simple( paragraphs, - TR::homescreen__settings_title.into(), - Some(TR::homescreen__settings_subtitle.into()), - None, - Some(TR::homescreen__settings_title.into()), - false, + ConfirmActionMenu::new(None, false, None), + ConfirmActionStrings::new( + TR::homescreen__settings_title.into(), + Some(TR::homescreen__settings_subtitle.into()), + None, + Some(TR::homescreen__settings_title.into()), + ), false, + None, ) } else { if !check_homescreen_format(jpeg) { @@ -539,8 +617,9 @@ extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Ma let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; let text_mono: bool = kwargs.get_or(Qstr::MP_QSTR_text_mono, true)?; - ConfirmBlobParams::new(title, value, description, verb, verb_cancel, hold, hold) + ConfirmBlobParams::new(title, value, description, verb, None, hold, hold) .with_subtitle(subtitle) + .with_verb_cancel(verb_cancel) .with_info_button(info_button) .with_chunkify(chunkify) .with_text_mono(text_mono) @@ -562,14 +641,12 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value)); } - flow::new_confirm_action_simple( + new_confirm_action_simple( paragraphs.into_paragraphs(), - title, - None, - None, - Some(title), - true, + ConfirmActionMenu::new(None, true, None), + ConfirmActionStrings::new(title, None, None, Some(title)), true, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -851,14 +928,17 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut ]) .into_paragraphs(); - flow::new_confirm_action_simple( + new_confirm_action_simple( paragraphs, - TR::coinjoin__title.into(), - None, - None, - Some(TR::coinjoin__title.into()), + ConfirmActionMenu::new(None, false, None), + ConfirmActionStrings::new( + TR::coinjoin__title.into(), + None, + None, + Some(TR::coinjoin__title.into()), + ), true, - false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1263,12 +1343,19 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// data: str | bytes, /// description: str | None, - /// extra: str | None, + /// description_font_green: bool = False, + /// text_mono: bool = True, + /// extra: str | None = None, + /// subtitle: str | None = None, /// verb: str | None = None, /// verb_cancel: str | None = None, + /// verb_info: str | None = None, + /// info: bool = True, /// hold: bool = False, /// chunkify: bool = False, /// prompt_screen: bool = False, + /// default_cancel: bool = False, + /// page_limit: int | None = None, /// ) -> LayoutObj[UiResult]: /// """Confirm byte sequence data.""" Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(), diff --git a/core/embed/rust/src/ui/model_tr/component/changing_text.rs b/core/embed/rust/src/ui/model_tr/component/changing_text.rs index 7aa6c5ac945..9546fd311a7 100644 --- a/core/embed/rust/src/ui/model_tr/component/changing_text.rs +++ b/core/embed/rust/src/ui/model_tr/component/changing_text.rs @@ -177,7 +177,7 @@ impl ChangingTextLine { let x_offset = if self.text.len() % 2 == 0 { 0 } else { 2 }; let baseline = Point::new(self.pad.area.x0 + x_offset, self.y_baseline()); - shape::Text::new(baseline, self.text.as_ref()) + shape::Text::new(baseline, &text_to_display) .with_font(self.font) .render(target); } diff --git a/core/embed/rust/src/ui/model_tr/component/page.rs b/core/embed/rust/src/ui/model_tr/component/page.rs index 935f184c88b..7179367bb58 100644 --- a/core/embed/rust/src/ui/model_tr/component/page.rs +++ b/core/embed/rust/src/ui/model_tr/component/page.rs @@ -2,7 +2,7 @@ use crate::{ translations::TR, ui::{ component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate}, - display::Color, + display::{Color, Font}, geometry::{Insets, Rect}, shape::Renderer, }, @@ -13,21 +13,26 @@ use super::{ ButtonDetails, ButtonLayout, ButtonPos, }; +#[derive(PartialEq)] +enum LastPageLayout { + Confirm, + ArmedConfirmPlusInfo, +} + pub struct ButtonPage where T: Component + Paginate, { page_count: usize, active_page: usize, + page_limit: Option, + last_page_layout: LastPageLayout, content: Child, pad: Pad, - /// Left button of the first screen cancel_btn_details: Option, - /// Right button of the last screen confirm_btn_details: Option, - /// Left button of every screen + info_btn_details: Option, back_btn_details: Option, - /// Right button of every screen apart the last one next_btn_details: Option, buttons: Child, } @@ -40,10 +45,17 @@ where Self { page_count: 0, // will be set in place() active_page: 0, + page_limit: None, + last_page_layout: LastPageLayout::Confirm, content: Child::new(content), pad: Pad::with_background(background).with_clear(), cancel_btn_details: Some(ButtonDetails::cancel_icon()), confirm_btn_details: Some(ButtonDetails::text(TR::buttons__confirm.into())), + info_btn_details: Some( + ButtonDetails::text("i".into()) + .with_fixed_width(theme::BUTTON_ICON_WIDTH) + .with_font(Font::NORMAL), + ), back_btn_details: Some(ButtonDetails::up_arrow_icon()), next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()), // Setting empty layout for now, we do not yet know the page count. @@ -53,6 +65,12 @@ where } } + pub fn with_armed_confirm_plus_info(mut self) -> Self { + self.last_page_layout = LastPageLayout::ArmedConfirmPlusInfo; + self.confirm_btn_details = Some(ButtonDetails::armed_text(TR::words__confirm.into())); + self + } + pub fn with_cancel_btn(mut self, btn_details: Option) -> Self { self.cancel_btn_details = btn_details; self @@ -73,6 +91,11 @@ where self } + pub fn with_page_limit(mut self, page_limit: Option) -> Self { + self.page_limit = page_limit; + self + } + pub fn has_next_page(&self) -> bool { self.active_page < self.page_count - 1 } @@ -119,11 +142,11 @@ where fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout { let btn_left = self.get_left_button_details(!has_prev); + let btn_middle = self.get_middle_button_details(has_next); let btn_right = self.get_right_button_details(has_next); - ButtonLayout::new(btn_left, None, btn_right) + ButtonLayout::new(btn_left, btn_middle, btn_right) } - /// Get the left button details, depending whether the page is first or not. fn get_left_button_details(&self, is_first: bool) -> Option { if is_first { self.cancel_btn_details.clone() @@ -132,13 +155,21 @@ where } } - /// Get the right button details, depending on whether there is a next - /// page. + fn get_middle_button_details(&self, has_next_page: bool) -> Option { + if has_next_page || self.last_page_layout == LastPageLayout::Confirm { + None + } else { + self.confirm_btn_details.clone() + } + } + fn get_right_button_details(&self, has_next_page: bool) -> Option { if has_next_page { self.next_btn_details.clone() - } else { + } else if self.last_page_layout == LastPageLayout::Confirm { self.confirm_btn_details.clone() + } else { + self.info_btn_details.clone() } } } @@ -172,6 +203,10 @@ where // Need to be called here, only after content is placed // and we can calculate the page count. self.page_count = self.content.page_count(); + if let Some(limit) = self.page_limit { + self.page_count = self.page_count.min(limit); + } + self.set_buttons_for_initial_page(self.page_count); self.buttons.place(button_area); bounds @@ -191,17 +226,25 @@ where return Some(PageMsg::Cancelled); } } + ButtonPos::Middle => { + return Some(PageMsg::Confirmed); + } ButtonPos::Right => { if self.has_next_page() { // Clicked NEXT. Scroll down. self.go_to_next_page(); self.change_page(ctx); } else { - // Clicked CONFIRM. Send result. - return Some(PageMsg::Confirmed); + match self.last_page_layout { + LastPageLayout::Confirm => { + return Some(PageMsg::Confirmed); + } + LastPageLayout::ArmedConfirmPlusInfo => { + return Some(PageMsg::Info); + } + } } } - _ => {} } } diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index f957ca85100..2f354c6185e 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -94,6 +94,7 @@ where match msg { PageMsg::Confirmed => Ok(CONFIRMED.as_obj()), PageMsg::Cancelled => Ok(CANCELLED.as_obj()), + PageMsg::Info => Ok(INFO.as_obj()), _ => Err(Error::TypeError), } } @@ -243,7 +244,9 @@ fn content_in_button_page( content: T, verb: TString<'static>, verb_cancel: Option>, + info: bool, hold: bool, + page_limit: Option, ) -> Result { // Left button - icon, text or nothing. let cancel_btn = verb_cancel.map(ButtonDetails::from_text_possible_icon); @@ -259,9 +262,17 @@ fn content_in_button_page( confirm_btn = confirm_btn.map(|btn| btn.with_default_duration()); } - let content = ButtonPage::new(content, theme::BG) + let mut content = ButtonPage::new(content, theme::BG) .with_cancel_btn(cancel_btn) - .with_confirm_btn(confirm_btn); + .with_page_limit(page_limit); + + if confirm_btn.is_some() { + content = content.with_confirm_btn(confirm_btn); + } + + if info { + content = content.with_armed_confirm_plus_info(); + } let mut frame = ScrollableFrame::new(content); if !title.is_empty() { @@ -303,7 +314,7 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M paragraphs.into_paragraphs() }; - content_in_button_page(title, paragraphs, verb, verb_cancel, hold) + content_in_button_page(title, paragraphs, verb, verb_cancel, false, hold, None) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } @@ -314,15 +325,25 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?; let description: Option = kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; - let extra: Option = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?; - let verb: TString<'static> = - kwargs.get_or(Qstr::MP_QSTR_verb, TR::buttons__confirm.into())?; - let verb_cancel: Option> = kwargs + let extra: Option = kwargs + .get(Qstr::MP_QSTR_extra) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let verb: Option = kwargs + .get(Qstr::MP_QSTR_verb) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let verb_cancel: Option = kwargs .get(Qstr::MP_QSTR_verb_cancel) .unwrap_or_else(|_| Obj::const_none()) .try_into_option()?; + let info: bool = kwargs.get_or(Qstr::MP_QSTR_info, false)?; let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; + let page_limit: Option = kwargs + .get(Qstr::MP_QSTR_page_limit) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; let style = if chunkify { // Chunkifying the address into smaller pieces when requested @@ -341,7 +362,15 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map } .into_paragraphs(); - content_in_button_page(title, paragraphs, verb, verb_cancel, hold) + content_in_button_page( + title, + paragraphs, + verb.unwrap_or(TR::buttons__confirm.into()), + verb_cancel, + info, + hold, + page_limit, + ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } @@ -388,7 +417,9 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m paragraphs.into_paragraphs(), TR::buttons__confirm.into(), None, + false, hold, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -418,7 +449,15 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs: .text_bold(TR::reset__tos_link); let formatted = FormattedText::new(ops).vertically_centered(); - content_in_button_page(title, formatted, button, Some("".into()), false) + content_in_button_page( + title, + formatted, + button, + Some("".into()), + false, + false, + None, + ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } @@ -500,7 +539,9 @@ extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Ma paragraphs, verb.unwrap_or(TR::buttons__confirm.into()), Some("".into()), + false, hold, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -523,7 +564,9 @@ extern "C" fn new_confirm_joint_total(n_args: usize, args: *const Obj, kwargs: * paragraphs, TR::buttons__hold_to_confirm.into(), Some("".into()), + false, true, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -554,6 +597,8 @@ extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs: TR::buttons__confirm.into(), Some("".into()), false, + false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -926,6 +971,8 @@ extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *m TR::buttons__confirm.into(), Some("".into()), false, + false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1209,6 +1256,8 @@ extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map button, Some("<".into()), false, + false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1234,7 +1283,9 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut paragraphs, TR::buttons__hold_to_confirm.into(), None, + false, true, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1430,6 +1481,8 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut button, Some("".into()), false, + false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1482,6 +1535,8 @@ extern "C" fn new_show_group_share_success( TR::buttons__continue.into(), None, false, + false, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1679,12 +1734,19 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// data: str | bytes, /// description: str | None, - /// extra: str | None, + /// description_font_green: bool = False, + /// text_mono: bool = True, + /// extra: str | None = None, + /// subtitle: str | None = None, /// verb: str = "CONFIRM", /// verb_cancel: str | None = None, + /// verb_info: str | None = None, + /// info: bool = True, /// hold: bool = False, /// chunkify: bool = False, /// prompt_screen: bool = False, + /// default_cancel: bool = False, + /// page_limit: int | None = None, /// ) -> LayoutObj[UiResult]: /// """Confirm byte sequence data.""" Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(), diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index 1d59d7ba55c..e00b12f649f 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -30,6 +30,7 @@ pub struct ButtonPage { /// Swipe controller. swipe: Swipe, scrollbar: ScrollBar, + page_limit: Option, /// Hold-to-confirm mode whenever this is `Some(loader)`. loader: Option, button_cancel: Option