1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-21 09:39:02 +00:00

feat(eckahrt): continue recovery flow

This commit is contained in:
Lukas Bielesch 2025-03-13 14:02:05 +01:00 committed by obrusvit
parent a99aa3144f
commit 60b453f4f0
9 changed files with 412 additions and 13 deletions

View File

@ -536,6 +536,8 @@ static void _librust_qstrs(void) {
MP_QSTR_reset__number_of_shares_info;
MP_QSTR_reset__one_share;
MP_QSTR_reset__only_one_share_will_be_created;
MP_QSTR_reset__recovery_share_description;
MP_QSTR_reset__recovery_share_number;
MP_QSTR_reset__recovery_share_title_template;
MP_QSTR_reset__recovery_wallet_backup_title;
MP_QSTR_reset__repeat_for_all_shares;
@ -797,6 +799,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__really_wanna;
MP_QSTR_words__receive;
MP_QSTR_words__recipient;
MP_QSTR_words__recovery_share;
MP_QSTR_words__settings;
MP_QSTR_words__sign;
MP_QSTR_words__signer;

View File

@ -806,14 +806,14 @@ pub enum TranslatedString {
recovery__title_recover = 522, // "Recover wallet"
recovery__title_remaining_shares = 523, // "Remaining shares"
recovery__type_word_x_of_y_template = 524, // "Type word {0} of {1}"
recovery__wallet_recovered = 525, // "Wallet recovery completed"
recovery__wallet_recovered = 525, // {"Bolt": "Wallet recovery completed", "Caesar": "Wallet recovery completed", "Delizia": "Wallet recovery completed", "Eckhart": "Wallet recovery completed."}
recovery__wanna_cancel_dry_run = 526, // "Are you sure you want to cancel the backup check?"
recovery__wanna_cancel_recovery = 527, // "Are you sure you want to cancel the recovery process?"
recovery__word_count_template = 528, // "({0} words)"
recovery__word_x_of_y_template = 529, // "Word {0} of {1}"
recovery__x_more_items_starting_template_plural = 530, // "{count} more {plural} starting"
recovery__x_more_shares_needed_template_plural = 531, // "{count} more {plural} needed"
recovery__x_of_y_entered_template = 532, // "{0} of {1} shares entered"
recovery__x_of_y_entered_template = 532, // {"Bolt": "{0} of {1} shares entered", "Caesar": "{0} of {1} shares entered", "Delizia": "{0} of {1} shares entered", "Eckhart": "{0} of {1} shares entered."}
recovery__you_have_entered = 533, // "You have entered"
reset__advanced_group_threshold_info = 534, // "The group threshold specifies the number of groups required to recover your wallet."
reset__all_x_of_y_template = 535, // "all {0} of {1} shares"
@ -1412,6 +1412,9 @@ pub enum TranslatedString {
words__pay_attention = 998, // "Pay attention"
address__check_with_source = 999, // "Check the address with source."
words__receive = 1000, // "Receive"
reset__recovery_share_description = 1001, // "A recovery share is a list of words you wrote down when setting up your Trezor."
reset__recovery_share_number = 1002, // "Your wallet backup consists of 1 to 16 shares."
words__recovery_share = 1003, // "Recovery share"
}
impl TranslatedString {
@ -2221,14 +2224,28 @@ impl TranslatedString {
Self::recovery__title_recover => "Recover wallet",
Self::recovery__title_remaining_shares => "Remaining shares",
Self::recovery__type_word_x_of_y_template => "Type word {0} of {1}",
#[cfg(feature = "layout_bolt")]
Self::recovery__wallet_recovered => "Wallet recovery completed",
#[cfg(feature = "layout_caesar")]
Self::recovery__wallet_recovered => "Wallet recovery completed",
#[cfg(feature = "layout_delizia")]
Self::recovery__wallet_recovered => "Wallet recovery completed",
#[cfg(feature = "layout_eckhart")]
Self::recovery__wallet_recovered => "Wallet recovery completed.",
Self::recovery__wanna_cancel_dry_run => "Are you sure you want to cancel the backup check?",
Self::recovery__wanna_cancel_recovery => "Are you sure you want to cancel the recovery process?",
Self::recovery__word_count_template => "({0} words)",
Self::recovery__word_x_of_y_template => "Word {0} of {1}",
Self::recovery__x_more_items_starting_template_plural => "{count} more {plural} starting",
Self::recovery__x_more_shares_needed_template_plural => "{count} more {plural} needed",
#[cfg(feature = "layout_bolt")]
Self::recovery__x_of_y_entered_template => "{0} of {1} shares entered",
#[cfg(feature = "layout_caesar")]
Self::recovery__x_of_y_entered_template => "{0} of {1} shares entered",
#[cfg(feature = "layout_delizia")]
Self::recovery__x_of_y_entered_template => "{0} of {1} shares entered",
#[cfg(feature = "layout_eckhart")]
Self::recovery__x_of_y_entered_template => "{0} of {1} shares entered.",
Self::recovery__you_have_entered => "You have entered",
Self::reset__advanced_group_threshold_info => "The group threshold specifies the number of groups required to recover your wallet.",
Self::reset__all_x_of_y_template => "all {0} of {1} shares",
@ -2904,6 +2921,9 @@ impl TranslatedString {
Self::words__pay_attention => "Pay attention",
Self::address__check_with_source => "Check the address with source.",
Self::words__receive => "Receive",
Self::reset__recovery_share_description => "A recovery share is a list of words you wrote down when setting up your Trezor.",
Self::reset__recovery_share_number => "Your wallet backup consists of 1 to 16 shares.",
Self::words__recovery_share => "Recovery share",
}
}
@ -4311,6 +4331,9 @@ impl TranslatedString {
Qstr::MP_QSTR_words__pay_attention => Some(Self::words__pay_attention),
Qstr::MP_QSTR_address__check_with_source => Some(Self::address__check_with_source),
Qstr::MP_QSTR_words__receive => Some(Self::words__receive),
Qstr::MP_QSTR_reset__recovery_share_description => Some(Self::reset__recovery_share_description),
Qstr::MP_QSTR_reset__recovery_share_number => Some(Self::reset__recovery_share_number),
Qstr::MP_QSTR_words__recovery_share => Some(Self::words__recovery_share),
_ => None,
}
}

View File

@ -0,0 +1,332 @@
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::{Alignment, Direction, LinearPlacement, Offset},
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,
RecoveryShare,
}
#[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::RecoveryShare.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::RecoveryShare, _) => Self::Menu.goto(),
_ => 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, _) => 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,
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,
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_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).with_top_padding(10));
}
};
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 = Paragraph::new(&theme::TEXT_REGULAR, cancel_intro)
.into_paragraphs()
.with_placement(LinearPlacement::vertical());
let content_cancel = TextScreen::new(paragraphs_cancel)
.with_header(Header::new(cancel_title.into()))
.with_action_bar(ActionBar::new_double(
Button::with_icon(theme::ICON_CHEVRON_LEFT),
Button::with_text(TR::buttons__cancel.into()).styled(theme::button_cancel()),
))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
_ => None,
})
.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_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
),
)
.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_and_subtext(
TR::words__recovery_share.into(),
TR::buttons__more_info.into(),
)
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
)
.item(
Button::with_text(cancel_btn.into())
.styled(theme::menu_item_title_orange())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
),
)
.with_header(Header::new(TR::recovery__title.into()).with_close_button())
.map(|msg| match msg {
VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)),
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
_ => None,
});
let paragraphs_recovery_share = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_REGULAR, TR::reset__recovery_share_description)
.with_bottom_padding(10),
Paragraph::new(&theme::TEXT_REGULAR, TR::reset__recovery_share_number),
])
.into_paragraphs()
.with_placement(LinearPlacement::vertical());
let content_recovery_share = TextScreen::new(paragraphs_recovery_share)
.with_header(Header::new(TR::words__recovery_share.into()).with_close_button())
.map(|_| Some(FlowMsg::Cancelled))
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::Other,
"recovery_share".into(),
));
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)?
.add_page(
&ContinueRecoveryBetweenShares::RecoveryShare,
content_recovery_share,
)?;
res
} else {
let content_menu = VerticalMenuScreen::new(
VerticalMenu::empty()
.item(
Button::with_text(TR::recovery__title_remaining_shares.into())
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
)
.item(
Button::with_text(cancel_btn.into())
.styled(theme::menu_item_title_orange())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
),
)
.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(|_| Some(FlowMsg::Cancelled))
.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

@ -659,6 +659,8 @@ class TR:
reset__number_of_shares_info: str = "= total number of unique word lists used for wallet backup."
reset__one_share: str = "1 share"
reset__only_one_share_will_be_created: str = "Only one share will be created."
reset__recovery_share_description: str = "A recovery share is a list of words you wrote down when setting up your Trezor."
reset__recovery_share_number: str = "Your wallet backup consists of 1 to 16 shares."
reset__recovery_share_title_template: str = "Recovery share #{0}"
reset__recovery_wallet_backup_title: str = "Wallet backup"
reset__repeat_for_all_shares: str = "Repeat for all shares."
@ -974,6 +976,7 @@ class TR:
words__really_wanna: str = "Do you really want to"
words__receive: str = "Receive"
words__recipient: str = "Recipient"
words__recovery_share: str = "Recovery share"
words__settings: str = "Settings"
words__sign: str = "Sign"
words__signer: str = "Signer"

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,

View File

@ -638,14 +638,24 @@
"recovery__type_word_x_of_y_template": "Type word {0} of {1}",
"recovery__unlock_repeated_backup": "Create additional backup?",
"recovery__unlock_repeated_backup_verb": "Create backup",
"recovery__wallet_recovered": "Wallet recovery completed",
"recovery__wallet_recovered": {
"Bolt": "Wallet recovery completed",
"Caesar": "Wallet recovery completed",
"Delizia": "Wallet recovery completed",
"Eckhart": "Wallet recovery completed."
},
"recovery__wanna_cancel_dry_run": "Are you sure you want to cancel the backup check?",
"recovery__wanna_cancel_recovery": "Are you sure you want to cancel the recovery process?",
"recovery__word_count_template": "({0} words)",
"recovery__word_x_of_y_template": "Word {0} of {1}",
"recovery__x_more_items_starting_template_plural": "{count} more {plural} starting",
"recovery__x_more_shares_needed_template_plural": "{count} more {plural} needed",
"recovery__x_of_y_entered_template": "{0} of {1} shares entered",
"recovery__x_of_y_entered_template": {
"Bolt": "{0} of {1} shares entered",
"Caesar": "{0} of {1} shares entered",
"Delizia": "{0} of {1} shares entered",
"Eckhart": "{0} of {1} shares entered."
},
"recovery__you_have_entered": "You have entered",
"reset__advanced_group_threshold_info": "The group threshold specifies the number of groups required to recover your wallet.",
"reset__all_x_of_y_template": "all {0} of {1} shares",
@ -697,6 +707,8 @@
"reset__one_share": "1 share",
"reset__only_one_share_will_be_created": "Only one share will be created.",
"reset__recovery_share_title_template": "Recovery share #{0}",
"reset__recovery_share_description": "A recovery share is a list of words you wrote down when setting up your Trezor.",
"reset__recovery_share_number": "Your wallet backup consists of 1 to 16 shares.",
"reset__recovery_wallet_backup_title": "Wallet backup",
"reset__repeat_for_all_shares": "Repeat for all shares.",
"reset__required_number_of_groups": "The required number of groups for recovery.",
@ -1031,6 +1043,7 @@
"words__really_wanna": "Do you really want to",
"words__receive": "Receive",
"words__recipient": "Recipient",
"words__recovery_share": "Recovery share",
"words__settings": "Settings",
"words__sign": "Sign",
"words__signer": "Signer",

View File

@ -999,5 +999,8 @@
"997": "backup__not_recommend",
"998": "words__pay_attention",
"999": "address__check_with_source",
"1000": "words__receive"
"1000": "words__receive",
"1001": "reset__recovery_share_description",
"1002": "reset__recovery_share_number",
"1003": "words__recovery_share"
}