From 5020868c2ce3c762532f50c0d3375b7eb4db1239 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Tue, 30 Apr 2024 13:32:48 +0200 Subject: [PATCH] feat(core/ui): T3T1 receive flow [no changelog] --- core/embed/rust/librust_qstr.h | 10 + .../generated/translated_string.rs | 30 ++- core/embed/rust/src/ui/component/base.rs | 2 +- core/embed/rust/src/ui/component/empty.rs | 1 + core/embed/rust/src/ui/component/label.rs | 18 +- .../rust/src/ui/component/text/layout.rs | 6 +- core/embed/rust/src/ui/layout/util.rs | 2 + .../component/address_details.rs | 61 ++--- .../src/ui/model_mercury/component/frame.rs | 15 +- .../model_mercury/component/status_screen.rs | 11 + .../src/ui/model_mercury/component/swipe.rs | 1 + .../src/ui/model_mercury/flow/get_address.rs | 251 ++++++++++++------ .../rust/src/ui/model_mercury/flow/mod.rs | 2 + .../ui/model_mercury/flow/warning_hi_prio.rs | 125 +++++++++ .../embed/rust/src/ui/model_mercury/layout.rs | 76 ++---- .../rust/src/ui/model_mercury/theme/mod.rs | 40 ++- .../ui/model_tr/component/address_details.rs | 2 +- .../ui/model_tt/component/address_details.rs | 2 +- core/mocks/generated/trezorui2.pyi | 37 +-- core/mocks/trezortranslate_keys.pyi | 10 +- core/src/apps/bitcoin/get_address.py | 9 +- .../src/trezor/ui/layouts/mercury/__init__.py | 130 +++------ core/src/trezor/ui/layouts/tr/__init__.py | 8 + core/src/trezor/ui/layouts/tt/__init__.py | 10 +- core/translations/cs.json | 3 +- core/translations/de.json | 3 +- core/translations/en.json | 10 +- core/translations/es.json | 3 +- core/translations/fr.json | 3 +- core/translations/order.json | 12 +- core/translations/signatures.json | 6 +- tests/input_flows.py | 4 +- 32 files changed, 583 insertions(+), 320 deletions(-) create mode 100644 core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 225c165646..5474b349a9 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -30,14 +30,21 @@ static void _librust_qstrs(void) { MP_QSTR_addr_mismatch__wrong_derivation_path; MP_QSTR_addr_mismatch__xpub_mismatch; MP_QSTR_address; + MP_QSTR_address__cancel_contact_support; + MP_QSTR_address__cancel_receive; + MP_QSTR_address__confirmed; MP_QSTR_address__public_key; + MP_QSTR_address__qr_code; MP_QSTR_address__title_cosigner; MP_QSTR_address__title_receive_address; MP_QSTR_address__title_yours; + MP_QSTR_address_details__account_info; MP_QSTR_address_details__derivation_path; + MP_QSTR_address_details__derivation_path_colon; MP_QSTR_address_details__title_receive_address; MP_QSTR_address_details__title_receiving_to; MP_QSTR_address_label; + MP_QSTR_address_qr; MP_QSTR_address_title; MP_QSTR_allow_cancel; MP_QSTR_altcoin_tx_summary; @@ -210,6 +217,7 @@ static void _librust_qstrs(void) { MP_QSTR_flow_get_address; MP_QSTR_flow_prompt_backup; MP_QSTR_flow_show_share_words; + MP_QSTR_flow_warning_hi_prio; MP_QSTR_get_language; MP_QSTR_hold; MP_QSTR_hold_danger; @@ -237,6 +245,7 @@ static void _librust_qstrs(void) { MP_QSTR_inputs__return; MP_QSTR_inputs__show; MP_QSTR_inputs__space; + MP_QSTR_instructions__continue_in_app; MP_QSTR_instructions__hold_to_confirm; MP_QSTR_instructions__swipe_up; MP_QSTR_instructions__tap_to_confirm; @@ -623,6 +632,7 @@ static void _librust_qstrs(void) { MP_QSTR_words__array_of; MP_QSTR_words__blockhash; MP_QSTR_words__buying; + MP_QSTR_words__cancel_and_exit; MP_QSTR_words__confirm; MP_QSTR_words__confirm_fee; MP_QSTR_words__contains; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index fb704f8d98..0c8bd01f3c 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -20,7 +20,7 @@ pub enum TranslatedString { address__title_cosigner = 7, // "Cosigner" address__title_receive_address = 8, // "Receive address" address__title_yours = 9, // "Yours" - address_details__derivation_path = 10, // "Derivation path:" + address_details__derivation_path_colon = 10, // "Derivation path:" address_details__title_receive_address = 11, // "Receive address" address_details__title_receiving_to = 12, // "Receiving to" authenticate__confirm_template = 13, // "Allow connected computer to confirm your {0} is genuine?" @@ -1260,6 +1260,14 @@ pub enum TranslatedString { reset__check_backup_instructions = 859, // "Let's do a quick check of your backup." words__instructions = 860, // "Instructions" words__not_recommended = 861, // "Not recommended!" + address_details__account_info = 862, // "Account info" + address__cancel_contact_support = 863, // "If receive address doesn't match, contact Trezor Support at trezor.io/support." + address__cancel_receive = 864, // "Cancel receive" + address__qr_code = 865, // "QR code" + address_details__derivation_path = 866, // "Derivation path" + instructions__continue_in_app = 867, // "Continue in the app" + words__cancel_and_exit = 868, // "Cancel and exit" + address__confirmed = 869, // "Receive address confirmed" } impl TranslatedString { @@ -1275,7 +1283,7 @@ impl TranslatedString { Self::address__title_cosigner => "Cosigner", Self::address__title_receive_address => "Receive address", Self::address__title_yours => "Yours", - Self::address_details__derivation_path => "Derivation path:", + Self::address_details__derivation_path_colon => "Derivation path:", Self::address_details__title_receive_address => "Receive address", Self::address_details__title_receiving_to => "Receiving to", Self::authenticate__confirm_template => "Allow connected computer to confirm your {0} is genuine?", @@ -2515,6 +2523,14 @@ impl TranslatedString { Self::reset__check_backup_instructions => "Let's do a quick check of your backup.", Self::words__instructions => "Instructions", Self::words__not_recommended => "Not recommended!", + Self::address_details__account_info => "Account info", + Self::address__cancel_contact_support => "If receive address doesn't match, contact Trezor Support at trezor.io/support.", + Self::address__cancel_receive => "Cancel receive", + Self::address__qr_code => "QR code", + Self::address_details__derivation_path => "Derivation path", + Self::instructions__continue_in_app => "Continue in the app", + Self::words__cancel_and_exit => "Cancel and exit", + Self::address__confirmed => "Receive address confirmed", } } @@ -2531,7 +2547,7 @@ impl TranslatedString { Qstr::MP_QSTR_address__title_cosigner => Some(Self::address__title_cosigner), Qstr::MP_QSTR_address__title_receive_address => Some(Self::address__title_receive_address), Qstr::MP_QSTR_address__title_yours => Some(Self::address__title_yours), - Qstr::MP_QSTR_address_details__derivation_path => Some(Self::address_details__derivation_path), + Qstr::MP_QSTR_address_details__derivation_path_colon => Some(Self::address_details__derivation_path_colon), Qstr::MP_QSTR_address_details__title_receive_address => Some(Self::address_details__title_receive_address), Qstr::MP_QSTR_address_details__title_receiving_to => Some(Self::address_details__title_receiving_to), Qstr::MP_QSTR_authenticate__confirm_template => Some(Self::authenticate__confirm_template), @@ -3771,6 +3787,14 @@ impl TranslatedString { Qstr::MP_QSTR_reset__check_backup_instructions => Some(Self::reset__check_backup_instructions), Qstr::MP_QSTR_words__instructions => Some(Self::words__instructions), Qstr::MP_QSTR_words__not_recommended => Some(Self::words__not_recommended), + Qstr::MP_QSTR_address_details__account_info => Some(Self::address_details__account_info), + Qstr::MP_QSTR_address__cancel_contact_support => Some(Self::address__cancel_contact_support), + Qstr::MP_QSTR_address__cancel_receive => Some(Self::address__cancel_receive), + Qstr::MP_QSTR_address__qr_code => Some(Self::address__qr_code), + Qstr::MP_QSTR_address_details__derivation_path => Some(Self::address_details__derivation_path), + Qstr::MP_QSTR_instructions__continue_in_app => Some(Self::instructions__continue_in_app), + Qstr::MP_QSTR_words__cancel_and_exit => Some(Self::words__cancel_and_exit), + Qstr::MP_QSTR_address__confirmed => Some(Self::address__confirmed), _ => None, } } diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index ab62262b53..91a7d53dff 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -82,7 +82,7 @@ pub struct Child { } impl Child { - pub fn new(component: T) -> Self { + pub const fn new(component: T) -> Self { Self { component, marked_for_paint: true, diff --git a/core/embed/rust/src/ui/component/empty.rs b/core/embed/rust/src/ui/component/empty.rs index 258f201d53..2d5f92cd16 100644 --- a/core/embed/rust/src/ui/component/empty.rs +++ b/core/embed/rust/src/ui/component/empty.rs @@ -1,6 +1,7 @@ use super::{Component, Event, EventCtx, Never}; use crate::ui::{geometry::Rect, shape::Renderer}; +#[derive(Clone)] pub struct Empty; impl Component for Empty { diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index a3cf62fbc4..869f254dab 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -18,7 +18,7 @@ pub struct Label<'a> { } impl<'a> Label<'a> { - pub fn new(text: TString<'a>, align: Alignment, style: TextStyle) -> Self { + pub const fn new(text: TString<'a>, align: Alignment, style: TextStyle) -> Self { Self { text, layout: TextLayout::new(style).with_align(align), @@ -26,34 +26,34 @@ impl<'a> Label<'a> { } } - pub fn left_aligned(text: TString<'a>, style: TextStyle) -> Self { + pub const fn left_aligned(text: TString<'a>, style: TextStyle) -> Self { Self::new(text, Alignment::Start, style) } - pub fn right_aligned(text: TString<'a>, style: TextStyle) -> Self { + pub const fn right_aligned(text: TString<'a>, style: TextStyle) -> Self { Self::new(text, Alignment::End, style) } - pub fn centered(text: TString<'a>, style: TextStyle) -> Self { + pub const fn centered(text: TString<'a>, style: TextStyle) -> Self { Self::new(text, Alignment::Center, style) } - pub fn top_aligned(mut self) -> Self { + pub const fn top_aligned(mut self) -> Self { self.vertical = Alignment::Start; self } - pub fn vertically_centered(mut self) -> Self { + pub const fn vertically_centered(mut self) -> Self { self.vertical = Alignment::Center; self } - pub fn bottom_aligned(mut self) -> Self { + pub const fn bottom_aligned(mut self) -> Self { self.vertical = Alignment::End; self } - pub fn styled(mut self, style: TextStyle) -> Self { + pub const fn styled(mut self, style: TextStyle) -> Self { self.layout.style = style; self } @@ -74,7 +74,7 @@ impl<'a> Label<'a> { self.layout.bounds } - pub fn alignment(&self) -> Alignment { + pub const fn alignment(&self) -> Alignment { self.layout.align } diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 682d414251..3655696b83 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -199,7 +199,7 @@ impl TextStyle { impl TextLayout { /// Create a new text layout, with empty size and default text parameters /// filled from `T`. - pub fn new(style: TextStyle) -> Self { + pub const fn new(style: TextStyle) -> Self { Self { bounds: Rect::zero(), padding_top: 0, @@ -210,12 +210,12 @@ impl TextLayout { } } - pub fn with_bounds(mut self, bounds: Rect) -> Self { + pub const fn with_bounds(mut self, bounds: Rect) -> Self { self.bounds = bounds; self } - pub fn with_align(mut self, align: Alignment) -> Self { + pub const fn with_align(mut self, align: Alignment) -> Self { self.align = align; self } diff --git a/core/embed/rust/src/ui/layout/util.rs b/core/embed/rust/src/ui/layout/util.rs index ef2355b64c..60604d7301 100644 --- a/core/embed/rust/src/ui/layout/util.rs +++ b/core/embed/rust/src/ui/layout/util.rs @@ -25,6 +25,7 @@ use crate::{ /// consumption and conversion time. pub const MAX_HEX_CHARS_ON_SCREEN: usize = 256; +#[derive(Clone)] pub enum StrOrBytes { Str(TString<'static>), Bytes(Obj), @@ -55,6 +56,7 @@ impl TryFrom for StrOrBytes { } } +#[derive(Clone)] pub struct ConfirmBlob { pub description: TString<'static>, pub extra: TString<'static>, diff --git a/core/embed/rust/src/ui/model_mercury/component/address_details.rs b/core/embed/rust/src/ui/model_mercury/component/address_details.rs index 2d80a05173..0ecb87a4e2 100644 --- a/core/embed/rust/src/ui/model_mercury/component/address_details.rs +++ b/core/embed/rust/src/ui/model_mercury/component/address_details.rs @@ -7,7 +7,7 @@ use crate::{ ui::{ component::{ text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, - Component, Event, EventCtx, Paginate, Qr, + Component, Event, EventCtx, Paginate, }, geometry::Rect, shape::Renderer, @@ -18,8 +18,8 @@ use super::{theme, Frame, FrameMsg}; const MAX_XPUBS: usize = 16; +#[derive(Clone)] pub struct AddressDetails { - qr_code: Frame, details: Frame>>, xpub_view: Frame>>, xpubs: Vec<(TString<'static>, TString<'static>), MAX_XPUBS>, @@ -29,43 +29,38 @@ pub struct AddressDetails { impl AddressDetails { pub fn new( - qr_title: TString<'static>, - qr_address: TString<'static>, - case_sensitive: bool, details_title: TString<'static>, account: Option>, path: Option>, ) -> Result { let mut para = ParagraphVecShort::new(); if let Some(a) = account { - para.add(Paragraph::new( - &theme::TEXT_NORMAL, - TR::words__account_colon, + para.add(Paragraph::new::( + &theme::TEXT_SUB_GREY, + TR::words__account.into(), + )); + para.add(Paragraph::new(&theme::TEXT_MONO_GREY_LIGHT, a)); + } + if account.is_some() & path.is_some() { + para.add(Paragraph::new( + &theme::TEXT_SUB_GREY, + TString::from_str(" "), )); - para.add(Paragraph::new(&theme::TEXT_MONO, a)); } if let Some(p) = path { - para.add(Paragraph::new( - &theme::TEXT_NORMAL, - TR::address_details__derivation_path, + para.add(Paragraph::new::( + &theme::TEXT_SUB_GREY, + TR::address_details__derivation_path.into(), )); - para.add(Paragraph::new(&theme::TEXT_MONO, p)); + para.add(Paragraph::new(&theme::TEXT_MONO_GREY_LIGHT, p)); } let result = Self { - qr_code: Frame::left_aligned( - qr_title, - qr_address - .map(|s| Qr::new(s, case_sensitive))? - .with_border(7), - ) - .with_cancel_button() - .with_border(theme::borders_horizontal_scroll()), details: Frame::left_aligned(details_title, para.into_paragraphs()) .with_cancel_button() .with_border(theme::borders_horizontal_scroll()), xpub_view: Frame::left_aligned( " \n ".into(), - Paragraph::new(&theme::TEXT_MONO, "").into_paragraphs(), + Paragraph::new(&theme::TEXT_MONO_GREY_LIGHT, "").into_paragraphs(), ) .with_cancel_button() .with_border(theme::borders_horizontal_scroll()), @@ -121,13 +116,13 @@ impl AddressDetails { impl Paginate for AddressDetails { fn page_count(&mut self) -> usize { let total_xpub_pages: u8 = self.xpub_page_count.iter().copied().sum(); - 2usize.saturating_add(total_xpub_pages.into()) + 1usize.saturating_add(total_xpub_pages.into()) } fn change_page(&mut self, to_page: usize) { self.current_page = to_page; - if to_page > 1 { - let i = to_page - 2; + if to_page > 0 { + let i = to_page - 1; let (xpub_index, xpub_page) = self.lookup(i); self.switch_xpub(xpub_index, xpub_page); } @@ -138,7 +133,6 @@ impl Component for AddressDetails { type Msg = (); fn place(&mut self, bounds: Rect) -> Rect { - self.qr_code.place(bounds); self.details.place(bounds); self.xpub_view.place(bounds); @@ -153,8 +147,7 @@ impl Component for AddressDetails { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { let msg = match self.current_page { - 0 => self.qr_code.event(ctx, event), - 1 => self.details.event(ctx, event), + 0 => self.details.event(ctx, event), _ => self.xpub_view.event(ctx, event), }; match msg { @@ -165,16 +158,14 @@ impl Component for AddressDetails { fn paint(&mut self) { match self.current_page { - 0 => self.qr_code.paint(), - 1 => self.details.paint(), + 0 => self.details.paint(), _ => self.xpub_view.paint(), } } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { match self.current_page { - 0 => self.qr_code.render(target), - 1 => self.details.render(target), + 0 => self.details.render(target), _ => self.xpub_view.render(target), } } @@ -182,8 +173,7 @@ impl Component for AddressDetails { #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { match self.current_page { - 0 => self.qr_code.bounds(sink), - 1 => self.details.bounds(sink), + 0 => self.details.bounds(sink), _ => self.xpub_view.bounds(sink), } } @@ -194,8 +184,7 @@ impl crate::trace::Trace for AddressDetails { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("AddressDetails"); match self.current_page { - 0 => t.child("qr_code", &self.qr_code), - 1 => t.child("details", &self.details), + 0 => t.child("details", &self.details), _ => t.child("xpub_view", &self.xpub_view), } } 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 499b0407c1..3d6961786d 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -35,7 +35,7 @@ impl Frame where T: Component, { - pub fn new(alignment: Alignment, title: TString<'static>, content: T) -> Self { + pub const fn new(alignment: Alignment, title: TString<'static>, content: T) -> Self { Self { title: Child::new( Label::new(title, alignment, theme::label_title_main()).vertically_centered(), @@ -49,19 +49,19 @@ where } } - pub fn left_aligned(title: TString<'static>, content: T) -> Self { + pub const fn left_aligned(title: TString<'static>, content: T) -> Self { Self::new(Alignment::Start, title, content) } - pub fn right_aligned(title: TString<'static>, content: T) -> Self { + pub const fn right_aligned(title: TString<'static>, content: T) -> Self { Self::new(Alignment::End, title, content) } - pub fn centered(title: TString<'static>, content: T) -> Self { + pub const fn centered(title: TString<'static>, content: T) -> Self { Self::new(Alignment::Center, title, content) } - pub fn with_border(mut self, border: Insets) -> Self { + pub const fn with_border(mut self, border: Insets) -> Self { self.border = border; self } @@ -130,6 +130,11 @@ where self } + pub fn with_danger(self) -> Self { + self.button_styled(theme::button_danger()) + .title_styled(theme::label_title_danger()) + } + pub fn inner(&self) -> &T { self.content.inner() } diff --git a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs index a4d0554acf..b32fa790d4 100644 --- a/core/embed/rust/src/ui/model_mercury/component/status_screen.rs +++ b/core/embed/rust/src/ui/model_mercury/component/status_screen.rs @@ -10,6 +10,7 @@ use super::{theme, Swipe, SwipeDirection}; /// Component showing status of an operation. Most typically embedded as a /// content of a Frame and showing success (checkmark with a circle around). +#[derive(Clone)] pub struct StatusScreen { area: Rect, icon: Icon, @@ -18,6 +19,7 @@ pub struct StatusScreen { dismiss_type: DismissType, } +#[derive(Clone)] enum DismissType { SwipeUp(Swipe), Timeout(Timeout), @@ -62,6 +64,15 @@ impl StatusScreen { DismissType::SwipeUp(Swipe::new().up()), ) } + + pub fn new_neutral_timeout() -> Self { + Self::new( + theme::ICON_SIMPLE_CHECKMARK, + theme::GREY_EXTRA_LIGHT, + theme::GREY_DARK, + DismissType::Timeout(Timeout::new(TIMEOUT_MS)), + ) + } } impl Component for StatusScreen { diff --git a/core/embed/rust/src/ui/model_mercury/component/swipe.rs b/core/embed/rust/src/ui/model_mercury/component/swipe.rs index 0a67a3b6f6..8f14783532 100644 --- a/core/embed/rust/src/ui/model_mercury/component/swipe.rs +++ b/core/embed/rust/src/ui/model_mercury/component/swipe.rs @@ -10,6 +10,7 @@ use super::theme; pub use crate::ui::component::SwipeDirection; +#[derive(Clone)] pub struct Swipe { pub area: Rect, pub allow_up: bool, diff --git a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs index 477d458c2d..1d928e555a 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs @@ -1,33 +1,41 @@ use crate::{ error, + micropython::{iter::IterBuf, qstr::Qstr}, + strutil::TString, + translations::TR, ui::{ component::{ - image::BlendedImage, - text::paragraphs::{Paragraph, Paragraphs}, - ComponentExt, Qr, SwipeDirection, Timeout, + text::paragraphs::{Paragraph, ParagraphSource, Paragraphs}, + ComponentExt, Qr, SwipeDirection, }, flow::{ base::Decision, flow_store, FlowMsg, FlowState, FlowStore, IgnoreSwipe, SwipeFlow, SwipePage, }, + layout::util::ConfirmBlob, }, }; use super::super::{ - component::{Frame, FrameMsg, IconDialog, VerticalMenu, VerticalMenuChoiceMsg}, + component::{ + AddressDetails, CancelInfoConfirmMsg, Frame, FrameMsg, PromptScreen, StatusScreen, + VerticalMenu, VerticalMenuChoiceMsg, + }, theme, }; -const LONGSTRING: &'static str = "https://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKohttps://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKo"; +const QR_BORDER: i16 = 4; #[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)] pub enum GetAddress { Address, + Tap, + Confirmed, Menu, QrCode, AccountInfo, Cancel, - Success, + CancelTap, } impl FlowState for GetAddress { @@ -36,8 +44,9 @@ impl FlowState for GetAddress { (GetAddress::Address, SwipeDirection::Left) => { Decision::Goto(GetAddress::Menu, direction) } - (GetAddress::Address, SwipeDirection::Up) => { - Decision::Goto(GetAddress::Success, direction) + (GetAddress::Address, SwipeDirection::Up) => Decision::Goto(GetAddress::Tap, direction), + (GetAddress::Tap, SwipeDirection::Down) => { + Decision::Goto(GetAddress::Address, direction) } (GetAddress::Menu, SwipeDirection::Right) => { Decision::Goto(GetAddress::Address, direction) @@ -48,7 +57,15 @@ impl FlowState for GetAddress { (GetAddress::AccountInfo, SwipeDirection::Right) => { Decision::Goto(GetAddress::Menu, direction) } - (GetAddress::Cancel, SwipeDirection::Up) => Decision::Return(FlowMsg::Cancelled), + (GetAddress::Cancel, SwipeDirection::Up) => { + Decision::Goto(GetAddress::CancelTap, direction) + } + (GetAddress::Cancel, SwipeDirection::Right) => { + Decision::Goto(GetAddress::Menu, direction) + } + (GetAddress::CancelTap, SwipeDirection::Down) => { + Decision::Goto(GetAddress::Cancel, direction) + } _ => Decision::Nothing, } } @@ -59,6 +76,12 @@ impl FlowState for GetAddress { Decision::Goto(GetAddress::Menu, SwipeDirection::Left) } + (GetAddress::Tap, FlowMsg::Confirmed) => { + Decision::Goto(GetAddress::Confirmed, SwipeDirection::Up) + } + + (GetAddress::Confirmed, _) => Decision::Return(FlowMsg::Confirmed), + (GetAddress::Menu, FlowMsg::Choice(0)) => { Decision::Goto(GetAddress::QrCode, SwipeDirection::Left) } @@ -87,14 +110,19 @@ impl FlowState for GetAddress { Decision::Goto(GetAddress::Menu, SwipeDirection::Right) } - (GetAddress::Success, _) => Decision::Return(FlowMsg::Confirmed), + (GetAddress::CancelTap, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Cancelled), + + (GetAddress::CancelTap, FlowMsg::Cancelled) => { + Decision::Goto(GetAddress::Menu, SwipeDirection::Right) + } + _ => Decision::Nothing, } } } use crate::{ - micropython::{buffer::StrBuffer, map::Map, obj::Obj, util}, + micropython::{map::Map, obj::Obj, util}, ui::layout::obj::LayoutObj, }; @@ -103,83 +131,132 @@ pub extern "C" fn new_get_address(n_args: usize, args: *const Obj, kwargs: *mut } impl GetAddress { - fn new(_args: &[Obj], _kwargs: &Map) -> Result { + fn new(_args: &[Obj], kwargs: &Map) -> Result { + let title: TString = "Receive address".into(); // TODO: address__title_receive_address w/o uppercase + 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 address: Obj = kwargs.get(Qstr::MP_QSTR_address)?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; + + let address_qr: TString = kwargs.get(Qstr::MP_QSTR_address_qr)?.try_into()?; + let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?; + + let account: Option = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?; + let path: Option = kwargs.get(Qstr::MP_QSTR_path)?.try_into_option()?; + let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?; + + // Address + let data_style = if chunkify { + let address: TString = address.try_into()?; + theme::get_chunkified_text_style(address.len()) + } else { + &theme::TEXT_MONO + }; + let paragraphs = ConfirmBlob { + description: description.unwrap_or("".into()), + extra: extra.unwrap_or("".into()), + data: address.try_into()?, + description_font: &theme::TEXT_NORMAL, + extra_font: &theme::TEXT_DEMIBOLD, + data_font: data_style, + } + .into_paragraphs(); + let content_address = Frame::left_aligned(title, SwipePage::vertical(paragraphs)) + .with_menu_button() + .with_footer(TR::instructions__swipe_up.into(), None) + .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)); + // .one_button_request(ButtonRequestCode::Address, "show_address"); + + // Tap + let content_tap = Frame::left_aligned(title, PromptScreen::new_tap_to_confirm()) + .with_footer(TR::instructions__tap_to_confirm.into(), None) + .map(|msg| match msg { + FrameMsg::Content(()) => Some(FlowMsg::Confirmed), + FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled), + _ => None, + }); + + let content_confirmed = IgnoreSwipe::new( + Frame::left_aligned( + TR::address__confirmed.into(), + StatusScreen::new_success_timeout(), + ) + .with_footer(TR::instructions__continue_in_app.into(), None), + ) + .map(|_| Some(FlowMsg::Confirmed)); + + // Menu + let content_menu = Frame::left_aligned( + "".into(), + VerticalMenu::empty() + .item(theme::ICON_QR_CODE, TR::address__qr_code.into()) + .item( + theme::ICON_CHEVRON_RIGHT, + TR::address_details__account_info.into(), + ) + .danger(theme::ICON_CANCEL, TR::address__cancel_receive.into()), + ) + .with_cancel_button() + .map(|msg| match msg { + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), + FrameMsg::Button(_) => Some(FlowMsg::Cancelled), + }); + + // QrCode + let content_qr = Frame::left_aligned( + title, + IgnoreSwipe::new( + address_qr + .map(|s| Qr::new(s, case_sensitive))? + .with_border(QR_BORDER), + ), + ) + .with_cancel_button() + .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)); + + // AccountInfo + let mut ad = AddressDetails::new(TR::address_details__account_info.into(), account, path)?; + for i in IterBuf::new().try_iterate(xpubs)? { + let [xtitle, text]: [TString; 2] = util::iter_into_array(i)?; + ad.add_xpub(xtitle, text)?; + } + let content_account = SwipePage::vertical(ad).map(|_| Some(FlowMsg::Cancelled)); + + // Cancel + let content_cancel_info = Frame::left_aligned( + TR::address__cancel_receive.into(), + SwipePage::vertical(Paragraphs::new(Paragraph::new( + &theme::TEXT_MAIN_GREY_LIGHT, + TR::address__cancel_contact_support, + ))), + ) + .with_cancel_button() + .with_footer(TR::instructions__swipe_up.into(), None) + .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)); + + // CancelTap + let content_cancel_tap = Frame::left_aligned( + TR::address__cancel_receive.into(), + PromptScreen::new_tap_to_cancel(), + ) + .with_cancel_button() + .with_footer(TR::instructions__tap_to_confirm.into(), None) + .map(|msg| match msg { + FrameMsg::Content(()) => Some(FlowMsg::Confirmed), + FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled), + _ => None, + }); + let store = flow_store() - .add( - Frame::left_aligned( - "Receive".into(), - SwipePage::vertical(Paragraphs::new(Paragraph::new( - &theme::TEXT_MONO, - StrBuffer::from(LONGSTRING), - ))), - ) - .with_subtitle("address".into()) - .with_menu_button() - .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)), - )? - .add( - Frame::left_aligned( - "".into(), - VerticalMenu::empty() - .item(theme::ICON_QR_CODE, "Address QR code".into()) - .item(theme::ICON_CHEVRON_RIGHT, "Account info".into()) - .danger(theme::ICON_CANCEL, "Cancel operation".into()), - ) - .with_cancel_button() - .map(|msg| match msg { - FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => { - Some(FlowMsg::Choice(i)) - } - FrameMsg::Button(_) => Some(FlowMsg::Cancelled), - }), - )? - .add( - Frame::left_aligned( - "Receive address".into(), - IgnoreSwipe::new(Qr::new( - "https://youtu.be/iFkEs4GNgfc?si=Jl4UZSIAYGVcLQKo", - true, - )?), - ) - .with_cancel_button() - .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)), - )? - .add( - Frame::left_aligned( - "Account info".into(), - SwipePage::vertical(Paragraphs::new(Paragraph::new( - &theme::TEXT_NORMAL, - StrBuffer::from("taproot xp"), - ))), - ) - .with_cancel_button() - .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)), - )? - .add( - Frame::left_aligned( - "Cancel receive".into(), - SwipePage::vertical(Paragraphs::new(Paragraph::new( - &theme::TEXT_NORMAL, - StrBuffer::from("O rly?"), - ))), - ) - .with_cancel_button() - .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled)), - )? - .add( - IconDialog::new( - BlendedImage::new( - theme::IMAGE_BG_CIRCLE, - theme::IMAGE_FG_WARN, - theme::SUCCESS_COLOR, - theme::FG, - theme::BG, - ), - StrBuffer::from("Confirmed"), - Timeout::new(100), - ) - .map(|_| Some(FlowMsg::Confirmed)), - )?; + .add(content_address)? + .add(content_tap)? + .add(content_confirmed)? + .add(content_menu)? + .add(content_qr)? + .add(content_account)? + .add(content_cancel_info)? + .add(content_cancel_tap)?; let res = SwipeFlow::new(GetAddress::Address, store)?; Ok(LayoutObj::new(res)?.into()) } 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 c9a14f6e58..9796605970 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/mod.rs @@ -3,9 +3,11 @@ pub mod confirm_reset_recover; pub mod get_address; pub mod prompt_backup; pub mod show_share_words; +pub mod warning_hi_prio; pub use confirm_reset_create::ConfirmResetCreate; pub use confirm_reset_recover::ConfirmResetRecover; pub use get_address::GetAddress; pub use prompt_backup::PromptBackup; pub use show_share_words::ShowShareWords; +pub use warning_hi_prio::WarningHiPrio; diff --git a/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs b/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs new file mode 100644 index 0000000000..68f60c91a9 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/flow/warning_hi_prio.rs @@ -0,0 +1,125 @@ +use crate::{ + error, + micropython::qstr::Qstr, + strutil::TString, + translations::TR, + ui::{ + component::{ + text::paragraphs::{Paragraph, ParagraphSource}, + ComponentExt, SwipeDirection, + }, + flow::{ + base::Decision, flow_store, FlowMsg, FlowState, FlowStore, IgnoreSwipe, SwipeFlow, + SwipePage, + }, + }, +}; + +use super::super::{ + component::{Frame, FrameMsg, StatusScreen, VerticalMenu, VerticalMenuChoiceMsg}, + theme, +}; + +#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)] +pub enum WarningHiPrio { + Message, + Menu, + Cancelled, +} + +impl FlowState for WarningHiPrio { + fn handle_swipe(&self, direction: SwipeDirection) -> Decision { + match (self, direction) { + (WarningHiPrio::Message, SwipeDirection::Left) => { + Decision::Goto(WarningHiPrio::Menu, direction) + } + (WarningHiPrio::Message, SwipeDirection::Up) => { + Decision::Goto(WarningHiPrio::Cancelled, direction) + } + (WarningHiPrio::Menu, SwipeDirection::Right) => { + Decision::Goto(WarningHiPrio::Message, direction) + } + _ => Decision::Nothing, + } + } + + fn handle_event(&self, msg: FlowMsg) -> Decision { + match (self, msg) { + (WarningHiPrio::Message, FlowMsg::Info) => { + Decision::Goto(WarningHiPrio::Menu, SwipeDirection::Left) + } + (WarningHiPrio::Menu, FlowMsg::Choice(1)) => Decision::Return(FlowMsg::Confirmed), + (WarningHiPrio::Menu, FlowMsg::Choice(_)) => { + Decision::Goto(WarningHiPrio::Cancelled, SwipeDirection::Up) + } + (WarningHiPrio::Menu, FlowMsg::Cancelled) => { + Decision::Goto(WarningHiPrio::Message, SwipeDirection::Right) + } + (WarningHiPrio::Cancelled, _) => Decision::Return(FlowMsg::Cancelled), + _ => Decision::Nothing, + } + } +} + +use crate::{ + micropython::{map::Map, obj::Obj, util}, + ui::layout::obj::LayoutObj, +}; + +pub extern "C" fn new_warning_hi_prio(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, WarningHiPrio::new) } +} + +impl WarningHiPrio { + const EXTRA_PADDING: i16 = 6; + + fn new(_args: &[Obj], kwargs: &Map) -> Result { + let title: TString = kwargs.get_or(Qstr::MP_QSTR_title, TR::words__warning.try_into()?)?; + let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; + let value: TString = kwargs.get_or(Qstr::MP_QSTR_value, "".into())?; + let cancel: TString = TR::words__cancel_and_exit.into(); + let confirm: TString = "Continue anyway".into(); // FIXME: en.json has punctuation + let done_title: TString = "Operation cancelled".into(); + + // Message + let paragraphs = [ + Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description), + Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, value) + .with_top_padding(Self::EXTRA_PADDING), + ] + .into_paragraphs(); + let content_message = Frame::left_aligned(title, SwipePage::vertical(paragraphs)) + .with_menu_button() + .with_footer(TR::instructions__swipe_up.into(), Some(cancel)) + .with_danger() + .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)); + // .one_button_request(ButtonRequestCode::Warning, br_type); + + // Menu + let content_menu = Frame::left_aligned( + "".into(), + VerticalMenu::empty() + .item(theme::ICON_CANCEL, "Cancel".into()) // TODO: button__cancel after it's lowercase + .danger(theme::ICON_CHEVRON_RIGHT, confirm), + ) + .with_cancel_button() + .map(|msg| match msg { + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), + FrameMsg::Button(_) => Some(FlowMsg::Cancelled), + }); + + // Cancelled + let content_cancelled = IgnoreSwipe::new( + Frame::left_aligned(done_title, StatusScreen::new_neutral_timeout()) + .with_footer(TR::instructions__continue_in_app.into(), None), + ) + .map(|_| Some(FlowMsg::Cancelled)); + + let store = flow_store() + .add(content_message)? + .add(content_menu)? + .add(content_cancelled)?; + let res = SwipeFlow::new(WarningHiPrio::Message, store)?; + Ok(LayoutObj::new(res)?.into()) + } +} diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 14c3a4a081..3d491ba33d 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -684,38 +684,6 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { - let block = move |_args: &[Obj], kwargs: &Map| { - let qr_title: TString = kwargs.get(Qstr::MP_QSTR_qr_title)?.try_into()?; - let details_title: TString = kwargs.get(Qstr::MP_QSTR_details_title)?.try_into()?; - let address: TString = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; - let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?; - let account: Option = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?; - let path: Option = kwargs.get(Qstr::MP_QSTR_path)?.try_into_option()?; - - let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?; - - let mut ad = AddressDetails::new( - qr_title, - address, - case_sensitive, - details_title, - account, - path, - )?; - - for i in IterBuf::new().try_iterate(xpubs)? { - let [xtitle, text]: [TString; 2] = util::iter_into_array(i)?; - ad.add_xpub(xtitle, text)?; - } - - let obj = - LayoutObj::new(SimplePage::horizontal(ad, theme::BG).with_swipe_right_to_go_back())?; - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_show_info_with_cancel(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -1012,12 +980,13 @@ extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let description: TString = kwargs.get_or(Qstr::MP_QSTR_description, "".into())?; let value: TString = kwargs.get_or(Qstr::MP_QSTR_value, "".into())?; - let content = SwipeUpScreen::new(Paragraphs::new([Paragraph::new( - &theme::TEXT_MAIN_GREY_LIGHT, - value, - )])); + let content = SwipeUpScreen::new(Paragraphs::new([ + Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description), + Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, value), + ])); let obj = LayoutObj::new( Frame::left_aligned(title, content) .with_warning_button() @@ -1753,19 +1722,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm TOS before creating a wallet and have a user hold to confirm creation.""" Qstr::MP_QSTR_flow_confirm_reset_create => obj_fn_kw!(0, flow::confirm_reset_create::new_confirm_reset_create).as_obj(), - /// def show_address_details( - /// *, - /// qr_title: str, - /// address: str, - /// case_sensitive: bool, - /// details_title: str, - /// account: str | None, - /// path: str | None, - /// xpubs: list[tuple[str, str]], - /// ) -> LayoutObj[UiResult]: - /// """Show address details - QR code, account, path, cosigner xpubs.""" - Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(), - /// def show_info_with_cancel( /// *, /// title: str, @@ -2092,9 +2048,29 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Show single-line text in the middle of the screen.""" Qstr::MP_QSTR_show_wait_text => obj_fn_1!(new_show_wait_text).as_obj(), - /// def flow_get_address() -> LayoutObj[UiResult]: + /// def flow_get_address( + /// *, + /// address: str | bytes, + /// description: str | None, + /// extra: str | None, + /// chunkify: bool, + /// address_qr: str | None, + /// case_sensitive: bool, + /// account: str | None, + /// path: str | None, + /// xpubs: list[tuple[str, str]], + /// ) -> LayoutObj[UiResult]: /// """Get address / receive funds.""" Qstr::MP_QSTR_flow_get_address => obj_fn_kw!(0, flow::get_address::new_get_address).as_obj(), + + /// def flow_warning_hi_prio( + /// *, + /// title: str, + /// description: str, + /// value: str = "", + /// ) -> LayoutObj[UiResult]: + /// """Warning modal with multiple steps to confirm.""" + Qstr::MP_QSTR_flow_warning_hi_prio => obj_fn_kw!(0, flow::warning_hi_prio::new_warning_hi_prio).as_obj(), }; #[cfg(test)] diff --git a/core/embed/rust/src/ui/model_mercury/theme/mod.rs b/core/embed/rust/src/ui/model_mercury/theme/mod.rs index 1aeb18bc67..efec7cf409 100644 --- a/core/embed/rust/src/ui/model_mercury/theme/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/theme/mod.rs @@ -215,6 +215,16 @@ pub const fn label_title_main() -> TextStyle { ) } +pub const fn label_title_danger() -> TextStyle { + TextStyle::new( + Font::NORMAL, + ORANGE_LIGHT, + GREY_DARK, + GREY_LIGHT, + GREY_LIGHT, + ) +} + pub const fn label_title_sub() -> TextStyle { TextStyle::new(Font::SUB, GREY, GREY_DARK, GREY_LIGHT, GREY_LIGHT) } @@ -355,9 +365,30 @@ pub const fn button_cancel() -> ButtonStyleSheet { } } -// TODO: delete pub const fn button_danger() -> ButtonStyleSheet { - button_cancel() + ButtonStyleSheet { + normal: &ButtonStyle { + font: Font::DEMIBOLD, + text_color: ORANGE_LIGHT, + button_color: BG, + icon_color: ORANGE_LIGHT, + background_color: BG, + }, + active: &ButtonStyle { + font: Font::DEMIBOLD, + text_color: ORANGE_LIGHT, + button_color: BG, + icon_color: ORANGE_LIGHT, + background_color: BG, + }, + disabled: &ButtonStyle { + font: Font::DEMIBOLD, + text_color: ORANGE_LIGHT, + button_color: BG, + icon_color: ORANGE_LIGHT, + background_color: BG, + }, + } } pub const fn button_reset() -> ButtonStyleSheet { @@ -591,6 +622,10 @@ pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, GREY_EXTRA_LIGHT, BG .with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth) .with_ellipsis_icon(ICON_PAGE_NEXT, 0) .with_prev_page_icon(ICON_PAGE_PREV, 0); +pub const TEXT_MONO_GREY_LIGHT: TextStyle = TextStyle { + text_color: GREY_LIGHT, + ..TEXT_MONO +}; /// Makes sure that the displayed text (usually address) will get divided into /// smaller chunks. pub const TEXT_MONO_ADDRESS_CHUNKS: TextStyle = TEXT_MONO @@ -656,6 +691,7 @@ pub const MNEMONIC_BUTTON_HEIGHT: i16 = 52; pub const RESULT_PADDING: i16 = 6; pub const RESULT_FOOTER_START: i16 = 171; pub const RESULT_FOOTER_HEIGHT: i16 = 62; +pub const DETAILS_SPACING: i16 = 8; // checklist settings pub const CHECKLIST_CHECK_WIDTH: i16 = 16; diff --git a/core/embed/rust/src/ui/model_tr/component/address_details.rs b/core/embed/rust/src/ui/model_tr/component/address_details.rs index 128742bfb4..e6714e36f7 100644 --- a/core/embed/rust/src/ui/model_tr/component/address_details.rs +++ b/core/embed/rust/src/ui/model_tr/component/address_details.rs @@ -52,7 +52,7 @@ impl AddressDetails { if let Some(path) = path { para.add(Paragraph::new( &theme::TEXT_BOLD, - TR::address_details__derivation_path, + TR::address_details__derivation_path_colon, )); para.add(Paragraph::new(&theme::TEXT_MONO, path)); } diff --git a/core/embed/rust/src/ui/model_tt/component/address_details.rs b/core/embed/rust/src/ui/model_tt/component/address_details.rs index ebcf2d40de..a6bc1bc57d 100644 --- a/core/embed/rust/src/ui/model_tt/component/address_details.rs +++ b/core/embed/rust/src/ui/model_tt/component/address_details.rs @@ -47,7 +47,7 @@ impl AddressDetails { if let Some(p) = path { para.add(Paragraph::new( &theme::TEXT_NORMAL, - TR::address_details__derivation_path, + TR::address_details__derivation_path_colon, )); para.add(Paragraph::new(&theme::TEXT_MONO, p)); } diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 34658d8417..bee6c6aeb1 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -155,20 +155,6 @@ def flow_confirm_reset_create() -> LayoutObj[UiResult]: """Confirm TOS before creating a wallet and have a user hold to confirm creation.""" -# rust/src/ui/model_mercury/layout.rs -def show_address_details( - *, - qr_title: str, - address: str, - case_sensitive: bool, - details_title: str, - account: str | None, - path: str | None, - xpubs: list[tuple[str, str]], -) -> LayoutObj[UiResult]: - """Show address details - QR code, account, path, cosigner xpubs.""" - - # rust/src/ui/model_mercury/layout.rs def show_info_with_cancel( *, @@ -529,8 +515,29 @@ def show_wait_text(message: str, /) -> LayoutObj[None]: # rust/src/ui/model_mercury/layout.rs -def flow_get_address() -> LayoutObj[UiResult]: +def flow_get_address( + *, + address: str | bytes, + description: str | None, + extra: str | None, + chunkify: bool, + address_qr: str | None, + case_sensitive: bool, + account: str | None, + path: str | None, + xpubs: list[tuple[str, str]], +) -> LayoutObj[UiResult]: """Get address / receive funds.""" + + +# rust/src/ui/model_mercury/layout.rs +def flow_warning_hi_prio( + *, + title: str, + description: str, + value: str = "", +) -> LayoutObj[UiResult]: + """Warning modal with multiple steps to confirm.""" CONFIRMED: UiResult CANCELLED: UiResult INFO: UiResult diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 117056ca29..a17c68fd63 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -8,11 +8,17 @@ class TR: addr_mismatch__support_url: str = "trezor.io/support" addr_mismatch__wrong_derivation_path: str = "Wrong derivation path for selected account." addr_mismatch__xpub_mismatch: str = "XPUB mismatch?" + address__cancel_contact_support: str = "If receive address doesn't match, contact Trezor Support at trezor.io/support." + address__cancel_receive: str = "Cancel receive" + address__confirmed: str = "Receive address confirmed" address__public_key: str = "Public key" + address__qr_code: str = "QR code" address__title_cosigner: str = "Cosigner" address__title_receive_address: str = "Receive address" address__title_yours: str = "Yours" - address_details__derivation_path: str = "Derivation path:" + address_details__account_info: str = "Account info" + address_details__derivation_path: str = "Derivation path" + address_details__derivation_path_colon: str = "Derivation path:" address_details__title_receive_address: str = "Receive address" address_details__title_receiving_to: str = "Receiving to" authenticate__confirm_template: str = "Allow connected computer to confirm your {0} is genuine?" @@ -356,6 +362,7 @@ class TR: inputs__return: str = "RETURN" inputs__show: str = "SHOW" inputs__space: str = "SPACE" + instructions__continue_in_app: str = "Continue in the app" instructions__hold_to_confirm: str = "Hold to confirm" instructions__swipe_up: str = "Swipe up" instructions__tap_to_confirm: str = "Tap to confirm" @@ -828,6 +835,7 @@ class TR: words__array_of: str = "Array of" words__blockhash: str = "Blockhash" words__buying: str = "Buying" + words__cancel_and_exit: str = "Cancel and exit" words__confirm: str = "Confirm" words__confirm_fee: str = "Confirm fee" words__contains: str = "Contains" diff --git a/core/src/apps/bitcoin/get_address.py b/core/src/apps/bitcoin/get_address.py index 51faec4376..9bbd8e845e 100644 --- a/core/src/apps/bitcoin/get_address.py +++ b/core/src/apps/bitcoin/get_address.py @@ -31,10 +31,9 @@ def _get_xpubs( @with_keychain async def get_address(msg: GetAddress, keychain: Keychain, coin: CoinInfo) -> Address: - from trezor import TR from trezor.enums import InputScriptType from trezor.messages import Address - from trezor.ui.layouts import show_address, show_warning + from trezor.ui.layouts import confirm_multisig_warning, show_address from apps.common.address_mac import get_address_mac from apps.common.paths import address_n_to_str, validate_path @@ -104,11 +103,7 @@ async def get_address(msg: GetAddress, keychain: Keychain, coin: CoinInfo) -> Ad pubnodes = [hd.node for hd in multisig.pubkeys] multisig_index = multisig_pubkey_index(multisig, node.public_key()) - await show_warning( - "warning_multisig", - TR.send__receiving_to_multisig, - TR.words__continue_anyway, - ) + await confirm_multisig_warning() await show_address( address_short, diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index f1a3d1cc46..57e799f7cc 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -279,17 +279,6 @@ async def confirm_action( ) -async def flow_demo() -> None: - await raise_if_not_confirmed( - interact( - RustLayout(trezorui2.flow_get_address()), - "get_address", - BR_TYPE_OTHER, - ), - ActionCancelled, - ) - - async def confirm_single( br_type: str, title: str, @@ -362,7 +351,7 @@ async def confirm_path_warning( path: str, path_type: str | None = None, ) -> None: - title = ( + description = ( TR.addr_mismatch__wrong_derivation_path if not path_type else f"{TR.words__unknown} {path_type.lower()}." @@ -370,11 +359,8 @@ async def confirm_path_warning( await raise_if_not_confirmed( interact( RustLayout( - trezorui2.show_warning( - title=title, - value=path, - description=TR.words__continue_anyway, - button=TR.buttons__continue, + trezorui2.flow_warning_hi_prio( + title=f"{TR.words__warning}!", description=description, value=path ) ), "path_warning", @@ -383,6 +369,21 @@ async def confirm_path_warning( ) +async def confirm_multisig_warning() -> None: + await raise_if_not_confirmed( + interact( + RustLayout( + trezorui2.flow_warning_hi_prio( + title=f"{TR.words__important}!", + description=TR.send__receiving_to_multisig, + ) + ), + "warning_multisig", + br_code=ButtonRequestType.Warning, + ) + ) + + async def confirm_homescreen( image: bytes, ) -> None: @@ -417,78 +418,35 @@ async def show_address( br_code: ButtonRequestType = ButtonRequestType.Address, chunkify: bool = False, ) -> None: - mismatch_title = mismatch_title or TR.addr_mismatch__mismatch # def_arg - send_button_request = True + def xpub_title(i: int) -> str: + result = f"Multisig XPUB #{i + 1}\n" + result += ( + f"({TR.address__title_yours.lower()})" + if i == multisig_index + else f"({TR.address__title_cosigner.lower()})" + ) + return result - if title is None: - title = TR.address__title_receive_address - if multisig_index is not None: - title = f"{title}\n(MULTISIG)" - details_title = TR.send__title_receiving_to - elif details_title is None: - details_title = title - - layout = RustLayout( - trezorui2.confirm_address( - title=title, - data=address, - description=network or "", - extra=None, - chunkify=chunkify, + await raise_if_not_confirmed( + interact( + RustLayout( + trezorui2.flow_get_address( + address=address, + description=network or "", + extra=None, + chunkify=chunkify, + address_qr=address if address_qr is None else address_qr, + case_sensitive=case_sensitive, + account=account, + path=path, + xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], + ) + ), + br_type, + br_code, ) ) - while True: - if send_button_request: - send_button_request = False - await button_request( - br_type, - br_code, - pages=layout.page_count(), - ) - layout.request_complete_repaint() - result = await ctx_wait(layout) - - # User pressed right button. - if result is CONFIRMED: - break - - # User pressed corner button or swiped left, go to address details. - elif result is INFO: - - def xpub_title(i: int) -> str: - result = f"MULTISIG XPUB #{i + 1}\n" - result += ( - f"({TR.address__title_yours})" - if i == multisig_index - else f"({TR.address__title_cosigner})" - ) - return result - - result = await ctx_wait( - RustLayout( - trezorui2.show_address_details( - qr_title=title, - address=address if address_qr is None else address_qr, - case_sensitive=case_sensitive, - details_title=details_title, - account=account, - path=path, - xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], - ) - ) - ) - assert result is CANCELLED - - else: - result = await ctx_wait( - RustLayout(trezorui2.show_mismatch(title=mismatch_title)) - ) - assert result in (CONFIRMED, CANCELLED) - # Right button aborts action, left goes back to showing address. - if result is CONFIRMED: - raise ActionCancelled - def show_pubkey( pubkey: str, @@ -1305,7 +1263,7 @@ async def confirm_signverify( items: list[tuple[str, str]] = [] if account is not None: - items.append((f"{TR.words__account}:", account)) + items.append((TR.words__account, account)) if path is not None: items.append((TR.address_details__derivation_path, path)) items.append( diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 0fa4a02a8c..c3de1166d1 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -492,6 +492,14 @@ def confirm_path_warning( ) +def confirm_multisig_warning() -> Awaitable[None]: + return show_warning( + "warning_multisig", + TR.send__receiving_to_multisig, + TR.words__continue_anyway, + ) + + def confirm_homescreen(image: bytes) -> Awaitable[None]: return raise_if_not_confirmed( interact( diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index 72b69cf65e..0735cffcd9 100644 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ b/core/src/trezor/ui/layouts/tt/__init__.py @@ -431,6 +431,14 @@ def confirm_path_warning(path: str, path_type: str | None = None) -> Awaitable[N ) +def confirm_multisig_warning() -> Awaitable[None]: + return show_warning( + "warning_multisig", + TR.send__receiving_to_multisig, + TR.words__continue_anyway, + ) + + def confirm_homescreen(image: bytes) -> Awaitable[None]: return raise_if_not_confirmed( interact( @@ -1349,7 +1357,7 @@ async def confirm_signverify( if account is not None: items.append((f"{TR.words__account}:", account)) if path is not None: - items.append((TR.address_details__derivation_path, path)) + items.append((TR.address_details__derivation_path_colon, path)) items.append( ( TR.sign_message__message_size, diff --git a/core/translations/cs.json b/core/translations/cs.json index ed18e1b322..5e0605513b 100644 --- a/core/translations/cs.json +++ b/core/translations/cs.json @@ -46,7 +46,8 @@ "address__title_cosigner": "Další podepisující", "address__title_receive_address": "Přijímací adresa", "address__title_yours": "Vaše", - "address_details__derivation_path": "Derivační cesta:", + "address_details__derivation_path": "Derivační cesta", + "address_details__derivation_path_colon": "Derivační cesta:", "address_details__title_receive_address": "Přijímací adresa", "address_details__title_receiving_to": "Příjem", "authenticate__confirm_template": "Povolit připojenému počítači potvrdit pravost vašeho {0}?", diff --git a/core/translations/de.json b/core/translations/de.json index 691ad4fba3..6fca28d28f 100644 --- a/core/translations/de.json +++ b/core/translations/de.json @@ -46,7 +46,8 @@ "address__title_cosigner": "Mitunterzeich.", "address__title_receive_address": "Empfäng-adresse", "address__title_yours": "Deiner", - "address_details__derivation_path": "Ableitungspfad:", + "address_details__derivation_path": "Ableitungspfad", + "address_details__derivation_path_colon": "Ableitungspfad:", "address_details__title_receive_address": "Empfäng-adresse", "address_details__title_receiving_to": "Empfänger", "authenticate__confirm_template": "Darf verbundener Computer bestätigen, dass {0} echt ist?", diff --git a/core/translations/en.json b/core/translations/en.json index cd3723fba5..789a97606b 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -10,11 +10,17 @@ "addr_mismatch__support_url": "trezor.io/support", "addr_mismatch__wrong_derivation_path": "Wrong derivation path for selected account.", "addr_mismatch__xpub_mismatch": "XPUB mismatch?", + "address__cancel_contact_support": "If receive address doesn't match, contact Trezor Support at trezor.io/support.", + "address__cancel_receive": "Cancel receive", + "address__confirmed": "Receive address confirmed", "address__public_key": "Public key", + "address__qr_code": "QR code", "address__title_cosigner": "Cosigner", "address__title_receive_address": "Receive address", "address__title_yours": "Yours", - "address_details__derivation_path": "Derivation path:", + "address_details__derivation_path": "Derivation path", + "address_details__derivation_path_colon": "Derivation path:", + "address_details__account_info": "Account info", "address_details__title_receive_address": "Receive address", "address_details__title_receiving_to": "Receiving to", "authenticate__confirm_template": "Allow connected computer to confirm your {0} is genuine?", @@ -358,6 +364,7 @@ "inputs__previous": "PREVIOUS", "inputs__show": "SHOW", "inputs__space": "SPACE", + "instructions__continue_in_app": "Continue in the app", "instructions__swipe_up": "Swipe up", "instructions__tap_to_confirm": "Tap to confirm", "instructions__hold_to_confirm": "Hold to confirm", @@ -830,6 +837,7 @@ "words__array_of": "Array of", "words__blockhash": "Blockhash", "words__buying": "Buying", + "words__cancel_and_exit": "Cancel and exit", "words__confirm": "Confirm", "words__confirm_fee": "Confirm fee", "words__contains": "Contains", diff --git a/core/translations/es.json b/core/translations/es.json index 8ab1f422ea..4f57043244 100644 --- a/core/translations/es.json +++ b/core/translations/es.json @@ -46,7 +46,8 @@ "address__title_cosigner": "Cofirmante", "address__title_receive_address": "Dirección destino", "address__title_yours": "Tuyo", - "address_details__derivation_path": "Ruta de derivación:", + "address_details__derivation_path": "Ruta de derivación", + "address_details__derivation_path_colon": "Ruta de derivación:", "address_details__title_receive_address": "Dirección destino", "address_details__title_receiving_to": "Recibir en", "authenticate__confirm_template": "¿Confirmar con el ordenador conectado que tu {0} es original?", diff --git a/core/translations/fr.json b/core/translations/fr.json index e4acd9b3fc..2a33588dfc 100644 --- a/core/translations/fr.json +++ b/core/translations/fr.json @@ -46,7 +46,8 @@ "address__title_cosigner": "Cosignataire", "address__title_receive_address": "Adr. de récep.", "address__title_yours": "La vôtre", - "address_details__derivation_path": "Chemin de dérivation :", + "address_details__derivation_path": "Chemin de dérivation", + "address_details__derivation_path_colon": "Chemin de dérivation :", "address_details__title_receive_address": "Adr. de récep.", "address_details__title_receiving_to": "Récep. sur", "authenticate__confirm_template": "Autoriser l'ord. connecté à conf. que votre {0} est auth. ?", diff --git a/core/translations/order.json b/core/translations/order.json index 95885a2668..107719622e 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -9,7 +9,7 @@ "7": "address__title_cosigner", "8": "address__title_receive_address", "9": "address__title_yours", - "10": "address_details__derivation_path", + "10": "address_details__derivation_path_colon", "11": "address_details__title_receive_address", "12": "address_details__title_receiving_to", "13": "authenticate__confirm_template", @@ -860,5 +860,13 @@ "858": "backup__create_backup_to_prevent_loss", "859": "reset__check_backup_instructions", "860": "words__instructions", - "861": "words__not_recommended" + "861": "words__not_recommended", + "862": "address_details__account_info", + "863": "address__cancel_contact_support", + "864": "address__cancel_receive", + "865": "address__qr_code", + "866": "address_details__derivation_path", + "867": "instructions__continue_in_app", + "868": "words__cancel_and_exit", + "869": "address__confirmed" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 84db9e0693..ea42b34d37 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "5261bdad43d4e3cda9263ec6ce080218a0d897e02307ca8cafa2525d6a8d3d6b", - "datetime": "2024-05-17T10:18:14.246905", - "commit": "58db509b926fd99b3a36eb3bd550945bf1a139c4" + "merkle_root": "d2a00bb90ebc87448eb0786432129db7c4e67316de7c491bf854d8429d2db9b8", + "datetime": "2024-05-17T10:29:37.039056", + "commit": "b3379e14e0658ab2327bffdfff5227f6079c8f74" }, "history": [ { diff --git a/tests/input_flows.py b/tests/input_flows.py index 828c145e4f..d683d25223 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -434,7 +434,7 @@ class InputFlowShowMultisigXPUBs(InputFlowBase): layout = self.debug.swipe_left(wait=True) # address details assert "Multisig 2 of 3" in layout.screen_content() - TR.assert_in(layout.screen_content(), "address_details__derivation_path") + TR.assert_in(layout.screen_content(), "address_details__derivation_path_colon") # Three xpub pages with the same testing logic for xpub_num in range(3): @@ -508,7 +508,7 @@ class InputFlowShowMultisigXPUBs(InputFlowBase): layout = self.debug.swipe_left(wait=True) # address details assert "Multisig 2 of 3" in layout.screen_content() - TR.assert_in(layout.screen_content(), "address_details__derivation_path") + TR.assert_in(layout.screen_content(), "address_details__derivation_path_colon") # Three xpub pages with the same testing logic for xpub_num in range(3):