1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-05 09:55:44 +00:00

feat(eckahrt): continue recovery flow

This commit is contained in:
Lukas Bielesch 2025-03-13 14:02:05 +01:00
parent 2a22ce3ca3
commit beab435a81
4 changed files with 315 additions and 8 deletions

View File

@ -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<TString<'static>>,
recovery_type: RecoveryType,
show_instructions: bool, // 1st screen of the recovery process
pages: Option<ParagraphVecLong<'static>>,
) -> Result<SwipeFlow, error::Error> {
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)
}

View File

@ -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;

View File

@ -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<TString<'static>>,
text: TString<'static>,
subtext: Option<TString<'static>>,
_button: Option<TString<'static>>,
_recovery_type: RecoveryType,
_show_instructions: bool,
_remaining_shares: Option<Obj>,
recovery_type: RecoveryType,
show_instructions: bool,
remaining_shares: Option<Obj>,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, 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(

View File

@ -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,