Skip to content

Commit 4f30ef8

Browse files
committed
feat(core/ui): T3T1 instruction screens between shares
Changes the visual appearance of the screens between shares during multi-share (shamir) recovery. [no changelog]
1 parent 53799cd commit 4f30ef8

File tree

9 files changed

+256
-134
lines changed

9 files changed

+256
-134
lines changed

core/embed/rust/librust_qstr.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,13 @@ static void _librust_qstrs(void) {
239239
MP_QSTR_fingerprint;
240240
MP_QSTR_firmware_update__title;
241241
MP_QSTR_firmware_update__title_fingerprint;
242+
MP_QSTR_first_screen;
242243
MP_QSTR_flow_confirm_output;
243244
MP_QSTR_flow_confirm_reset_create;
244245
MP_QSTR_flow_confirm_reset_recover;
245246
MP_QSTR_flow_confirm_set_new_pin;
246247
MP_QSTR_flow_confirm_summary;
248+
MP_QSTR_flow_continue_recovery;
247249
MP_QSTR_flow_get_address;
248250
MP_QSTR_flow_prompt_backup;
249251
MP_QSTR_flow_request_number;
@@ -659,7 +661,9 @@ static void _librust_qstrs(void) {
659661
MP_QSTR_storage_msg__verifying_pin;
660662
MP_QSTR_storage_msg__wrong_pin;
661663
MP_QSTR_subprompt;
664+
MP_QSTR_subtext;
662665
MP_QSTR_subtitle;
666+
MP_QSTR_text;
663667
MP_QSTR_text_confirm;
664668
MP_QSTR_text_info;
665669
MP_QSTR_text_mono;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use crate::{
2+
error,
3+
micropython::{map::Map, obj::Obj, qstr::Qstr, util},
4+
strutil::TString,
5+
translations::TR,
6+
ui::{
7+
component::{
8+
swipe_detect::SwipeSettings,
9+
text::paragraphs::{Paragraph, ParagraphVecShort, Paragraphs, VecExt},
10+
ComponentExt, SwipeDirection,
11+
},
12+
flow::{
13+
base::{DecisionBuilder as _, StateChange},
14+
FlowMsg, FlowState, SwipeFlow,
15+
},
16+
layout::obj::LayoutObj,
17+
model_mercury::component::SwipeContent,
18+
},
19+
};
20+
21+
use super::super::{
22+
component::{Frame, FrameMsg, VerticalMenu, VerticalMenuChoiceMsg},
23+
theme,
24+
};
25+
26+
const RECOVERY_TYPE_DRY_RUN: u32 = 1;
27+
const RECOVERY_TYPE_UNLOCK_REPEATED_BACKUP: u32 = 2;
28+
29+
#[derive(Copy, Clone, PartialEq, Eq)]
30+
pub enum ContinueRecovery {
31+
Main,
32+
Menu,
33+
}
34+
35+
impl FlowState for ContinueRecovery {
36+
#[inline]
37+
fn index(&'static self) -> usize {
38+
*self as usize
39+
}
40+
41+
fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange {
42+
match (self, direction) {
43+
(Self::Main, SwipeDirection::Left) => Self::Menu.swipe(direction),
44+
(Self::Menu, SwipeDirection::Right) => Self::Main.swipe(direction),
45+
(Self::Main, SwipeDirection::Up) => self.return_msg(FlowMsg::Confirmed),
46+
_ => self.do_nothing(),
47+
}
48+
}
49+
50+
fn handle_event(&'static self, msg: FlowMsg) -> StateChange {
51+
match (self, msg) {
52+
(Self::Main, FlowMsg::Info) => Self::Menu.transit(),
53+
(Self::Menu, FlowMsg::Cancelled) => Self::Main.swipe_right(),
54+
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled),
55+
_ => self.do_nothing(),
56+
}
57+
}
58+
}
59+
60+
#[allow(clippy::not_unsafe_ptr_arg_deref)]
61+
pub extern "C" fn new_continue_recovery(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
62+
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ContinueRecovery::new_obj) }
63+
}
64+
65+
impl ContinueRecovery {
66+
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
67+
let first_screen: bool = kwargs.get(Qstr::MP_QSTR_first_screen)?.try_into()?;
68+
let recovery_type: u32 = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
69+
let text: TString = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?; // #shares entered
70+
let subtext: Option<TString> = kwargs.get(Qstr::MP_QSTR_subtext)?.try_into_option()?; // #shares remaining
71+
72+
let (title, cancel_btn) = match recovery_type {
73+
RECOVERY_TYPE_DRY_RUN => (
74+
TR::recovery__title_dry_run.into(),
75+
TR::recovery__cancel_dry_run.into(),
76+
),
77+
RECOVERY_TYPE_UNLOCK_REPEATED_BACKUP => (
78+
TR::recovery__title_dry_run.into(),
79+
TR::recovery__cancel_dry_run.into(),
80+
),
81+
_ => (
82+
TR::recovery__title.into(),
83+
TR::recovery__title_cancel_recovery.into(),
84+
),
85+
};
86+
87+
let mut pars = ParagraphVecShort::new();
88+
if first_screen {
89+
pars.add(Paragraph::new(
90+
&theme::TEXT_MAIN_GREY_EXTRA_LIGHT,
91+
TR::recovery__enter_each_word,
92+
));
93+
} else {
94+
pars.add(Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, text));
95+
if let Some(sub) = subtext {
96+
pars.add(Paragraph::new(&theme::TEXT_SUB_GREY, sub));
97+
}
98+
}
99+
100+
let (footer_instruction, footer_description) = if first_screen {
101+
(TR::instructions__swipe_up.into(), None)
102+
} else {
103+
(
104+
TR::instructions__swipe_up.into(),
105+
Some(TR::instructions__enter_next_share.into()),
106+
)
107+
};
108+
109+
let paragraphs = Paragraphs::new(pars);
110+
let content_main = Frame::left_aligned(title, SwipeContent::new(paragraphs))
111+
.with_subtitle(TR::words__instructions.into())
112+
.with_menu_button()
113+
.with_footer(footer_instruction, footer_description)
114+
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
115+
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
116+
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
117+
118+
let content_menu = Frame::left_aligned(
119+
TString::empty(),
120+
VerticalMenu::empty().danger(theme::ICON_CANCEL, cancel_btn),
121+
)
122+
.with_cancel_button()
123+
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
124+
.map(|msg| match msg {
125+
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
126+
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
127+
});
128+
129+
let res = SwipeFlow::new(&ContinueRecovery::Main)?
130+
.with_page(&ContinueRecovery::Main, content_main)?
131+
.with_page(&ContinueRecovery::Menu, content_menu)?;
132+
Ok(LayoutObj::new(res)?.into())
133+
}
134+
}

