1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-16 01:22:02 +00:00

feat(core): new design of PIN dialogs

[no changelog]
This commit is contained in:
grdddj 2023-06-20 09:25:02 +02:00 committed by Jiří Musil
parent 10449759bf
commit 672d6b7d13
18 changed files with 343 additions and 137 deletions

View File

@ -85,6 +85,7 @@ static void _librust_qstrs(void) {
MP_QSTR_max_len; MP_QSTR_max_len;
MP_QSTR_max_rounds; MP_QSTR_max_rounds;
MP_QSTR_min_count; MP_QSTR_min_count;
MP_QSTR_multiple_pages_texts;
MP_QSTR_notification; MP_QSTR_notification;
MP_QSTR_notification_level; MP_QSTR_notification_level;
MP_QSTR_page_count; MP_QSTR_page_count;

View File

@ -617,6 +617,15 @@ where
) )
} }
/// Cancel cross on left and right arrow facing down.
pub fn up_arrow_none_arrow_wide() -> Self {
Self::new(
Some(ButtonDetails::up_arrow_icon()),
None,
Some(ButtonDetails::down_arrow_icon_wide()),
)
}
/// Cancel cross on left and right arrow facing down. /// Cancel cross on left and right arrow facing down.
pub fn cancel_none_arrow_down() -> Self { pub fn cancel_none_arrow_down() -> Self {
Self::new( Self::new(
@ -756,6 +765,11 @@ impl ButtonActions {
) )
} }
/// Only confirming with middle
pub fn none_confirm_none() -> Self {
Self::new(None, Some(ButtonAction::Confirm), None)
}
/// Going to last page with left, to the next page with right /// Going to last page with left, to the next page with right
pub fn last_none_next() -> Self { pub fn last_none_next() -> Self {
Self::new( Self::new(
@ -799,6 +813,11 @@ impl ButtonActions {
Self::new(None, None, Some(ButtonAction::NextPage)) Self::new(None, None, Some(ButtonAction::NextPage))
} }
/// Only going to the next page with middle
pub fn none_next_none() -> Self {
Self::new(None, Some(ButtonAction::NextPage), None)
}
/// Only going to the prev page with left /// Only going to the prev page with left
pub fn prev_none_none() -> Self { pub fn prev_none_none() -> Self {
Self::new(Some(ButtonAction::PrevPage), None, None) Self::new(Some(ButtonAction::PrevPage), None, None)

View File

@ -16,6 +16,8 @@ pub struct ChangingTextLine<T> {
font: Font, font: Font,
/// Whether to show the text. Can be disabled. /// Whether to show the text. Can be disabled.
show_content: bool, show_content: bool,
/// What to show in front of the text if it doesn't fit.
ellipsis: &'static str,
alignment: Alignment, alignment: Alignment,
} }
@ -29,6 +31,7 @@ where
text, text,
font, font,
show_content: true, show_content: true,
ellipsis: "...",
alignment, alignment,
} }
} }
@ -41,6 +44,12 @@ where
Self::new(text, Font::BOLD, Alignment::Center) Self::new(text, Font::BOLD, Alignment::Center)
} }
/// Not showing ellipsis at the beginning of longer texts.
pub fn without_ellipsis(mut self) -> Self {
self.ellipsis = "";
self
}
/// Update the text to be displayed in the line. /// Update the text to be displayed in the line.
pub fn update_text(&mut self, text: T) { pub fn update_text(&mut self, text: T) {
self.text = text; self.text = text;
@ -93,7 +102,7 @@ where
fn paint_long_content_with_ellipsis(&self) { fn paint_long_content_with_ellipsis(&self) {
let text_to_display = long_line_content_with_ellipsis( let text_to_display = long_line_content_with_ellipsis(
self.text.as_ref(), self.text.as_ref(),
"...", self.ellipsis,
self.font, self.font,
self.pad.area.width(), self.pad.area.width(),
); );

View File

@ -23,6 +23,7 @@ enum PinAction {
} }
const MAX_PIN_LENGTH: usize = 50; const MAX_PIN_LENGTH: usize = 50;
const EMPTY_PIN_STR: &str = "_";
const CHOICE_LENGTH: usize = 13; const CHOICE_LENGTH: usize = 13;
const NUMBER_START_INDEX: usize = 3; const NUMBER_START_INDEX: usize = 3;
@ -80,9 +81,9 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
pub struct PinEntry<T: StringType + Clone> { pub struct PinEntry<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryPIN, T, PinAction>, choice_page: ChoicePage<ChoiceFactoryPIN, T, PinAction>,
pin_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>, pin_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
subprompt_line: Child<ChangingTextLine<T>>, subprompt: T,
prompt: T,
show_real_pin: bool, show_real_pin: bool,
show_last_digit: bool,
textbox: TextBox<MAX_PIN_LENGTH>, textbox: TextBox<MAX_PIN_LENGTH>,
} }
@ -90,61 +91,62 @@ impl<T> PinEntry<T>
where where
T: StringType + Clone, T: StringType + Clone,
{ {
pub fn new(prompt: T, subprompt: T) -> Self { pub fn new(subprompt: T) -> Self {
let choices = ChoiceFactoryPIN; let pin_line_content = if !subprompt.as_ref().is_empty() {
String::from(subprompt.as_ref())
} else {
String::from(EMPTY_PIN_STR)
};
Self { Self {
// Starting at a random digit. // Starting at a random digit.
choice_page: ChoicePage::new(choices) choice_page: ChoicePage::new(ChoiceFactoryPIN)
.with_initial_page_counter(get_random_digit_position()) .with_initial_page_counter(get_random_digit_position())
.with_carousel(true), .with_carousel(true),
pin_line: Child::new(ChangingTextLine::center_bold(String::from(prompt.as_ref()))), pin_line: Child::new(
subprompt_line: Child::new(ChangingTextLine::center_mono(subprompt)), ChangingTextLine::center_bold(pin_line_content).without_ellipsis(),
prompt, ),
subprompt,
show_real_pin: false, show_real_pin: false,
show_last_digit: false,
textbox: TextBox::empty(), textbox: TextBox::empty(),
} }
} }
/// Performs overall update of the screen. /// Performs overall update of the screen.
fn update(&mut self, ctx: &mut EventCtx) { fn update(&mut self, ctx: &mut EventCtx) {
self.update_header_info(ctx); self.update_pin_line(ctx);
ctx.request_paint(); ctx.request_paint();
} }
/// Update the header information - (sub)prompt and visible PIN. /// Show updated content in the changing line.
/// If PIN is empty, showing prompt in `pin_line` and sub-prompt in the /// Many possibilities, according to the PIN state.
/// `subprompt_line`. Otherwise disabling the `subprompt_line` and showing fn update_pin_line(&mut self, ctx: &mut EventCtx) {
/// the PIN - either in real numbers or masked in asterisks. let pin_line_text = if self.is_empty() && !self.subprompt.as_ref().is_empty() {
fn update_header_info(&mut self, ctx: &mut EventCtx) { String::from(self.subprompt.as_ref())
let show_prompts = self.is_empty(); } else if self.is_empty() {
String::from(EMPTY_PIN_STR)
let text = if show_prompts {
String::from(self.prompt.as_ref())
} else if self.show_real_pin { } else if self.show_real_pin {
String::from(self.pin()) String::from(self.pin())
} else { } else {
// Showing asterisks and the last digit. // Showing asterisks and possibly the last digit.
let mut dots: String<MAX_PIN_LENGTH> = String::new(); let mut dots: String<MAX_PIN_LENGTH> = String::new();
for _ in 0..self.textbox.len() - 1 { for _ in 0..self.textbox.len() - 1 {
unwrap!(dots.push('*')); unwrap!(dots.push('*'));
} }
let last_char = unwrap!(self.textbox.content().chars().last()); let last_char = if self.show_last_digit {
unwrap!(self.textbox.content().chars().last())
} else {
'*'
};
unwrap!(dots.push(last_char)); unwrap!(dots.push(last_char));
dots dots
}; };
// Force repaint of the whole header.
// Putting the current text into the PIN line.
self.pin_line.mutate(ctx, |ctx, pin_line| { self.pin_line.mutate(ctx, |ctx, pin_line| {
pin_line.update_text(text); pin_line.update_text(pin_line_text);
pin_line.request_complete_repaint(ctx); pin_line.request_complete_repaint(ctx);
}); });
// Showing subprompt only conditionally.
self.subprompt_line.mutate(ctx, |ctx, subprompt_line| {
subprompt_line.show_or_not(show_prompts);
subprompt_line.request_complete_repaint(ctx);
});
} }
pub fn pin(&self) -> &str { pub fn pin(&self) -> &str {
@ -168,21 +170,23 @@ where
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let pin_height = self.pin_line.inner().needed_height(); let pin_height = self.pin_line.inner().needed_height();
let subtitle_height = self.subprompt_line.inner().needed_height(); let (pin_area, choice_area) = bounds.split_top(pin_height);
let (title_area, subtitle_and_choice_area) = bounds.split_top(pin_height); self.pin_line.place(pin_area);
let (subtitle_area, choice_area) = subtitle_and_choice_area.split_top(subtitle_height);
self.pin_line.place(title_area);
self.subprompt_line.place(subtitle_area);
self.choice_page.place(choice_area); self.choice_page.place(choice_area);
bounds bounds
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Any event when showing real PIN should hide it // Any event when showing real PIN should hide it
// Same with showing last digit
if self.show_real_pin { if self.show_real_pin {
self.show_real_pin = false; self.show_real_pin = false;
self.update(ctx) self.update(ctx)
} }
if self.show_last_digit {
self.show_last_digit = false;
self.update(ctx)
}
match self.choice_page.event(ctx, event) { match self.choice_page.event(ctx, event) {
Some(PinAction::Delete) => { Some(PinAction::Delete) => {
@ -201,6 +205,7 @@ where
// Choosing random digit to be shown next // Choosing random digit to be shown next
self.choice_page self.choice_page
.set_page_counter(ctx, get_random_digit_position()); .set_page_counter(ctx, get_random_digit_position());
self.show_last_digit = true;
self.update(ctx); self.update(ctx);
None None
} }
@ -210,7 +215,6 @@ where
fn paint(&mut self) { fn paint(&mut self) {
self.pin_line.paint(); self.pin_line.paint();
self.subprompt_line.paint();
self.choice_page.paint(); self.choice_page.paint();
} }
} }
@ -224,11 +228,7 @@ where
{ {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("PinKeyboard"); t.component("PinKeyboard");
t.string("prompt", self.prompt.as_ref()); t.string("subprompt", self.subprompt.as_ref());
let subprompt = self.subprompt_line.inner().get_text();
if !subprompt.as_ref().is_empty() {
t.string("subprompt", subprompt.as_ref());
}
t.string("pin", self.textbox.content()); t.string("pin", self.textbox.content());
t.child("choice_page", &self.choice_page); t.child("choice_page", &self.choice_page);
} }

View File

@ -24,7 +24,8 @@ use crate::{
}, },
ComponentExt, FormattedText, LineBreaking, Timeout, ComponentExt, FormattedText, LineBreaking, Timeout,
}, },
display, geometry, display,
geometry::{self, Alignment},
layout::{ layout::{
obj::{ComponentMsgObj, LayoutObj}, obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO}, result::{CANCELLED, CONFIRMED, INFO},
@ -740,7 +741,7 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj
"CONFIRM", "CONFIRM",
"Press both left and right at the same\ntime to confirm.", "Press both left and right at the same\ntime to confirm.",
ButtonLayout::none_armed_none("CONFIRM".into()), ButtonLayout::none_armed_none("CONFIRM".into()),
ButtonActions::prev_next_none(), ButtonActions::none_next_none(),
) )
}, },
5 => { 5 => {
@ -775,6 +776,33 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
extern "C" fn new_show_error(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let get_page = move |page_index| {
assert!(page_index == 0);
let btn_layout = ButtonLayout::none_armed_none(button.clone());
let btn_actions = ButtonActions::none_confirm_none();
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL)
.alignment(Alignment::Center)
.text_bold(title.clone())
.newline()
.text_normal(description.clone());
let formatted = FormattedText::new(ops).vertically_aligned(Alignment::Center);
Page::new(btn_layout, btn_actions, formatted)
};
let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| { let block = move |_args: &[Obj], kwargs: &Map| {
let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?; let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?;
@ -814,6 +842,61 @@ extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *m
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
extern "C" fn new_multiple_pages_texts(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let verb: StrBuffer = kwargs.get(Qstr::MP_QSTR_verb)?.try_into()?;
let items: Gc<List> = kwargs.get(Qstr::MP_QSTR_items)?.try_into()?;
// Cache the page count so that we can move `items` into the closure.
let page_count = items.len();
// Closure to lazy-load the information on given page index.
// Done like this to allow arbitrarily many pages without
// the need of any allocation here in Rust.
let get_page = move |page_index| {
let item_obj = unwrap!(items.get(page_index));
let text = unwrap!(item_obj.try_into());
let (btn_layout, btn_actions) = if page_count == 1 {
// There is only one page
(
ButtonLayout::cancel_none_text(verb.clone()),
ButtonActions::cancel_none_confirm(),
)
} else if page_index == 0 {
// First page
(
ButtonLayout::cancel_none_arrow_wide(),
ButtonActions::cancel_none_next(),
)
} else if page_index == page_count - 1 {
// Last page
(
ButtonLayout::up_arrow_none_text(verb.clone()),
ButtonActions::prev_none_confirm(),
)
} else {
// Page in the middle
(
ButtonLayout::up_arrow_none_arrow_wide(),
ButtonActions::prev_none_next(),
)
};
let ops = OpTextLayout::new(theme::TEXT_NORMAL).text_normal(text);
let formatted = FormattedText::new(ops).vertically_aligned(Alignment::Center);
Page::new(btn_layout, btn_actions, formatted)
};
let pages = FlowPages::new(get_page, page_count);
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| { let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -999,7 +1082,9 @@ extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map)
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?; let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
let obj = LayoutObj::new(PinEntry::new(prompt, subprompt))?; let obj =
LayoutObj::new(Frame::new(prompt, PinEntry::new(subprompt)).with_title_centered())?;
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) }
@ -1442,6 +1527,15 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Show user how to interact with the device.""" /// """Show user how to interact with the device."""
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(), Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),
/// def show_error(
/// *,
/// title: str,
/// description: str,
/// button: str,
/// ) -> object:
/// """Show a popup with text centered both vertically and horizontally. With just a middle button."""
Qstr::MP_QSTR_show_error => obj_fn_kw!(0, new_show_error).as_obj(),
/// def confirm_modify_fee( /// def confirm_modify_fee(
/// *, /// *,
/// title: str, # ignored /// title: str, # ignored
@ -1466,6 +1560,15 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """ /// """
Qstr::MP_QSTR_confirm_fido => obj_fn_kw!(0, new_confirm_fido).as_obj(), Qstr::MP_QSTR_confirm_fido => obj_fn_kw!(0, new_confirm_fido).as_obj(),
/// def multiple_pages_texts(
/// *,
/// title: str,
/// verb: str,
/// items: list[str],
/// ) -> object:
/// """Show multiple texts, each on its own page."""
Qstr::MP_QSTR_multiple_pages_texts => obj_fn_kw!(0, new_multiple_pages_texts).as_obj(),
/// def show_info( /// def show_info(
/// *, /// *,
/// title: str, /// title: str,

View File

@ -149,6 +149,16 @@ def tutorial() -> object:
"""Show user how to interact with the device.""" """Show user how to interact with the device."""
# rust/src/ui/model_tr/layout.rs
def show_error(
*,
title: str,
description: str,
button: str,
) -> object:
"""Show a popup with text centered both vertically and horizontally. With just a middle button."""
# rust/src/ui/model_tr/layout.rs # rust/src/ui/model_tr/layout.rs
def confirm_modify_fee( def confirm_modify_fee(
*, *,
@ -174,6 +184,16 @@ def confirm_fido(
""" """
# rust/src/ui/model_tr/layout.rs
def multiple_pages_texts(
*,
title: str,
verb: str,
items: list[str],
) -> object:
"""Show multiple texts, each on its own page."""
# rust/src/ui/model_tr/layout.rs # rust/src/ui/model_tr/layout.rs
def show_info( def show_info(
*, *,

View File

@ -52,10 +52,10 @@ async def change_pin(msg: ChangePin) -> Success:
msg_screen = "PIN changed." msg_screen = "PIN changed."
msg_wire = "PIN changed" msg_wire = "PIN changed"
else: else:
msg_screen = "PIN protection enabled." msg_screen = "PIN protection\nturned on."
msg_wire = "PIN enabled" msg_wire = "PIN enabled"
else: else:
msg_screen = "PIN protection disabled." msg_screen = "PIN protection\nturned off."
msg_wire = "PIN removed" msg_wire = "PIN removed"
await show_success("success_pin", msg_screen) await show_success("success_pin", msg_screen)
@ -67,34 +67,30 @@ def _require_confirm_change_pin(msg: ChangePin) -> Awaitable[None]:
has_pin = config.has_pin() has_pin = config.has_pin()
br_type = "set_pin"
title = "PIN settings" title = "PIN settings"
if msg.remove and has_pin: # removing pin if msg.remove and has_pin: # removing pin
return confirm_action( return confirm_action(
br_type, "disable_pin",
title, title,
description="Do you want to disable PIN protection?", description="Are you sure you want to turn off PIN protection?",
verb="Disable", verb="Turn off",
) )
if not msg.remove and has_pin: # changing pin if not msg.remove and has_pin: # changing pin
return confirm_action( return confirm_action(
br_type, "change_pin",
title, title,
description="Do you want to change your PIN?", description="Change PIN?",
verb="Change", verb="Change",
) )
if not msg.remove and not has_pin: # setting new pin if not msg.remove and not has_pin: # setting new pin
return confirm_set_new_pin( return confirm_set_new_pin(
br_type, "set_pin",
title, title,
"Do you want to enable PIN protection?", "PIN",
[ "PIN will be required to access this device.",
"PIN will be used to access this device.",
"PIN should be 4-50 digits long.",
],
) )
# removing non-existing PIN # removing non-existing PIN

View File

@ -68,15 +68,15 @@ def _require_confirm_action(
return confirm_action( return confirm_action(
"disable_wipe_code", "disable_wipe_code",
title, title,
description="Do you want to disable wipe code protection?", description="Turn off wipe code protection?",
verb="Disable", verb="Turn off",
) )
if not msg.remove and has_wipe_code: if not msg.remove and has_wipe_code:
return confirm_action( return confirm_action(
"change_wipe_code", "change_wipe_code",
title, title,
description="Do you want to change the wipe code?", description="Change wipe code?",
verb="Change", verb="Change",
) )
@ -84,10 +84,8 @@ def _require_confirm_action(
return confirm_set_new_pin( return confirm_set_new_pin(
"set_wipe_code", "set_wipe_code",
title, title,
"Do you want to enable wipe code?", "wipe code",
[ "Wipe code can be used to erase all data from this device.",
"Wipe code can be used to erase all data from this device.",
],
) )
# Removing non-existing wipe code. # Removing non-existing wipe code.

View File

@ -1130,7 +1130,7 @@ async def request_pin_on_device(
result = await interact( result = await interact(
RustLayout( RustLayout(
trezorui2.request_pin( trezorui2.request_pin(
prompt=prompt, prompt=prompt.upper(),
subprompt=subprompt, subprompt=subprompt,
allow_cancel=allow_cancel, allow_cancel=allow_cancel,
wrong_pin=wrong_pin, wrong_pin=wrong_pin,
@ -1151,33 +1151,73 @@ async def confirm_reenter_pin(
) -> None: ) -> None:
br_type = "reenter_wipe_code" if is_wipe_code else "reenter_pin" br_type = "reenter_wipe_code" if is_wipe_code else "reenter_pin"
title = "CHECK WIPE CODE" if is_wipe_code else "CHECK PIN" title = "CHECK WIPE CODE" if is_wipe_code else "CHECK PIN"
description = "wipe code" if is_wipe_code else "PIN"
return await confirm_action( return await confirm_action(
br_type, br_type,
title, title,
action="Please re-enter to confirm.", description=f"Please re-enter {description} to confirm.",
verb="BEGIN", verb="CONTINUE",
br_code=BR_TYPE_OTHER, br_code=BR_TYPE_OTHER,
) )
async def show_error(
br_type: str,
title: str,
description: str,
button: str,
br_code: ButtonRequestType = BR_TYPE_OTHER,
) -> None:
await interact(
RustLayout(
trezorui2.show_error(
title=title,
description=description,
button=button,
)
),
br_type,
br_code,
)
async def confirm_multiple_pages_texts(
br_type: str,
title: str,
items: list[str],
verb: str,
br_code: ButtonRequestType = BR_TYPE_OTHER,
) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.multiple_pages_texts(
title=title,
verb=verb,
items=items,
)
),
br_type,
br_code,
)
)
async def pin_mismatch_popup( async def pin_mismatch_popup(
is_wipe_code: bool = False, is_wipe_code: bool = False,
) -> None: ) -> None:
title = "WIPE CODE MISMATCH" if is_wipe_code else "PIN MISMATCH"
description = "wipe codes" if is_wipe_code else "PINs" description = "wipe codes" if is_wipe_code else "PINs"
return await confirm_action( br_code = "wipe_code_mismatch" if is_wipe_code else "pin_mismatch"
"pin_mismatch", return await show_error(
title, br_code,
description=f"The {description} you entered do not match.\nPlease try again.", f"Entered {description} do not match!",
verb="TRY AGAIN", "Please check again.",
verb_cancel=None, "CHECK AGAIN",
br_code=BR_TYPE_OTHER, BR_TYPE_OTHER,
) )
async def wipe_code_same_as_pin_popup( async def wipe_code_same_as_pin_popup() -> None:
is_wipe_code: bool = False,
) -> None:
return await confirm_action( return await confirm_action(
"wipe_code_same_as_pin", "wipe_code_same_as_pin",
"INVALID WIPE CODE", "INVALID WIPE CODE",
@ -1192,34 +1232,33 @@ async def confirm_set_new_pin(
br_type: str, br_type: str,
title: str, title: str,
description: str, description: str,
information: list[str], information: str,
br_code: ButtonRequestType = BR_TYPE_OTHER, br_code: ButtonRequestType = BR_TYPE_OTHER,
) -> None: ) -> None:
await confirm_action( question = f"Turn on {description} protection?"
await confirm_multiple_pages_texts(
br_type, br_type,
title, title.upper(),
description=description, [question, information],
verb="ENABLE", "TURN ON",
br_code=br_code, br_code,
) )
# Additional information for the user to know about PIN/WIPE CODE # Not showing extra info for wipe code
if "wipe_code" in br_type: if "wipe_code" in br_type:
verb = "HODL TO BEGIN" # Easter egg from @Hannsek return
else:
information.append(
"Position of individual numbers will change between entries for enhanced security."
)
verb = "HOLD TO BEGIN"
return await confirm_action( # Additional information for the user to know about PIN
next_info = [
"PIN should be 4-50 digits long.",
"Position of the cursor will change between entries for enhanced security.",
]
await confirm_multiple_pages_texts(
br_type, br_type,
"", title.upper(),
description="\n\r".join(information), next_info,
verb=verb, "CONTINUE",
hold=True, br_code,
br_code=br_code,
) )

View File

@ -1217,17 +1217,37 @@ async def confirm_set_new_pin(
br_type: str, br_type: str,
title: str, title: str,
description: str, description: str,
information: list[str], # unused on TT information: str,
br_code: ButtonRequestType = BR_TYPE_OTHER, br_code: ButtonRequestType = BR_TYPE_OTHER,
) -> None: ) -> None:
await confirm_action( await raise_if_not_confirmed(
br_type, interact(
title, RustLayout(
description=description, trezorui2.confirm_emphasized(
verb="ENABLE", title=title.upper(),
br_code=br_code, items=(
"Turn on ",
(True, description),
" protection?\n\n",
information,
),
verb="TURN ON",
)
),
br_type,
br_code,
)
) )
# await confirm_action(
# ctx,
# br_type,
# title,
# description=description,
# verb="TURN ON",
# br_code=br_code,
# )
async def mnemonic_word_entering() -> None: async def mnemonic_word_entering() -> None:
"""Not supported for TT.""" """Not supported for TT."""

View File

@ -308,7 +308,7 @@ class LayoutContent(UnstructuredJSONReader):
def pin(self) -> str: def pin(self) -> str:
"""Get PIN from the layout.""" """Get PIN from the layout."""
assert self.main_component() == "PinKeyboard" assert "PinKeyboard" in self.all_components()
return self.find_unique_value_by_key("pin", default="", only_type=str) return self.find_unique_value_by_key("pin", default="", only_type=str)
def passphrase(self) -> str: def passphrase(self) -> str:

View File

@ -58,7 +58,7 @@ def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int)
device_handler.run(device.apply_settings, auto_lock_delay_ms=delay_ms) # type: ignore device_handler.run(device.apply_settings, auto_lock_delay_ms=delay_ms) # type: ignore
assert debug.wait_layout().main_component() == "PinKeyboard" assert "PinKeyboard" in debug.wait_layout().all_components()
debug.input("1234") debug.input("1234")
@ -246,7 +246,7 @@ def unlock_dry_run(debug: "DebugLink") -> "LayoutContent":
in debug.wait_layout().text_content() in debug.wait_layout().text_content()
) )
layout = go_next(debug, wait=True) layout = go_next(debug, wait=True)
assert layout.main_component() == "PinKeyboard" assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4, wait=True) layout = debug.input(PIN4, wait=True)
assert layout is not None assert layout is not None
@ -276,7 +276,7 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle
# lockscreen triggered automatically # lockscreen triggered automatically
debug.wait_layout(wait_for_external_change=True) debug.wait_layout(wait_for_external_change=True)
layout = go_next(debug, wait=True) layout = go_next(debug, wait=True)
assert layout.main_component() == "PinKeyboard" assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4, wait=True) layout = debug.input(PIN4, wait=True)
assert layout is not None assert layout is not None

View File

@ -46,7 +46,7 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
# unlock with message # unlock with message
device_handler.run(common.get_test_address) device_handler.run(common.get_test_address)
assert debug.wait_layout().main_component() == "PinKeyboard" assert "PinKeyboard" in debug.wait_layout().all_components()
debug.input("1234", wait=True) debug.input("1234", wait=True)
assert device_handler.result() assert device_handler.result()
@ -69,7 +69,7 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
layout = debug.wait_layout() layout = debug.wait_layout()
else: else:
layout = debug.click(buttons.INFO, wait=True) layout = debug.click(buttons.INFO, wait=True)
assert layout.main_component() == "PinKeyboard" assert "PinKeyboard" in layout.all_components()
debug.input("1234", wait=True) debug.input("1234", wait=True)
assert device_handler.features().unlocked is True assert device_handler.features().unlocked is True

View File

@ -83,19 +83,19 @@ def prepare(
elif situation == Situation.PIN_SETUP: elif situation == Situation.PIN_SETUP:
# Set new PIN # Set new PIN
device_handler.run(device.change_pin) # type: ignore device_handler.run(device.change_pin) # type: ignore
assert "enable PIN protection" in debug.wait_layout().text_content() assert "Turn on" in debug.wait_layout().text_content()
if debug.model == "T": if debug.model == "T":
go_next(debug) go_next(debug)
elif debug.model == "R": elif debug.model == "R":
go_next(debug, wait=True) go_next(debug, wait=True)
go_next(debug, wait=True) go_next(debug, wait=True)
go_next(debug, wait=True) go_next(debug, wait=True)
debug.press_right_htc(1000) go_next(debug, wait=True)
elif situation == Situation.PIN_CHANGE: elif situation == Situation.PIN_CHANGE:
# Change PIN # Change PIN
device_handler.run(device.change_pin) # type: ignore device_handler.run(device.change_pin) # type: ignore
_input_see_confirm(debug, old_pin) _input_see_confirm(debug, old_pin)
assert "change your PIN" in debug.read_layout().text_content() assert "Change PIN" in debug.read_layout().text_content()
go_next(debug, wait=True) go_next(debug, wait=True)
_input_see_confirm(debug, old_pin) _input_see_confirm(debug, old_pin)
elif situation == Situation.WIPE_CODE_SETUP: elif situation == Situation.WIPE_CODE_SETUP:
@ -103,10 +103,12 @@ def prepare(
device_handler.run(device.change_wipe_code) # type: ignore device_handler.run(device.change_wipe_code) # type: ignore
if old_pin: if old_pin:
_input_see_confirm(debug, old_pin) _input_see_confirm(debug, old_pin)
assert "enable wipe code" in debug.wait_layout().text_content() assert "Turn on" in debug.wait_layout().text_content()
go_next(debug, wait=True) go_next(debug, wait=True)
if debug.model == "R": if debug.model == "R":
debug.press_right_htc(1000) go_next(debug, wait=True)
go_next(debug, wait=True)
go_next(debug, wait=True)
if old_pin: if old_pin:
debug.wait_layout() debug.wait_layout()
_input_see_confirm(debug, old_pin) _input_see_confirm(debug, old_pin)
@ -119,7 +121,7 @@ def prepare(
def _assert_pin_entry(debug: "DebugLink") -> None: def _assert_pin_entry(debug: "DebugLink") -> None:
assert debug.read_layout().main_component() == "PinKeyboard" assert "PinKeyboard" in debug.read_layout().all_components()
def _input_pin(debug: "DebugLink", pin: str, check: bool = False) -> None: def _input_pin(debug: "DebugLink", pin: str, check: bool = False) -> None:
@ -262,10 +264,11 @@ def test_pin_setup(device_handler: "BackgroundDeviceHandler"):
def test_pin_setup_mismatch(device_handler: "BackgroundDeviceHandler"): def test_pin_setup_mismatch(device_handler: "BackgroundDeviceHandler"):
with PIN_CANCELLED, prepare(device_handler, Situation.PIN_SETUP) as debug: with PIN_CANCELLED, prepare(device_handler, Situation.PIN_SETUP) as debug:
_enter_two_times(debug, "1", "2") _enter_two_times(debug, "1", "2")
go_next(debug)
if debug.model == "T": if debug.model == "T":
go_next(debug)
_cancel_pin(debug) _cancel_pin(debug)
elif debug.model == "R": elif debug.model == "R":
debug.press_middle()
debug.press_no() debug.press_no()

View File

@ -1278,7 +1278,7 @@ def test_prevtx_forbidden_fields(client: Client, field, value):
"field, value", "field, value",
(("expiry", 9), ("timestamp", 42), ("version_group_id", 69), ("branch_id", 13)), (("expiry", 9), ("timestamp", 42), ("version_group_id", 69), ("branch_id", 13)),
) )
def test_signtx_forbidden_fields(client: Client, field, value): def test_signtx_forbidden_fields(client: Client, field: str, value: int):
inp0 = messages.TxInputType( inp0 = messages.TxInputType(
address_n=parse_path("m/44h/0h/0h/0/0"), # 1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL address_n=parse_path("m/44h/0h/0h/0/0"), # 1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL
prev_hash=TXHASH_157041, prev_hash=TXHASH_157041,
@ -1357,7 +1357,9 @@ def test_incorrect_input_script_type(client: Client, script_type):
messages.OutputScriptType.PAYTOSCRIPTHASH, messages.OutputScriptType.PAYTOSCRIPTHASH,
), ),
) )
def test_incorrect_output_script_type(client: Client, script_type): def test_incorrect_output_script_type(
client: Client, script_type: messages.OutputScriptType
):
address_n = parse_path("m/44h/1h/0h/0/0") # mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q address_n = parse_path("m/44h/1h/0h/0/0") # mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q
attacker_multisig_public_key = bytes.fromhex( attacker_multisig_public_key = bytes.fromhex(
"030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0" "030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0"
@ -1407,7 +1409,7 @@ def test_incorrect_output_script_type(client: Client, script_type):
"lock_time, sequence", "lock_time, sequence",
((499_999_999, 0xFFFFFFFE), (500_000_000, 0xFFFFFFFE), (1, 0xFFFFFFFF)), ((499_999_999, 0xFFFFFFFE), (500_000_000, 0xFFFFFFFE), (1, 0xFFFFFFFF)),
) )
def test_lock_time(client: Client, lock_time, sequence): def test_lock_time(client: Client, lock_time: int, sequence: int):
# input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5 # input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5
inp1 = messages.TxInputType( inp1 = messages.TxInputType(
@ -1548,7 +1550,6 @@ def test_information(client: Client):
with client: with client:
IF = InputFlowSignTxInformation(client) IF = InputFlowSignTxInformation(client)
client.set_input_flow(IF.get()) client.set_input_flow(IF.get())
client.watch_layout(True)
btc.sign_tx( btc.sign_tx(
client, client,
@ -1584,7 +1585,6 @@ def test_information_mixed(client: Client):
with client: with client:
IF = InputFlowSignTxInformationMixed(client) IF = InputFlowSignTxInformationMixed(client)
client.set_input_flow(IF.get()) client.set_input_flow(IF.get())
client.watch_layout(True)
btc.sign_tx( btc.sign_tx(
client, client,
@ -1616,7 +1616,6 @@ def test_information_cancel(client: Client):
with client, pytest.raises(Cancelled): with client, pytest.raises(Cancelled):
IF = InputFlowSignTxInformationCancel(client) IF = InputFlowSignTxInformationCancel(client)
client.set_input_flow(IF.get()) client.set_input_flow(IF.get())
client.watch_layout(True)
btc.sign_tx( btc.sign_tx(
client, client,
@ -1663,7 +1662,6 @@ def test_information_replacement(client: Client):
with client: with client:
IF = InputFlowSignTxInformationReplacement(client) IF = InputFlowSignTxInformationReplacement(client)
client.set_input_flow(IF.get()) client.set_input_flow(IF.get())
client.watch_layout(True)
btc.sign_tx( btc.sign_tx(
client, client,

View File

@ -63,7 +63,7 @@ def test_set_remove_wipe_code(client: Client):
assert client.features.wipe_code_protection is False assert client.features.wipe_code_protection is False
with client: with client:
br_amount = 5 if client.debug.model == "T" else 7 br_amount = 5 if client.debug.model == "T" else 6
client.set_expected_responses( client.set_expected_responses(
[messages.ButtonRequest()] * br_amount [messages.ButtonRequest()] * br_amount
+ [messages.Success, messages.Features] + [messages.Success, messages.Features]
@ -118,7 +118,7 @@ def test_set_wipe_code_to_pin(client: Client):
_ensure_unlocked(client, PIN4) _ensure_unlocked(client, PIN4)
with client: with client:
br_amount = 7 if client.debug.model == "T" else 9 br_amount = 7 if client.debug.model == "T" else 8
client.set_expected_responses( client.set_expected_responses(
[messages.ButtonRequest()] * br_amount [messages.ButtonRequest()] * br_amount
+ [messages.Success, messages.Features] + [messages.Success, messages.Features]
@ -134,7 +134,7 @@ def test_set_wipe_code_to_pin(client: Client):
def test_set_pin_to_wipe_code(client: Client): def test_set_pin_to_wipe_code(client: Client):
# Set wipe code. # Set wipe code.
with client: with client:
br_amount = 4 if client.debug.model == "T" else 6 br_amount = 4 if client.debug.model == "T" else 5
client.set_expected_responses( client.set_expected_responses(
[messages.ButtonRequest()] * br_amount [messages.ButtonRequest()] * br_amount
+ [messages.Success, messages.Features] + [messages.Success, messages.Features]

View File

@ -53,7 +53,7 @@ def test_sd_protect_unlock(client: Client):
def input_flow_enable_sd_protect(): def input_flow_enable_sd_protect():
yield # Enter PIN to unlock device yield # Enter PIN to unlock device
assert layout().main_component() == "PinKeyboard" assert "PinKeyboard" in layout().all_components()
client.debug.input("1234") client.debug.input("1234")
yield # do you really want to enable SD protection yield # do you really want to enable SD protection
@ -61,7 +61,7 @@ def test_sd_protect_unlock(client: Client):
client.debug.press_yes() client.debug.press_yes()
yield # enter current PIN yield # enter current PIN
assert layout().main_component() == "PinKeyboard" assert "PinKeyboard" in layout().all_components()
client.debug.input("1234") client.debug.input("1234")
yield # you have successfully enabled SD protection yield # you have successfully enabled SD protection
@ -79,15 +79,15 @@ def test_sd_protect_unlock(client: Client):
client.debug.press_yes() client.debug.press_yes()
yield # enter current PIN yield # enter current PIN
assert layout().main_component() == "PinKeyboard" assert "PinKeyboard" in layout().all_components()
client.debug.input("1234") client.debug.input("1234")
yield # enter new PIN yield # enter new PIN
assert layout().main_component() == "PinKeyboard" assert "PinKeyboard" in layout().all_components()
client.debug.input("1234") client.debug.input("1234")
yield # enter new PIN again yield # enter new PIN again
assert layout().main_component() == "PinKeyboard" assert "PinKeyboard" in layout().all_components()
client.debug.input("1234") client.debug.input("1234")
yield # Pin change successful yield # Pin change successful
@ -107,7 +107,7 @@ def test_sd_protect_unlock(client: Client):
client.debug.press_yes() client.debug.press_yes()
yield # enter current PIN yield # enter current PIN
assert layout().main_component() == "PinKeyboard" assert "PinKeyboard" in layout().all_components()
client.debug.input("1234") client.debug.input("1234")
yield # SD card problem yield # SD card problem

View File

@ -1413,11 +1413,11 @@ def bip39_recovery_possible_pin(
# PIN when requested # PIN when requested
if pin is not None: if pin is not None:
yield yield
assert debug.wait_layout().main_component() == "PinKeyboard" assert "PinKeyboard" in debug.wait_layout().all_components()
debug.input(pin) debug.input(pin)
yield yield
assert debug.wait_layout().main_component() == "PinKeyboard" assert "PinKeyboard" in debug.wait_layout().all_components()
debug.input(pin) debug.input(pin)
yield yield
@ -1464,7 +1464,7 @@ class InputFlowBip39RecoveryPIN(InputFlowBase):
self.debug.input("654") self.debug.input("654")
yield yield
assert "re-enter to confirm" in self.layout().text_content() assert "re-enter PIN" in self.layout().text_content()
self.debug.press_right() self.debug.press_right()
yield yield