From c300576d6c48662d2d83727cfbc0fb2dd5b83c23 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 | 12 +- .../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 +- .../rust/src/ui/model_tr/component/page.rs | 69 +- core/embed/rust/src/ui/model_tr/layout.rs | 77 +- .../rust/src/ui/model_tt/component/page.rs | 12 + core/embed/rust/src/ui/model_tt/layout.rs | 42 +- core/mocks/generated/trezorui2.pyi | 28 +- core/mocks/trezortranslate_keys.pyi | 9 +- core/src/apps/ethereum/layout.py | 42 +- core/src/apps/ethereum/sign_tx.py | 13 +- 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 | 94 +- 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 +- .../device_tests/ethereum/test_definitions.py | 37 +- tests/device_tests/ethereum/test_signtx.py | 19 +- tests/input_flows.py | 20 +- tests/input_flows_helpers.py | 33 +- tests/ui_tests/fixtures.json | 2128 ++++++++--------- 43 files changed, 2048 insertions(+), 1423 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 0000000000..e9e128b5aa --- /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 60229124ac..7bb5c5ef01 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -129,6 +129,7 @@ static void _librust_qstrs(void) { MP_QSTR_button; MP_QSTR_button_event; MP_QSTR_button_request; + MP_QSTR_button_style_confirm; MP_QSTR_buttons__abort; MP_QSTR_buttons__access; MP_QSTR_buttons__again; @@ -167,6 +168,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; @@ -218,8 +220,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; @@ -307,6 +311,7 @@ static void _librust_qstrs(void) { MP_QSTR_instructions__swipe_up; MP_QSTR_instructions__tap_to_confirm; MP_QSTR_instructions__tap_to_start; + MP_QSTR_instructions__view_all_data; MP_QSTR_is_type_of; MP_QSTR_items; MP_QSTR_items_title; @@ -349,6 +354,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; @@ -730,6 +736,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; @@ -980,6 +987,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; @@ -998,13 +1006,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 682f9572d8..9b8c34779e 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" + instructions__view_all_data = 972, // "View all data in the 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::instructions__view_all_data => "View all data in the 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_instructions__view_all_data => Some(Self::instructions__view_all_data), + #[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 c8c992c8a1..4db9378241 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 476ea1b72a..e99f6d1ec5 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 1e6aa12f48..de90949e91 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 0e2913f320..aa0e2cc147 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)), - title, - subtitle, - verb_cancel, - prompt_screen, + SwipeContent::new(SwipePage::vertical(content).with_limit(page_limit)), + menu, + strings, hold, - info, + 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).with_limit(page_limit)), + menu, + strings, + hold, + 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 feb3f17ca9..c00df2ef2b 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/mod.rs @@ -19,7 +19,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 652d75c1fe..b0f2189f00 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -50,7 +50,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, }, }, @@ -238,14 +241,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) } @@ -256,14 +257,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 { @@ -272,7 +277,7 @@ impl ConfirmBlobParams { data: Obj, description: Option>, verb: Option>, - verb_cancel: Option>, + verb_info: Option>, prompt: bool, hold: bool, ) -> Self { @@ -281,14 +286,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, } } @@ -302,6 +311,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 @@ -317,12 +331,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()?; @@ -335,14 +364,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, ) } } @@ -353,7 +391,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()) @@ -362,21 +410,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) } @@ -408,7 +479,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) } } @@ -426,14 +503,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) } @@ -457,12 +532,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()), + 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, - Some(TR::homescreen__settings_title.into()), - false, - false, ) } else { if !check_homescreen_format(jpeg) { @@ -540,8 +618,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) @@ -563,14 +642,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) } @@ -849,14 +926,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) } @@ -1261,12 +1341,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/page.rs b/core/embed/rust/src/ui/model_tr/component/page.rs index 935f184c88..7179367bb5 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 0690f24047..7cd85eb883 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,10 +262,15 @@ 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_page_limit(page_limit) .with_confirm_btn(confirm_btn); + if info { + content = content.with_armed_confirm_plus_info(); + } + let mut frame = ScrollableFrame::new(content); if !title.is_empty() { frame = frame.with_title(title); @@ -303,7 +311,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 +322,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 +359,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) } } @@ -393,7 +419,9 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m paragraphs.into_paragraphs(), button_text, Some("".into()), + false, hold, + None, ) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -423,7 +451,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) } } @@ -505,7 +541,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) } @@ -528,7 +566,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) } @@ -559,6 +599,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) } @@ -932,6 +974,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) } @@ -1215,6 +1259,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) } @@ -1240,7 +1286,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) } @@ -1436,6 +1484,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) } @@ -1488,6 +1538,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) } @@ -1685,12 +1737,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 1d59d7ba55..e00b12f649 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