diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs b/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs new file mode 100644 index 0000000000..d5113c479c --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/flow/continue_recovery_homepage.rs @@ -0,0 +1,285 @@ +use crate::{ + error, + strutil::TString, + translations::TR, + ui::{ + button_request::{ButtonRequest, ButtonRequestCode}, + component::{ + button_request::ButtonRequestExt, + text::paragraphs::{ + Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, VecExt, + }, + ComponentExt, + }, + flow::{ + base::{Decision, DecisionBuilder as _}, + FlowController, FlowMsg, SwipeFlow, + }, + geometry::{Direction, LinearPlacement}, + layout::util::RecoveryType, + }, +}; + +use super::super::{ + component::Button, + firmware::{ + ActionBar, Header, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen, + VerticalMenuScreenMsg, + }, + theme, +}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ContinueRecoveryBeforeShares { + Main, + Menu, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ContinueRecoveryBetweenShares { + Main, + Menu, + Cancel, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ContinueRecoveryBetweenSharesAdvanced { + Main, + Menu, + Cancel, + RemainingShares, +} + +impl FlowController for ContinueRecoveryBeforeShares { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, _direction: Direction) -> Decision { + self.do_nothing() + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Main, FlowMsg::Info) => Self::Menu.goto(), + (Self::Main, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed), + (Self::Menu, FlowMsg::Cancelled) => Self::Main.goto(), + (Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled), + _ => self.do_nothing(), + } + } +} + +impl FlowController for ContinueRecoveryBetweenShares { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, _direction: Direction) -> Decision { + self.do_nothing() + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Main, FlowMsg::Info) => Self::Menu.goto(), + (Self::Main, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed), + (Self::Menu, FlowMsg::Choice(0)) => Self::Cancel.goto(), + (Self::Menu, FlowMsg::Cancelled) => Self::Main.goto(), + (Self::Cancel, FlowMsg::Cancelled) => Self::Menu.goto(), + (Self::Cancel, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Cancelled), + _ => self.do_nothing(), + } + } +} + +impl FlowController for ContinueRecoveryBetweenSharesAdvanced { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, _direction: Direction) -> Decision { + self.do_nothing() + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Main, FlowMsg::Info) => Self::Menu.goto(), + (Self::Main, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed), + (Self::Menu, FlowMsg::Choice(0)) => Self::RemainingShares.goto(), + (Self::Menu, FlowMsg::Choice(1)) => Self::Cancel.goto(), + (Self::Menu, FlowMsg::Cancelled) => Self::Main.goto(), + (Self::Cancel, FlowMsg::Cancelled) => Self::Menu.goto(), + (Self::Cancel, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Cancelled), + (Self::RemainingShares, FlowMsg::Cancelled) => Self::Menu.goto(), + _ => self.do_nothing(), + } + } +} + +pub fn new_continue_recovery_homepage( + text: TString<'static>, + subtext: Option>, + recovery_type: RecoveryType, + show_instructions: bool, // 1st screen of the recovery process + pages: Option>, +) -> Result { + let (header, confirm_btn, cancel_btn, cancel_title, cancel_intro) = match recovery_type { + RecoveryType::Normal if show_instructions => ( + Header::new(TR::recovery__title.into()).with_menu_button(), + TR::buttons__continue, + TR::recovery__title_cancel_recovery, + TR::recovery__title_cancel_recovery, + TR::recovery__wanna_cancel_recovery, + ), + RecoveryType::Normal => ( + Header::new(TR::words__title_done.into()) + .with_text_style(theme::label_title_confirm()) + .with_icon(theme::ICON_DONE, theme::GREEN_LIGHT) + .with_menu_button(), + TR::instructions__enter_next_share, + TR::recovery__title_cancel_recovery, + TR::recovery__title_cancel_recovery, + TR::recovery__wanna_cancel_recovery, + ), + _ => ( + Header::new(TR::recovery__title_dry_run.into()).with_menu_button(), + TR::buttons__continue, + TR::recovery__cancel_dry_run, + TR::recovery__title_cancel_dry_run, + TR::recovery__wanna_cancel_dry_run, + ), + }; + + let mut pars_main = ParagraphVecShort::new(); + if show_instructions { + pars_main.add(Paragraph::new( + &theme::TEXT_REGULAR, + TR::recovery__enter_each_word, + )); + } else { + pars_main.add(Paragraph::new(&theme::TEXT_REGULAR, text)); + if let Some(sub) = subtext { + pars_main.add(Paragraph::new(&theme::TEXT_REGULAR, sub)); + } + }; + + let content_main = TextScreen::new( + pars_main + .into_paragraphs() + .with_placement(LinearPlacement::vertical()), + ) + .with_header(header) + .with_action_bar(ActionBar::new_single(Button::with_text(confirm_btn.into()))) + .repeated_button_request(ButtonRequest::new( + ButtonRequestCode::RecoveryHomepage, + "recovery".into(), + )) + .map(|msg| match msg { + TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), + TextScreenMsg::Menu => Some(FlowMsg::Info), + _ => None, + }); + + let paragraphs_cancel = ParagraphVecShort::from_iter([ + Paragraph::new(&theme::TEXT_REGULAR, cancel_intro).with_bottom_padding(17), + Paragraph::new(&theme::TEXT_REGULAR, TR::recovery__progress_will_be_lost), + ]) + .into_paragraphs() + .with_placement(LinearPlacement::vertical()); + + let content_cancel = TextScreen::new(paragraphs_cancel) + .with_header(Header::new(cancel_title.into()).with_close_button()) + .map(|msg| match msg { + TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed), + TextScreenMsg::Cancelled | TextScreenMsg::Menu => Some(FlowMsg::Cancelled), + }) + .repeated_button_request(ButtonRequest::new( + ButtonRequestCode::ProtectCall, + "abort_recovery".into(), + )); + + let res = if show_instructions { + let content_menu = VerticalMenuScreen::new( + VerticalMenu::empty() + .item(Button::with_text(cancel_btn.into()).styled(theme::menu_item_title_orange())), + ) + .with_header(Header::new(TString::empty()).with_close_button()) + .map(|msg| match msg { + VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)), + VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled), + _ => None, + }); + + let mut res = SwipeFlow::new(&ContinueRecoveryBeforeShares::Main)?; + res.add_page(&ContinueRecoveryBeforeShares::Main, content_main)? + .add_page(&ContinueRecoveryBeforeShares::Menu, content_menu)?; + res + } else if pages.is_none() { + let content_menu = VerticalMenuScreen::new( + VerticalMenu::empty() + .item(Button::with_text(cancel_btn.into()).styled(theme::menu_item_title_orange())), + ) + .with_header(Header::new(TString::empty()).with_close_button()) + .map(|msg| match msg { + VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)), + VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled), + _ => None, + }); + + let mut res = SwipeFlow::new(&ContinueRecoveryBetweenShares::Main)?; + res.add_page(&ContinueRecoveryBetweenShares::Main, content_main)? + .add_page(&ContinueRecoveryBetweenShares::Menu, content_menu)? + .add_page(&ContinueRecoveryBetweenShares::Cancel, content_cancel)?; + res + } else { + let content_menu = VerticalMenuScreen::new( + VerticalMenu::empty() + .item(Button::with_text( + TR::recovery__title_remaining_shares.into(), + )) + .item(Button::with_text(cancel_btn.into()).styled(theme::menu_item_title_orange())), + ) + .with_header(Header::new(TString::empty()).with_close_button()) + .map(|msg| match msg { + VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)), + VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled), + _ => None, + }); + + let n_remaining_shares = pages.as_ref().unwrap().len() / 2; + let content_remaining_shares = TextScreen::new( + pages + .unwrap() + .into_paragraphs() + .with_placement(LinearPlacement::vertical()), + ) + .with_header(Header::new(TR::recovery__title_remaining_shares.into()).with_close_button()) + .map(|msg| match msg { + TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled), + TextScreenMsg::Menu => Some(FlowMsg::Info), + _ => None, + }) + .repeated_button_request(ButtonRequest::new( + ButtonRequestCode::Other, + "show_shares".into(), + )) + .with_pages(move |_| n_remaining_shares); + + let mut res = SwipeFlow::new(&ContinueRecoveryBetweenSharesAdvanced::Main)?; + res.add_page(&ContinueRecoveryBetweenSharesAdvanced::Main, content_main)? + .add_page(&ContinueRecoveryBetweenSharesAdvanced::Menu, content_menu)? + .add_page( + &ContinueRecoveryBetweenSharesAdvanced::Cancel, + content_cancel, + )? + .add_page( + &ContinueRecoveryBetweenSharesAdvanced::RemainingShares, + content_remaining_shares, + )?; + res + }; + Ok(res) +} diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs index 8172bf6978..5c0785458d 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs @@ -1,5 +1,6 @@ pub mod confirm_reset; pub mod confirm_set_new_pin; +pub mod continue_recovery_homepage; pub mod get_address; pub mod prompt_backup; pub mod request_passphrase; @@ -8,6 +9,7 @@ pub mod show_share_words; pub use confirm_reset::new_confirm_reset; pub use confirm_set_new_pin::new_set_new_pin; +pub use continue_recovery_homepage::new_continue_recovery_homepage; pub use get_address::GetAddress; pub use prompt_backup::PromptBackup; pub use request_passphrase::RequestPassphrase; diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 6ac24e7cd3..c2c3505dc1 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -11,7 +11,8 @@ use crate::{ text::{ op::OpTextLayout, paragraphs::{ - Checklist, Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt, + Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, + Paragraphs, VecExt, }, }, Empty, FormattedText, @@ -326,14 +327,33 @@ impl FirmwareUI for UIEckhart { } fn continue_recovery_homepage( - _text: TString<'static>, - _subtext: Option>, + text: TString<'static>, + subtext: Option>, _button: Option>, - _recovery_type: RecoveryType, - _show_instructions: bool, - _remaining_shares: Option, + recovery_type: RecoveryType, + show_instructions: bool, + remaining_shares: Option, ) -> Result, Error> { - Err::, Error>(Error::ValueError(c"not implemented")) + let pages_vec = if let Some(pages_obj) = remaining_shares { + let mut vec = ParagraphVecLong::new(); + for page in IterBuf::new().try_iterate(pages_obj)? { + let [title, description]: [TString; 2] = util::iter_into_array(page)?; + vec.add(Paragraph::new(&theme::TEXT_REGULAR, title)) + .add(Paragraph::new(&theme::TEXT_MONO_LIGHT, description).break_after()); + } + Some(vec) + } else { + None + }; + + let flow = flow::continue_recovery_homepage::new_continue_recovery_homepage( + text, + subtext, + recovery_type, + show_instructions, + pages_vec, + )?; + LayoutObj::new_root(flow) } fn flow_confirm_output( diff --git a/core/src/trezor/ui/layouts/eckhart/recovery.py b/core/src/trezor/ui/layouts/eckhart/recovery.py index 1f2ddfa267..410ff53a7d 100644 --- a/core/src/trezor/ui/layouts/eckhart/recovery.py +++ b/core/src/trezor/ui/layouts/eckhart/recovery.py @@ -99,7 +99,7 @@ async def show_group_share_success(share_index: int, group_index: int) -> None: async def continue_recovery( - _button_label: str, # unused on delizia + _button_label: str, # unused on eckhart text: str, subtext: str | None, recovery_type: RecoveryType,