1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-24 07:18:09 +00:00

feat(core/rust): new design of wallet creation dialogs

[no changelog]
This commit is contained in:
grdddj 2023-06-26 10:50:28 +02:00 committed by Jiří Musil
parent 672d6b7d13
commit 03f77c50e9
20 changed files with 236 additions and 198 deletions

View File

@ -37,6 +37,7 @@ static void _librust_qstrs(void) {
MP_QSTR_case_sensitive; MP_QSTR_case_sensitive;
MP_QSTR_confirm_action; MP_QSTR_confirm_action;
MP_QSTR_confirm_address; MP_QSTR_confirm_address;
MP_QSTR_confirm_backup;
MP_QSTR_confirm_blob; MP_QSTR_confirm_blob;
MP_QSTR_confirm_coinjoin; MP_QSTR_confirm_coinjoin;
MP_QSTR_confirm_emphasized; MP_QSTR_confirm_emphasized;

View File

@ -97,14 +97,13 @@ impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
}; };
} }
// Drawing text // Drawing text
Op::Text(text) => { Op::Text(text, continued) => {
// Try to fit text on the current page and if they do not fit, // Try to fit text on the current page and if they do not fit,
// return the appropriate OutOfBounds message // return the appropriate OutOfBounds message
// Inserting the ellipsis at the very beginning of the text if needed // Inserting the ellipsis at the very beginning of the text if needed
// (just once for the first Op::Text on the non-first page). // (just for incomplete texts that were separated)
self.layout.continues_from_prev_page = self.layout.continues_from_prev_page = continued;
skip_bytes > 0 && total_processed_chars == 0;
let fit = self.layout.layout_text(text.as_ref(), cursor, sink); let fit = self.layout.layout_text(text.as_ref(), cursor, sink);
@ -146,14 +145,16 @@ impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
let mut skipped = 0; let mut skipped = 0;
ops_iter.filter_map(move |op| { ops_iter.filter_map(move |op| {
match op { match op {
Op::Text(text) if skipped < skip_bytes => { Op::Text(text, _continued) if skipped < skip_bytes => {
let skip_text_bytes_if_fits_partially = skip_bytes - skipped; let skip_text_bytes_if_fits_partially = skip_bytes - skipped;
skipped = skipped.saturating_add(text.as_ref().len()); skipped = skipped.saturating_add(text.as_ref().len());
if skipped > skip_bytes { if skipped > skip_bytes {
// Fits partially // Fits partially
// Skipping some bytes at the beginning, leaving rest // Skipping some bytes at the beginning, leaving rest
// Signifying that the text continues from previous page
Some(Op::Text( Some(Op::Text(
text.skip_prefix(skip_text_bytes_if_fits_partially), text.skip_prefix(skip_text_bytes_if_fits_partially),
true,
)) ))
} else { } else {
// Does not fit at all // Does not fit at all
@ -184,15 +185,15 @@ impl<T: StringType + Clone> OpTextLayout<T> {
} }
pub fn text(self, text: T) -> Self { pub fn text(self, text: T) -> Self {
self.with_new_item(Op::Text(text)) self.with_new_item(Op::Text(text, false))
} }
pub fn newline(self) -> Self { pub fn newline(self) -> Self {
self.with_new_item(Op::Text("\n".into())) self.text("\n".into())
} }
pub fn newline_half(self) -> Self { pub fn newline_half(self) -> Self {
self.with_new_item(Op::Text("\r".into())) self.text("\r".into())
} }
pub fn next_page(self) -> Self { pub fn next_page(self) -> Self {
@ -238,7 +239,9 @@ impl<T: StringType + Clone> OpTextLayout<T> {
#[derive(Clone)] #[derive(Clone)]
pub enum Op<T: StringType> { pub enum Op<T: StringType> {
/// Render text with current color and font. /// Render text with current color and font.
Text(T), /// Bool signifies whether this is a split Text Op continued from previous
/// page. If true, a leading ellipsis will be rendered.
Text(T, bool),
/// Set current text color. /// Set current text color.
Color(Color), Color(Color),
/// Set currently used font. /// Set currently used font.

View File

@ -567,6 +567,15 @@ where
) )
} }
/// Left text and WIDE right arrow.
pub fn text_none_arrow_wide(text: T) -> Self {
Self::new(
Some(ButtonDetails::text(text)),
None,
Some(ButtonDetails::down_arrow_icon_wide()),
)
}
/// Only right text. /// Only right text.
pub fn none_none_text(text: T) -> Self { pub fn none_none_text(text: T) -> Self {
Self::new(None, None, Some(ButtonDetails::text(text))) Self::new(None, None, Some(ButtonDetails::text(text)))

View File

@ -39,10 +39,11 @@ where
{ {
pub fn new(pages: FlowPages<F, T>) -> Self { pub fn new(pages: FlowPages<F, T>) -> Self {
let current_page = pages.get(0); let current_page = pages.get(0);
let title = current_page.title().map(Title::new);
Self { Self {
pages, pages,
current_page, current_page,
title: None, title,
content_area: Rect::zero(), content_area: Rect::zero(),
title_area: Rect::zero(), title_area: Rect::zero(),
scrollbar: Child::new(ScrollBar::to_be_filled_later()), scrollbar: Child::new(ScrollBar::to_be_filled_later()),
@ -85,11 +86,11 @@ where
/// position. /// position.
fn change_current_page(&mut self, ctx: &mut EventCtx) { fn change_current_page(&mut self, ctx: &mut EventCtx) {
self.current_page = self.pages.get(self.page_counter); self.current_page = self.pages.get(self.page_counter);
if self.title.is_some() { if let Some(title) = self.current_page.title() {
if let Some(title) = self.current_page.title() { self.title = Some(Title::new(title));
self.title = Some(Title::new(title)); self.title.place(self.title_area);
self.title.place(self.title_area); } else {
} self.title = None;
} }
let scrollbar_active_index = self let scrollbar_active_index = self
.pages .pages

View File

@ -25,9 +25,7 @@ where
cancel_btn_details: Option<ButtonDetails<U>>, cancel_btn_details: Option<ButtonDetails<U>>,
/// Right button of the last screen /// Right button of the last screen
confirm_btn_details: Option<ButtonDetails<U>>, confirm_btn_details: Option<ButtonDetails<U>>,
/// Left button of the last page /// Left button of every screen
last_back_btn_details: Option<ButtonDetails<U>>,
/// Left button of every screen in the middle
back_btn_details: Option<ButtonDetails<U>>, back_btn_details: Option<ButtonDetails<U>>,
/// Right button of every screen apart the last one /// Right button of every screen apart the last one
next_btn_details: Option<ButtonDetails<U>>, next_btn_details: Option<ButtonDetails<U>>,
@ -47,8 +45,7 @@ where
pad: Pad::with_background(background).with_clear(), pad: Pad::with_background(background).with_clear(),
cancel_btn_details: Some(ButtonDetails::cancel_icon()), cancel_btn_details: Some(ButtonDetails::cancel_icon()),
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())), confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()), back_btn_details: Some(ButtonDetails::up_arrow_icon()),
last_back_btn_details: Some(ButtonDetails::up_arrow_icon()),
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()), next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
// Setting empty layout for now, we do not yet know the page count. // Setting empty layout for now, we do not yet know the page count.
// Initial button layout will be set in `place()` after we can call // Initial button layout will be set in `place()` after we can call
@ -122,18 +119,15 @@ where
} }
fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout<U> { fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout<U> {
let btn_left = self.get_left_button_details(!has_prev, !has_next); let btn_left = self.get_left_button_details(!has_prev);
let btn_right = self.get_right_button_details(has_next); let btn_right = self.get_right_button_details(has_next);
ButtonLayout::new(btn_left, None, btn_right) ButtonLayout::new(btn_left, None, btn_right)
} }
/// Get the left button details, depending whether the page is first, last, /// Get the left button details, depending whether the page is first or not.
/// or in the middle. fn get_left_button_details(&self, is_first: bool) -> Option<ButtonDetails<U>> {
fn get_left_button_details(&self, is_first: bool, is_last: bool) -> Option<ButtonDetails<U>> {
if is_first { if is_first {
self.cancel_btn_details.clone() self.cancel_btn_details.clone()
} else if is_last {
self.last_back_btn_details.clone()
} else { } else {
self.back_btn_details.clone() self.back_btn_details.clone()
} }

View File

@ -11,15 +11,15 @@ use crate::{
use heapless::{String, Vec}; use heapless::{String, Vec};
use super::{common::display_left, scrollbar::SCROLLBAR_SPACE, theme, title::Title, ScrollBar}; use super::{common::display_left, scrollbar::SCROLLBAR_SPACE, theme, ScrollBar};
const WORDS_PER_PAGE: usize = 3; const WORDS_PER_PAGE: usize = 3;
const EXTRA_LINE_HEIGHT: i16 = 2; const EXTRA_LINE_HEIGHT: i16 = 2;
const NUMBER_X_OFFSET: i16 = 5; const NUMBER_X_OFFSET: i16 = 0;
const NUMBER_WORD_OFFSET: i16 = 20; const WORD_X_OFFSET: i16 = 25;
const NUMBER_FONT: Font = Font::DEMIBOLD; const NUMBER_FONT: Font = Font::DEMIBOLD;
const WORD_FONT: Font = Font::BIG; const WORD_FONT: Font = Font::BIG;
const INFO_TOP_OFFSET: i16 = 15; const INFO_TOP_OFFSET: i16 = 20;
const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less
/// Showing the given share words. /// Showing the given share words.
@ -28,7 +28,6 @@ where
T: StringType, T: StringType,
{ {
area: Rect, area: Rect,
title: Child<Title<T>>,
scrollbar: Child<ScrollBar>, scrollbar: Child<ScrollBar>,
share_words: Vec<T, MAX_WORDS>, share_words: Vec<T, MAX_WORDS>,
page_index: usize, page_index: usize,
@ -38,10 +37,9 @@ impl<T> ShareWords<T>
where where
T: StringType + Clone, T: StringType + Clone,
{ {
pub fn new(title: T, share_words: Vec<T, MAX_WORDS>) -> Self { pub fn new(share_words: Vec<T, MAX_WORDS>) -> Self {
let mut instance = Self { let mut instance = Self {
area: Rect::zero(), area: Rect::zero(),
title: Child::new(Title::new(title)),
scrollbar: Child::new(ScrollBar::to_be_filled_later()), scrollbar: Child::new(ScrollBar::to_be_filled_later()),
share_words, share_words,
page_index: 0, page_index: 0,
@ -53,15 +51,7 @@ where
} }
fn word_index(&self) -> usize { fn word_index(&self) -> usize {
(self.page_index - 2) * WORDS_PER_PAGE self.page_index * WORDS_PER_PAGE
}
fn is_entry_page(&self) -> bool {
self.page_index == 0
}
fn is_second_page(&self) -> bool {
self.page_index == 1
} }
fn is_final_page(&self) -> bool { fn is_final_page(&self) -> bool {
@ -74,36 +64,13 @@ where
} else { } else {
self.share_words.len() / WORDS_PER_PAGE + 1 self.share_words.len() / WORDS_PER_PAGE + 1
}; };
// Two pages before the words, one after it // One page after the words
2 + word_screens + 1 word_screens + 1
} }
fn get_first_text(&self) -> String<100> { fn get_final_text(&self) -> String<50> {
build_string!( build_string!(
100, 50,
"Write down all ",
inttostr!(self.share_words.len() as u8),
" words in order."
)
}
/// Display the first page with user information.
fn paint_entry_page(&mut self) {
self.render_text_on_screen(&self.get_first_text(), Font::MONO);
}
fn get_second_text(&self) -> String<100> {
build_string!(100, "Do NOT make digital copies!")
}
/// Display the second page with user information.
fn paint_second_page(&mut self) {
self.render_text_on_screen(&self.get_second_text(), Font::MONO);
}
fn get_final_text(&self) -> String<100> {
build_string!(
100,
"I wrote down all ", "I wrote down all ",
inttostr!(self.share_words.len() as u8), inttostr!(self.share_words.len() as u8),
" words in order." " words in order."
@ -112,15 +79,10 @@ where
/// Display the final page with user confirmation. /// Display the final page with user confirmation.
fn paint_final_page(&mut self) { fn paint_final_page(&mut self) {
self.render_text_on_screen(&self.get_final_text(), Font::MONO);
}
/// Shows text in the main screen area.
fn render_text_on_screen(&self, text: &str, font: Font) {
text_multiline( text_multiline(
self.area.split_top(INFO_TOP_OFFSET).1, self.area.split_top(INFO_TOP_OFFSET).1,
text, &self.get_final_text(),
font, Font::NORMAL,
theme::FG, theme::FG,
theme::BG, theme::BG,
Alignment::Start, Alignment::Start,
@ -138,9 +100,10 @@ where
break; break;
} }
let word = &self.share_words[index]; let word = &self.share_words[index];
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset); let baseline = self.area.top_left() + Offset::y(y_offset);
display_left(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT); let ordinal = build_string!(5, inttostr!(index as u8 + 1), ".");
display_left(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT); display_left(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal, NUMBER_FONT);
display_left(baseline + Offset::x(WORD_X_OFFSET), &word, WORD_FONT);
} }
} }
} }
@ -152,12 +115,11 @@ where
type Msg = Never; type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let (title_area, _) = bounds.split_top(theme::FONT_HEADER.line_height()); let (top_area, _) = bounds.split_top(theme::FONT_HEADER.line_height());
let (title_area, scrollbar_area) = let (_, scrollbar_area) =
title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE); top_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE);
self.title.place(title_area);
self.scrollbar.place(scrollbar_area); self.scrollbar.place(scrollbar_area);
self.area = bounds; self.area = bounds;
@ -165,7 +127,6 @@ where
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.scrollbar.event(ctx, event); self.scrollbar.event(ctx, event);
None None
} }
@ -174,12 +135,7 @@ where
// Showing scrollbar in all cases // Showing scrollbar in all cases
// Individual pages are responsible for not colliding with it // Individual pages are responsible for not colliding with it
self.scrollbar.paint(); self.scrollbar.paint();
if self.is_entry_page() { if self.is_final_page() {
self.title.paint();
self.paint_entry_page();
} else if self.is_second_page() {
self.paint_second_page();
} else if self.is_final_page() {
self.paint_final_page(); self.paint_final_page();
} else { } else {
self.paint_words(); self.paint_words();
@ -211,19 +167,10 @@ where
{ {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ShareWords"); t.component("ShareWords");
let content = if self.is_entry_page() { let content = if self.is_final_page() {
build_string!(
100,
self.title.inner().get_text(),
"\n",
&self.get_first_text()
)
} else if self.is_second_page() {
self.get_second_text()
} else if self.is_final_page() {
self.get_final_text() self.get_final_text()
} else { } else {
let mut content = String::<100>::new(); let mut content = String::<50>::new();
for i in 0..WORDS_PER_PAGE { for i in 0..WORDS_PER_PAGE {
let index = self.word_index() + i; let index = self.word_index() + i;
if index >= self.share_words.len() { if index >= self.share_words.len() {
@ -231,7 +178,7 @@ where
} }
let word = &self.share_words[index]; let word = &self.share_words[index];
let current_line = let current_line =
build_string!(20, inttostr!(index as u8 + 1), " ", word.as_ref(), "\n"); build_string!(50, inttostr!(index as u8 + 1), ". ", word.as_ref(), "\n");
unwrap!(content.push_str(&current_line)); unwrap!(content.push_str(&current_line));
} }
content content

View File

@ -413,9 +413,43 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs:
.text_normal("More info at".into()) .text_normal("More info at".into())
.newline() .newline()
.text_bold("trezor.io/tos".into()); .text_bold("trezor.io/tos".into());
let formatted = FormattedText::new(ops); let formatted = FormattedText::new(ops).vertically_centered();
content_in_button_page(title, formatted, button, None, false) content_in_button_page(title, formatted, button, Some("".into()), false)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_backup(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], _kwargs: &Map| {
let get_page = move |page_index| match page_index {
0 => {
let btn_layout = ButtonLayout::text_none_arrow_wide("SKIP".into());
let btn_actions = ButtonActions::cancel_none_next();
let ops = OpTextLayout::new(theme::TEXT_NORMAL)
.text_normal("New wallet created.".into())
.newline()
.newline()
.text_normal("It should be backed up now!".into());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title("SUCCESS".into())
}
1 => {
let btn_layout = ButtonLayout::up_arrow_none_text("BACK UP".into());
let btn_actions = ButtonActions::prev_none_confirm();
let ops = OpTextLayout::new(theme::TEXT_NORMAL).text_normal(
"You can use your backup to recover your wallet at any time.".into(),
);
let formatted = FormattedText::new(ops).vertically_centered();
Page::<StrBuffer>::new(btn_layout, btn_actions, formatted)
.with_title("BACK UP WALLET".into())
}
_ => unreachable!(),
};
let pages = FlowPages::new(get_page, 2);
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
}; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
@ -532,7 +566,6 @@ extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut M
let address_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_label)?.try_into()?; let address_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_label)?.try_into()?;
let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?; let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?;
let address_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_title)?.try_into()?; let address_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_title)?.try_into()?;
let address_title_clone = address_title.clone();
let amount_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_title)?.try_into()?; let amount_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_title)?.try_into()?;
let get_page = move |page_index| { let get_page = move |page_index| {
@ -566,7 +599,7 @@ extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut M
}; };
let pages = FlowPages::new(get_page, 2); let pages = FlowPages::new(get_page, 2);
let obj = LayoutObj::new(Flow::new(pages).with_common_title(address_title_clone))?; let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into()) Ok(obj.into())
}; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -669,11 +702,11 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
.line_breaking(LineBreaking::BreakWordsNoHyphen) .line_breaking(LineBreaking::BreakWordsNoHyphen)
.text_mono(address.clone()); .text_mono(address.clone());
let formatted = FormattedText::new(ops); let formatted = FormattedText::new(ops);
Page::new(btn_layout, btn_actions, formatted) Page::new(btn_layout, btn_actions, formatted).with_title(title.clone())
}; };
let pages = FlowPages::new(get_page, 1); let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title))?; let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into()) Ok(obj.into())
}; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -688,7 +721,7 @@ fn tutorial_screen(
btn_actions: ButtonActions, btn_actions: ButtonActions,
) -> Page<StrBuffer> { ) -> Page<StrBuffer> {
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL).text_normal(text.into()); let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL).text_normal(text.into());
let formatted = FormattedText::new(ops).vertically_aligned(geometry::Alignment::Center); let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(title.into()) Page::new(btn_layout, btn_actions, formatted).with_title(title.into())
} }
@ -766,11 +799,7 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj
let pages = FlowPages::new(get_page, PAGE_COUNT); let pages = FlowPages::new(get_page, PAGE_COUNT);
let obj = LayoutObj::new( let obj = LayoutObj::new(Flow::new(pages).with_scrollbar(false))?;
Flow::new(pages)
.with_scrollbar(false)
.with_common_title("HELLO".into()),
)?;
Ok(obj.into()) Ok(obj.into())
}; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -1156,16 +1185,17 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map)
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| { let block = |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?; let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
let share_words: Vec<StrBuffer, 33> = iter_into_vec(share_words_obj)?; let share_words: Vec<StrBuffer, 33> = iter_into_vec(share_words_obj)?;
let cancel_btn = Some(ButtonDetails::up_arrow_icon());
let confirm_btn = Some( let confirm_btn = Some(
ButtonDetails::<StrBuffer>::text("HOLD TO CONFIRM".into()).with_default_duration(), ButtonDetails::<StrBuffer>::text("HOLD TO CONFIRM".into()).with_default_duration(),
); );
let obj = LayoutObj::new( let obj = LayoutObj::new(
ButtonPage::new(ShareWords::new(title, share_words), theme::BG) ButtonPage::new(ShareWords::new(share_words), theme::BG)
.with_cancel_btn(cancel_btn)
.with_confirm_btn(confirm_btn), .with_confirm_btn(confirm_btn),
)?; )?;
Ok(obj.into()) Ok(obj.into())
@ -1460,6 +1490,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm TOS before device setup.""" /// """Confirm TOS before device setup."""
Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(), Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(),
/// def confirm_backup() -> object:
/// """Strongly recommend user to do backup."""
Qstr::MP_QSTR_confirm_backup => obj_fn_kw!(0, new_confirm_backup).as_obj(),
/// def show_address_details( /// def show_address_details(
/// *, /// *,
/// address: str, /// address: str,
@ -1649,7 +1683,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def show_share_words( /// def show_share_words(
/// *, /// *,
/// title: str,
/// share_words: Iterable[str], /// share_words: Iterable[str],
/// ) -> object: /// ) -> object:
/// """Shows a backup seed.""" /// """Shows a backup seed."""

View File

@ -75,6 +75,11 @@ def confirm_reset_device(
"""Confirm TOS before device setup.""" """Confirm TOS before device setup."""
# rust/src/ui/model_tr/layout.rs
def confirm_backup() -> object:
"""Strongly recommend user to do backup."""
# rust/src/ui/model_tr/layout.rs # rust/src/ui/model_tr/layout.rs
def show_address_details( def show_address_details(
*, *,
@ -285,7 +290,6 @@ def select_word(
# rust/src/ui/model_tr/layout.rs # rust/src/ui/model_tr/layout.rs
def show_share_words( def show_share_words(
*, *,
title: str,
share_words: Iterable[str], share_words: Iterable[str],
) -> object: ) -> object:
"""Shows a backup seed.""" """Shows a backup seed."""

View File

@ -132,12 +132,12 @@ async def _show_confirmation_success(
async def _show_confirmation_failure() -> None: async def _show_confirmation_failure() -> None:
from trezor.ui.layouts.recovery import show_recovery_warning from trezor.ui.layouts.reset import show_reset_warning
await show_recovery_warning( await show_reset_warning(
"warning_backup_check", "warning_backup_check",
"Please check again.", "Please check again",
"That is the wrong word.", "Wrong word selected!",
"Check again", "Check again",
ButtonRequestType.ResetDevice, ButtonRequestType.ResetDevice,
) )
@ -171,7 +171,7 @@ async def bip39_show_and_confirm_mnemonic(mnemonic: str) -> None:
# make the user confirm some words from the mnemonic # make the user confirm some words from the mnemonic
if await _share_words_confirmed(None, words): if await _share_words_confirmed(None, words):
break # this share is confirmed, go to next one break # mnemonic is confirmed, go next
# SLIP39 # SLIP39

View File

@ -400,26 +400,25 @@ async def confirm_reset_device(
) )
# TODO cleanup @ redesign
async def confirm_backup() -> bool: async def confirm_backup() -> bool:
if await get_bool( br_type = "backup_device"
"backup_device", br_code = ButtonRequestType.ResetDevice
"SUCCESS",
description="New wallet has been created.\nIt should be backed up now!", result = await interact(
verb="BACK UP", RustLayout(trezorui2.confirm_backup()),
verb_cancel="SKIP", br_type,
br_code=ButtonRequestType.ResetDevice, br_code,
): )
if result is CONFIRMED:
return True return True
return await get_bool( return await get_bool(
"backup_device", br_type,
"WARNING", "SKIP BACKUP",
"Are you sure you want to skip the backup?\n", description="Are you sure you want to skip the backup?",
"You can back up your Trezor once, at any time.",
verb="BACK UP", verb="BACK UP",
verb_cancel="SKIP", verb_cancel="SKIP",
br_code=ButtonRequestType.ResetDevice, br_code=br_code,
) )

View File

@ -6,7 +6,7 @@ from trezor.wire import ActionCancelled
import trezorui2 import trezorui2
from ..common import interact from ..common import interact
from . import RustLayout, confirm_action, raise_if_not_confirmed from . import RustLayout, confirm_action, show_error
CONFIRMED = trezorui2.CONFIRMED # global_import_cache CONFIRMED = trezorui2.CONFIRMED # global_import_cache
@ -21,9 +21,11 @@ async def show_share_words(
group_index: int | None = None, group_index: int | None = None,
) -> None: ) -> None:
# Showing words, asking for write down confirmation and preparing for check # Showing words, asking for write down confirmation and preparing for check
br_type = "backup_words"
br_code = ButtonRequestType.ResetDevice
if share_index is None: if share_index is None:
title = "RECOVERY SEED" title = "STANDARD BACKUP"
check_title = "CHECK BACKUP" check_title = "CHECK BACKUP"
elif group_index is None: elif group_index is None:
title = f"SHARE #{share_index + 1}" title = f"SHARE #{share_index + 1}"
@ -32,26 +34,37 @@ async def show_share_words(
title = f"G{group_index + 1} - SHARE {share_index + 1}" title = f"G{group_index + 1} - SHARE {share_index + 1}"
check_title = f"GROUP {group_index + 1} - SHARE {share_index + 1}" check_title = f"GROUP {group_index + 1} - SHARE {share_index + 1}"
await raise_if_not_confirmed( # We want the option to go back from words to the previous screen
interact( # (by sending CANCELLED)
while True:
await confirm_action(
br_type,
title,
description=f"Write down all {len(share_words)} words in order.",
verb="SHOW WORDS",
verb_cancel=None,
br_code=br_code,
)
result = await interact(
RustLayout( RustLayout(
trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"] trezorui2.show_share_words( # type: ignore [Arguments missing for parameters]
title=title,
share_words=share_words, # type: ignore [No parameter named "share_words"] share_words=share_words, # type: ignore [No parameter named "share_words"]
) )
), ),
"backup_words", br_type,
ButtonRequestType.ResetDevice, br_code,
) )
) if result is CONFIRMED:
break
await confirm_action( await confirm_action(
"backup_words", br_type,
check_title, check_title,
description="Select the correct word for each position.", description="Select the correct word for each position.",
verb="CONTINUE", verb="CONTINUE",
verb_cancel=None, verb_cancel=None,
br_code=ButtonRequestType.ResetDevice, br_code=br_code,
) )
@ -234,13 +247,12 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
async def show_warning_backup(slip39: bool) -> None: async def show_warning_backup(slip39: bool) -> None:
await confirm_action( await show_error(
"backup_warning", "backup_warning",
"SHAMIR BACKUP" if slip39 else "WALLET BACKUP", "REMEMBER",
description="You can use your backup to recover your wallet at any time.", "Never make a digital copy of your backup or upload it online!",
verb="HOLD TO BEGIN", "OK, I UNDERSTAND",
hold=True, ButtonRequestType.ResetDevice,
br_code=ButtonRequestType.ResetDevice,
) )
@ -253,3 +265,21 @@ async def show_success_backup() -> None:
verb_cancel=None, verb_cancel=None,
br_code=ButtonRequestType.Success, br_code=ButtonRequestType.Success,
) )
async def show_reset_warning(
ctx: GenericContext,
br_type: str,
content: str,
subheader: str | None = None,
button: str = "TRY AGAIN",
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
await show_error(
ctx,
br_type,
button.upper(),
subheader or "",
content,
br_code=br_code,
)

View File

@ -7,7 +7,7 @@ from trezor.wire.context import wait as ctx_wait
import trezorui2 import trezorui2
from ..common import interact from ..common import interact
from . import RustLayout from . import RustLayout, raise_if_not_confirmed
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Sequence from typing import Callable, Sequence
@ -335,3 +335,26 @@ async def show_success_backup() -> None:
text = "Use your backup when you need to recover your wallet." text = "Use your backup when you need to recover your wallet."
await show_success("success_backup", text, "Your backup is done.") await show_success("success_backup", text, "Your backup is done.")
async def show_reset_warning(
br_type: str,
content: str,
subheader: str | None = None,
button: str = "TRY AGAIN",
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_warning(
title=subheader or "",
description=content,
button=button.upper(),
allow_cancel=False,
)
),
br_type,
br_code,
)
)

View File

@ -581,20 +581,20 @@ class DebugLink:
x, y = click x, y = click
return self.input(x=x, y=y, hold_ms=hold_ms, wait=True) return self.input(x=x, y=y, hold_ms=hold_ms, wait=True)
def press_yes(self, wait: bool = False) -> None: def press_yes(self, wait: bool = False) -> Optional[LayoutContent]:
self.input(button=messages.DebugButton.YES, wait=wait) return self.input(button=messages.DebugButton.YES, wait=wait)
def press_no(self, wait: bool = False) -> None: def press_no(self, wait: bool = False) -> Optional[LayoutContent]:
self.input(button=messages.DebugButton.NO, wait=wait) return self.input(button=messages.DebugButton.NO, wait=wait)
def press_info(self, wait: bool = False) -> None: def press_info(self, wait: bool = False) -> Optional[LayoutContent]:
self.input(button=messages.DebugButton.INFO, wait=wait) return self.input(button=messages.DebugButton.INFO, wait=wait)
def swipe_up(self, wait: bool = False) -> None: def swipe_up(self, wait: bool = False) -> Optional[LayoutContent]:
self.input(swipe=messages.DebugSwipeDirection.UP, wait=wait) return self.input(swipe=messages.DebugSwipeDirection.UP, wait=wait)
def swipe_down(self, wait: bool = False) -> None: def swipe_down(self, wait: bool = False) -> Optional[LayoutContent]:
self.input(swipe=messages.DebugSwipeDirection.DOWN, wait=wait) return self.input(swipe=messages.DebugSwipeDirection.DOWN, wait=wait)
@overload @overload
def swipe_right(self) -> None: def swipe_right(self) -> None:

View File

@ -20,14 +20,10 @@ def confirm_new_wallet(debug: "DebugLink") -> None:
debug.press_right(wait=True) debug.press_right(wait=True)
def confirm_read(debug: "DebugLink", title: str, hold: bool = False) -> None: def confirm_read(debug: "DebugLink", title: str, middle_r: bool = False) -> None:
layout = debug.read_layout() layout = debug.read_layout()
if title == "Caution": if title == "Caution":
if debug.model == "T": assert "Never make a digital copy" in layout.text_content()
# TODO: could look into button texts
assert "OK, I UNDERSTAND" in layout.json_str
elif debug.model == "R":
assert "use your backup to recover" in layout.text_content()
elif title == "Success": elif title == "Success":
# TODO: improve this # TODO: improve this
assert any( assert any(
@ -36,7 +32,7 @@ def confirm_read(debug: "DebugLink", title: str, hold: bool = False) -> None:
"success", "success",
"finished", "finished",
"done", "done",
"has been created", "created",
"Keep it safe", "Keep it safe",
) )
) )
@ -50,12 +46,10 @@ def confirm_read(debug: "DebugLink", title: str, hold: bool = False) -> None:
elif debug.model == "R": elif debug.model == "R":
if layout.page_count() > 1: if layout.page_count() > 1:
debug.press_right(wait=True) debug.press_right(wait=True)
if hold: if middle_r:
# TODO: create debug.hold_right()? debug.press_middle(wait=True)
debug.press_yes()
else: else:
debug.press_right() debug.press_right(wait=True)
debug.wait_layout()
def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> None: def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> None:
@ -94,18 +88,19 @@ def read_words(
assert layout.title() == "RECOVERY SEED" assert layout.title() == "RECOVERY SEED"
elif debug.model == "R": elif debug.model == "R":
if backup_type == messages.BackupType.Slip39_Advanced: if backup_type == messages.BackupType.Slip39_Advanced:
assert "SHARE" in layout.text_content() assert "SHARE" in layout.title()
elif backup_type == messages.BackupType.Slip39_Basic: elif backup_type == messages.BackupType.Slip39_Basic:
assert layout.text_content().startswith("SHARE #") assert layout.title().startswith("SHARE #")
else: else:
assert layout.text_content().startswith("RECOVERY SEED") assert layout.title() == "STANDARD BACKUP"
assert "Write down" in layout.text_content()
layout = debug.press_right(wait=True)
# Swiping through all the pages and loading the words # Swiping through all the pages and loading the words
for i in range(layout.page_count() - 1): for _ in range(layout.page_count() - 1):
# In model R, first two pages are just informational words.extend(layout.seed_words())
if not (debug.model == "R" and i < 2): layout = debug.swipe_up(wait=True)
words.extend(layout.seed_words())
layout = debug.input(swipe=messages.DebugSwipeDirection.UP, wait=True)
assert layout is not None assert layout is not None
if debug.model == "T": if debug.model == "T":
words.extend(layout.seed_words()) words.extend(layout.seed_words())

View File

@ -52,8 +52,8 @@ def test_reset_bip39(device_handler: "BackgroundDeviceHandler"):
# confirm back up # confirm back up
reset.confirm_read(debug, "Success") reset.confirm_read(debug, "Success")
# confirm backup warning (hold-to-confirm on TR) # confirm backup warning
reset.confirm_read(debug, "Caution", hold=True) reset.confirm_read(debug, "Caution", middle_r=True)
# read words # read words
words = reset.read_words(debug, messages.BackupType.Bip39) words = reset.read_words(debug, messages.BackupType.Bip39)

View File

@ -105,8 +105,8 @@ def test_reset_slip39_advanced(
else: else:
raise RuntimeError("not a supported combination") raise RuntimeError("not a supported combination")
# confirm backup warning (hold-to-confirm on TR) # confirm backup warning
reset.confirm_read(debug, "Caution", hold=True) reset.confirm_read(debug, "Caution", middle_r=True)
all_words: list[str] = [] all_words: list[str] = []
for _ in range(group_count): for _ in range(group_count):

View File

@ -85,8 +85,8 @@ def test_reset_slip39_basic(
# confirm checklist # confirm checklist
reset.confirm_read(debug, "Checklist") reset.confirm_read(debug, "Checklist")
# confirm backup warning (hold-to-confirm on TR) # confirm backup warning
reset.confirm_read(debug, "Caution", hold=True) reset.confirm_read(debug, "Caution", middle_r=True)
all_words: list[str] = [] all_words: list[str] = []
for _ in range(num_of_shares): for _ in range(num_of_shares):

View File

@ -339,14 +339,14 @@ def read_and_confirm_mnemonic_tr(
debug: "DebugLink", choose_wrong: bool = False debug: "DebugLink", choose_wrong: bool = False
) -> Generator[None, "ButtonRequest", Optional[str]]: ) -> Generator[None, "ButtonRequest", Optional[str]]:
mnemonic: list[str] = [] mnemonic: list[str] = []
yield # write down all 12 words in order
debug.press_yes()
br = yield br = yield
assert br.pages is not None assert br.pages is not None
for i in range(br.pages - 1): for _ in range(br.pages - 1):
layout = debug.wait_layout() layout = debug.wait_layout()
# First two pages have just instructions words = layout.seed_words()
if i > 1: mnemonic.extend(words)
words = layout.seed_words()
mnemonic.extend(words)
debug.press_right() debug.press_right()
debug.press_yes() debug.press_yes()

View File

@ -107,7 +107,7 @@ def test_skip_backup_msg(client: Client, backup_type, backup_flow):
@pytest.mark.skip_t1 @pytest.mark.skip_t1
@pytest.mark.parametrize("backup_type, backup_flow", VECTORS) @pytest.mark.parametrize("backup_type, backup_flow", VECTORS)
@pytest.mark.setup_client(uninitialized=True) @pytest.mark.setup_client(uninitialized=True)
def test_skip_backup_manual(client: Client, backup_type, backup_flow): def test_skip_backup_manual(client: Client, backup_type: BackupType, backup_flow):
with WITH_MOCK_URANDOM, client: with WITH_MOCK_URANDOM, client:
IF = InputFlowResetSkipBackup(client) IF = InputFlowResetSkipBackup(client)
client.set_input_flow(IF.get()) client.set_input_flow(IF.get())

View File

@ -2027,5 +2027,4 @@ class InputFlowResetSkipBackup(InputFlowBase):
yield # Skip Backup yield # Skip Backup
self.debug.press_no() self.debug.press_no()
yield # Confirm skip backup yield # Confirm skip backup
self.debug.press_right()
self.debug.press_no() self.debug.press_no()