core/embed/rust/src/ui/model_mercury/flow/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod confirm_reset_create;
44
pub mod confirm_reset_recover;
55
pub mod confirm_set_new_pin;
66
pub mod confirm_summary;
7+
pub mod continue_recovery;
78
pub mod get_address;
89
pub mod prompt_backup;
910
pub mod request_number;
@@ -20,6 +21,7 @@ pub use confirm_reset_create::ConfirmResetCreate;
2021
pub use confirm_reset_recover::ConfirmResetRecover;
2122
pub use confirm_set_new_pin::SetNewPin;
2223
pub use confirm_summary::new_confirm_summary;
24+
pub use continue_recovery::new_continue_recovery;
2325
pub use get_address::GetAddress;
2426
pub use prompt_backup::PromptBackup;
2527
pub use request_number::RequestNumber;

core/embed/rust/src/ui/model_mercury/layout.rs

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,6 @@ impl ConfirmBlobParams {
404404
}
405405
}
406406

407-
const RECOVERY_TYPE_DRY_RUN: u32 = 1;
408-
const RECOVERY_TYPE_UNLOCK_REPEATED_BACKUP: u32 = 2;
409-
410407
extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
411408
let block = move |_args: &[Obj], kwargs: &Map| {
412409
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@@ -1062,34 +1059,6 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M
10621059
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
10631060
}
10641061

1065-
extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
1066-
let block = move |_args: &[Obj], kwargs: &Map| {
1067-
let _title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
1068-
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
1069-
let _button: TString = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
1070-
let recovery_type: u32 = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
1071-
let _info_button: bool = kwargs.get_or(Qstr::MP_QSTR_info_button, false)?;
1072-
1073-
let paragraphs = Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, description));
1074-
1075-
let notification = match recovery_type {
1076-
RECOVERY_TYPE_DRY_RUN => TR::recovery__title_dry_run.into(),
1077-
RECOVERY_TYPE_UNLOCK_REPEATED_BACKUP => TR::recovery__title_dry_run.into(),
1078-
_ => TR::recovery__title.into(),
1079-
};
1080-
1081-
let obj = LayoutObj::new(SwipeUpScreen::new(
1082-
Frame::left_aligned(notification, SwipeContent::new(paragraphs))
1083-
.with_cancel_button()
1084-
.with_footer(TR::instructions__swipe_up.into(), None)
1085-
.with_subtitle(TR::words__instructions.into())
1086-
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
1087-
))?;
1088-
Ok(obj.into())
1089-
};
1090-
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
1091-
}
1092-
10931062
extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
10941063
let block = move |_args: &[Obj], _kwargs: &Map| {
10951064
let obj = LayoutObj::new(Frame::left_aligned(
@@ -1731,16 +1700,15 @@ pub static mp_module_trezorui2: Module = obj_module! {
17311700
/// mark next to them."""
17321701
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(),
17331702

1734-
/// def confirm_recovery(
1703+
/// def flow_continue_recovery(
17351704
/// *,
1736-
/// title: str,
1737-
/// description: str,
1738-
/// button: str,
1705+
/// first_screen: bool,
17391706
/// recovery_type: RecoveryType,
1740-
/// info_button: bool = False,
1707+
/// text: str,
1708+
/// subtext: str | None = None,
17411709
/// ) -> LayoutObj[UiResult]:
17421710
/// """Device recovery homescreen."""
1743-
Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(),
1711+
Qstr::MP_QSTR_flow_continue_recovery => obj_fn_kw!(0, flow::continue_recovery::new_continue_recovery).as_obj(),
17441712

17451713
/// def select_word_count(
17461714
/// *,

core/mocks/generated/trezorui2.pyi

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,12 @@ def show_checklist(
450450

451451

452452
# rust/src/ui/model_mercury/layout.rs
453-
def confirm_recovery(
453+
def flow_continue_recovery(
454454
*,
455-
title: str,
456-
description: str,
457-
button: str,
455+
first_screen: bool,
458456
recovery_type: RecoveryType,
459-
info_button: bool = False,
457+
text: str,
458+
subtext: str | None = None,
460459
) -> LayoutObj[UiResult]:
461460
"""Device recovery homescreen."""
462461

core/src/apps/management/recovery_device/layout.py

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,6 @@
1818
from trezor.enums import BackupType
1919

2020

21-
async def _confirm_abort(dry_run: bool = False) -> None:
22-
if dry_run:
23-
await confirm_action(
24-
"abort_recovery",
25-
TR.recovery__title_cancel_dry_run,
26-
TR.recovery__cancel_dry_run,
27-
description=TR.recovery__wanna_cancel_dry_run,
28-
verb=TR.buttons__cancel,
29-
br_code=ButtonRequestType.ProtectCall,
30-
)
31-
else:
32-
await confirm_action(
33-
"abort_recovery",
34-
TR.recovery__title_cancel_recovery,
35-
TR.recovery__progress_will_be_lost,
36-
TR.recovery__wanna_cancel_recovery,
37-
verb=TR.buttons__cancel,
38-
reverse=True,
39-
br_code=ButtonRequestType.ProtectCall,
40-
)
41-
42-
4321
async def request_mnemonic(
4422
word_count: int, backup_type: BackupType | None
4523
) -> str | None:
@@ -149,24 +127,12 @@ async def homescreen_dialog(
149127
show_info: bool = False,
150128
) -> None:
151129
import storage.recovery as storage_recovery
152-
from trezor.enums import RecoveryType
153130
from trezor.ui.layouts.recovery import continue_recovery
154-
from trezor.wire import ActionCancelled
155131

156132
from .recover import RecoveryAborted
157133

158134
recovery_type = storage_recovery.get_type()
159-
160-
while True:
161-
if await continue_recovery(
162-
button_label, text, subtext, info_func, recovery_type, show_info
163-
):
164-
# go forward in the recovery process
165-
break
166-
# user has chosen to abort, confirm the choice
167-
try:
168-
await _confirm_abort(recovery_type != RecoveryType.NormalRecovery)
169-
except ActionCancelled:
170-
pass
171-
else:
172-
raise RecoveryAborted
135+
if not await continue_recovery(
136+
button_label, text, subtext, info_func, recovery_type, show_info
137+
):
138+
raise RecoveryAborted

core/src/trezor/ui/layouts/mercury/recovery.py

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,10 @@
88
from . import RustLayout, raise_if_not_confirmed
99

1010
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
11+
CANCELLED = trezorui2.CANCELLED # global_import_cache
1112
INFO = trezorui2.INFO # global_import_cache
1213

1314

14-
async def _is_confirmed_info(
15-
dialog: RustLayout,
16-
info_func: Callable,
17-
) -> bool:
18-
while True:
19-
result = await dialog
20-
21-
if result is trezorui2.INFO:
22-
await info_func()
23-
dialog.request_complete_repaint()
24-
else:
25-
return result is CONFIRMED
26-
27-
2815
async def request_word_count(recovery_type: RecoveryType) -> int:
2916
selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type))
3017
count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount)
@@ -112,38 +99,30 @@ async def show_group_share_success(share_index: int, group_index: int) -> None:
11299

113100

114101
async def continue_recovery(
115-
button_label: str,
102+
button_label: str, # unused on mercury
116103
text: str,
117104
subtext: str | None,
118-
info_func: Callable | None,
105+
info_func: Callable | None, # TODO: see below
119106
recovery_type: RecoveryType,
120-
show_info: bool = False, # unused on TT
107+
show_info: bool = False,
121108
) -> bool:
122-
from ..common import button_request
123-
124-
if show_info:
125-
# Show this just one-time
126-
description = TR.recovery__enter_each_word
127-
else:
128-
description = subtext or ""
109+
# TODO: info_func should be changed to return data to be shown (and not show
110+
# them) so that individual models can implement showing logic on their own.
111+
# T3T1 should move the data to `flow_continue_recovery` and hide them
112+
# in the context menu
129113

114+
# NOTE: show_info can be understood as first screen before any shares
130115
homepage = RustLayout(
131-
trezorui2.confirm_recovery(
132-
title=text,
133-
description=description,
134-
button=button_label,
135-
info_button=info_func is not None,
116+
trezorui2.flow_continue_recovery(
117+
first_screen=show_info,
136118
recovery_type=recovery_type,
119+
text=text,
120+
subtext=subtext,
137121
)
138122
)
139-
140-
await button_request("recovery", ButtonRequestType.RecoveryHomepage)
141-
142-
if info_func is not None:
143-
return await _is_confirmed_info(homepage, info_func)
144-
else:
145-
result = await homepage
146-
return result is CONFIRMED
123+
# TODO: the button request might go to rust
124+
result = await interact(homepage, "recovery", ButtonRequestType.RecoveryHomepage)
125+
return result is CONFIRMED
147126

148127

149128
async def show_recovery_warning(

0 commit comments

Comments
 (0)