mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-04-23 02:29:10 +00:00
feat(core/ui): ability to cancel recovery on word count selector
This commit is contained in:
parent
6d7b039e6a
commit
64b9084b6c
core
.changelog.d
embed/rust/src/ui
mocks/generated
src/apps/management/recovery_device
tests
1
core/.changelog.d/3503.added
Normal file
1
core/.changelog.d/3503.added
Normal file
@ -0,0 +1 @@
|
||||
Added ability to cancel recovery on word count selection screen.
|
@ -1463,9 +1463,9 @@ pub static mp_module_trezorui_api: Module = obj_module! {
|
||||
/// def select_word_count(
|
||||
/// *,
|
||||
/// recovery_type: RecoveryType,
|
||||
/// ) -> LayoutObj[int | str]: # TR returns str
|
||||
/// ) -> LayoutObj[int | str | UIResult]: # TR returns str
|
||||
/// """Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
||||
/// For unlocking a repeated backup, select from 20 or 33."""
|
||||
/// For unlocking a repeated backup, select between 20 and 33."""
|
||||
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
|
||||
|
||||
/// def set_brightness(*, current: int | None = None) -> LayoutObj[UiResult]:
|
||||
|
@ -1,7 +1,10 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Grid, GridCellSpan, Rect},
|
||||
shape::Renderer,
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Grid, GridCellSpan, Rect},
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
@ -11,37 +14,90 @@ use super::super::{
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum SelectWordCountMsg {
|
||||
Selected(u32),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
type Cell = (usize, usize);
|
||||
|
||||
struct Btn {
|
||||
text: TString<'static>,
|
||||
msg: SelectWordCountMsg,
|
||||
placement: GridCellSpan,
|
||||
}
|
||||
|
||||
impl Btn {
|
||||
pub const fn new(content: &'static str, value: u32, cell: Cell) -> Self {
|
||||
Self {
|
||||
text: TString::Str(content),
|
||||
msg: SelectWordCountMsg::Selected(value),
|
||||
placement: GridCellSpan {
|
||||
from: cell,
|
||||
to: (cell.0, cell.1 + 1),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelectWordCountLayout {
|
||||
choice_buttons: &'static [Btn],
|
||||
cancel_button_placement: GridCellSpan,
|
||||
}
|
||||
|
||||
impl SelectWordCountLayout {
|
||||
/*
|
||||
* 12 | 18 | 20
|
||||
* ------------
|
||||
* x | 24 | 33
|
||||
*/
|
||||
pub const LAYOUT_ALL: SelectWordCountLayout = SelectWordCountLayout {
|
||||
choice_buttons: &[
|
||||
Btn::new("12", 12, (0, 0)),
|
||||
Btn::new("18", 18, (0, 2)),
|
||||
Btn::new("20", 20, (0, 4)),
|
||||
Btn::new("24", 24, (1, 2)),
|
||||
Btn::new("33", 33, (1, 4)),
|
||||
],
|
||||
cancel_button_placement: GridCellSpan {
|
||||
from: (1, 0),
|
||||
to: (1, 1),
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* x | 20 | 33
|
||||
*/
|
||||
pub const LAYOUT_MULTISHARE: SelectWordCountLayout = SelectWordCountLayout {
|
||||
choice_buttons: &[Btn::new("20", 20, (0, 2)), Btn::new("33", 33, (0, 4))],
|
||||
cancel_button_placement: GridCellSpan {
|
||||
from: (0, 0),
|
||||
to: (0, 1),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub struct SelectWordCount {
|
||||
keypad: ValueKeypad,
|
||||
layout: SelectWordCountLayout,
|
||||
choice_buttons: Vec<Button, 5>,
|
||||
cancel_button: Button,
|
||||
}
|
||||
|
||||
impl SelectWordCount {
|
||||
const NUMBERS_ALL: [u32; 5] = [12, 18, 20, 24, 33];
|
||||
const LABELS_ALL: [&'static str; 5] = ["12", "18", "20", "24", "33"];
|
||||
const CELLS_ALL: [(usize, usize); 5] = [(0, 0), (0, 2), (0, 4), (1, 0), (1, 2)];
|
||||
pub fn new(layout: SelectWordCountLayout) -> Self {
|
||||
let choice_buttons = layout
|
||||
.choice_buttons
|
||||
.iter()
|
||||
.map(|btn| Button::with_text(btn.text).styled(theme::button_pin()))
|
||||
.collect();
|
||||
|
||||
const NUMBERS_MULTISHARE: [u32; 2] = [20, 33];
|
||||
const LABELS_MULTISHARE: [&'static str; 2] = ["20", "33"];
|
||||
const CELLS_MULTISHARE: [(usize, usize); 2] = [(0, 0), (0, 2)];
|
||||
let cancel_button = Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel());
|
||||
|
||||
pub fn new_all() -> Self {
|
||||
Self {
|
||||
keypad: ValueKeypad::new(&Self::NUMBERS_ALL, &Self::LABELS_ALL, &Self::CELLS_ALL),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_multishare() -> Self {
|
||||
Self {
|
||||
keypad: ValueKeypad::new(
|
||||
&Self::NUMBERS_MULTISHARE,
|
||||
&Self::LABELS_MULTISHARE,
|
||||
&Self::CELLS_MULTISHARE,
|
||||
),
|
||||
layout,
|
||||
choice_buttons,
|
||||
cancel_button,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,69 +105,38 @@ impl SelectWordCount {
|
||||
impl Component for SelectWordCount {
|
||||
type Msg = SelectWordCountMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.keypad.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.keypad.event(ctx, event)
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
self.keypad.render(target)
|
||||
}
|
||||
}
|
||||
|
||||
type ValueKeyPacked = (Button, u32, (usize, usize)); // (Button, number, cell)
|
||||
|
||||
pub struct ValueKeypad {
|
||||
buttons: Vec<ValueKeyPacked, 5>,
|
||||
}
|
||||
|
||||
impl ValueKeypad {
|
||||
fn new(numbers: &[u32], labels: &[&'static str], cells: &[(usize, usize)]) -> Self {
|
||||
let mut buttons = Vec::new();
|
||||
|
||||
for ((&number, &label), &cell) in numbers.iter().zip(labels).zip(cells).take(5) {
|
||||
unwrap!(buttons.push((
|
||||
Button::with_text(label.into()).styled(theme::button_pin()),
|
||||
number,
|
||||
cell
|
||||
)));
|
||||
}
|
||||
|
||||
Self { buttons }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ValueKeypad {
|
||||
type Msg = SelectWordCountMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (_, bounds) = bounds.split_bottom(2 * theme::BUTTON_HEIGHT + theme::BUTTON_SPACING);
|
||||
let grid = Grid::new(bounds, 2, 6).with_spacing(theme::BUTTON_SPACING);
|
||||
for (btn, _, (x, y)) in self.buttons.iter_mut() {
|
||||
btn.place(grid.cells(GridCellSpan {
|
||||
from: (*x, *y),
|
||||
to: (*x, *y + 1),
|
||||
}));
|
||||
for (i, button) in self.choice_buttons.iter_mut().enumerate() {
|
||||
button.place(grid.cells(self.layout.choice_buttons[i].placement));
|
||||
}
|
||||
self.cancel_button
|
||||
.place(grid.cells(self.layout.cancel_button_placement));
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
for (i, (btn, _, _)) in self.buttons.iter_mut().enumerate() {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
return Some(SelectWordCountMsg::Selected(self.buttons[i].1));
|
||||
for (i, button) in self.choice_buttons.iter_mut().enumerate() {
|
||||
if matches!(button.event(ctx, event), Some(ButtonMsg::Clicked)) {
|
||||
return Some(self.layout.choice_buttons[i].msg);
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
self.cancel_button.event(ctx, event),
|
||||
Some(ButtonMsg::Clicked)
|
||||
) {
|
||||
return Some(SelectWordCountMsg::Cancelled);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
for btn in self.buttons.iter() {
|
||||
btn.0.render(target)
|
||||
for button in self.choice_buttons.iter() {
|
||||
button.render(target);
|
||||
}
|
||||
self.cancel_button.render(target);
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,13 +144,5 @@ impl Component for ValueKeypad {
|
||||
impl crate::trace::Trace for SelectWordCount {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("SelectWordCount");
|
||||
t.child("keypad", &self.keypad);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for ValueKeypad {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ValueKeypad");
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ pub use keyboard::{
|
||||
passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
|
||||
pin::{PinKeyboard, PinKeyboardMsg},
|
||||
slip39::Slip39Input,
|
||||
word_count::{SelectWordCount, SelectWordCountMsg},
|
||||
word_count::{SelectWordCount, SelectWordCountLayout, SelectWordCountMsg},
|
||||
};
|
||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||
#[cfg(feature = "translations")]
|
||||
|
@ -64,6 +64,7 @@ impl TryFrom<SelectWordCountMsg> for Obj {
|
||||
fn try_from(value: SelectWordCountMsg) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
SelectWordCountMsg::Selected(i) => i.try_into(),
|
||||
SelectWordCountMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ use super::{
|
||||
check_homescreen_format, AddressDetails, Bip39Input, Button, ButtonMsg, ButtonPage,
|
||||
ButtonStyleSheet, CancelConfirmMsg, CoinJoinProgress, Dialog, FidoConfirm, Frame,
|
||||
Homescreen, IconDialog, Lockscreen, MnemonicKeyboard, NumberInputDialog,
|
||||
PassphraseKeyboard, PinKeyboard, Progress, SelectWordCount, SetBrightnessDialog,
|
||||
ShareWords, SimplePage, Slip39Input,
|
||||
PassphraseKeyboard, PinKeyboard, Progress, SelectWordCount, SelectWordCountLayout,
|
||||
SetBrightnessDialog, ShareWords, SimplePage, Slip39Input,
|
||||
},
|
||||
fonts, theme, UIBolt,
|
||||
};
|
||||
@ -700,16 +700,17 @@ impl FirmwareUI for UIBolt {
|
||||
TR::recovery__num_of_words,
|
||||
));
|
||||
|
||||
let content = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||
SelectWordCount::new_multishare()
|
||||
} else {
|
||||
SelectWordCount::new_all()
|
||||
};
|
||||
|
||||
let selector = SelectWordCount::new(
|
||||
if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||
SelectWordCountLayout::LAYOUT_MULTISHARE
|
||||
} else {
|
||||
SelectWordCountLayout::LAYOUT_ALL
|
||||
},
|
||||
);
|
||||
let layout = RootComponent::new(Frame::left_aligned(
|
||||
theme::label_title(),
|
||||
title,
|
||||
Dialog::new(paragraphs, content),
|
||||
Dialog::new(paragraphs, selector),
|
||||
));
|
||||
Ok(layout)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
component::{ButtonLayout, Choice, ChoiceFactory, ChoicePage},
|
||||
component::{ButtonLayout, Choice, ChoiceFactory, ChoiceMsg, ChoicePage},
|
||||
fonts,
|
||||
theme::bootloader::{BLD_BG, BLD_FG, ICON_EXIT, ICON_REDO, ICON_TRASH},
|
||||
};
|
||||
@ -140,11 +140,7 @@ impl Menu {
|
||||
let choices = MenuChoiceFactory::new(firmware_present);
|
||||
Self {
|
||||
pad: Pad::with_background(BLD_BG).with_clear(),
|
||||
choice_page: Child::new(
|
||||
ChoicePage::new(choices)
|
||||
.with_carousel(true)
|
||||
.with_only_one_item(true),
|
||||
),
|
||||
choice_page: Child::new(ChoicePage::new(choices).with_only_one_item(true)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,7 +155,10 @@ impl Component for Menu {
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.choice_page.event(ctx, event).map(|evt| evt.0)
|
||||
match self.choice_page.event(ctx, event) {
|
||||
Some(ChoiceMsg::Choice { item, .. }) => Some(item),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
|
@ -11,6 +11,11 @@ use super::super::{
|
||||
|
||||
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
|
||||
|
||||
pub enum ChoiceMsg<T> {
|
||||
Choice { item: T, long_press: bool },
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub trait Choice {
|
||||
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool);
|
||||
|
||||
@ -50,6 +55,12 @@ pub trait ChoiceFactory {
|
||||
fn get(&self, index: usize) -> (Self::Item, Self::Action);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone)]
|
||||
pub enum ChoiceControls {
|
||||
Carousel,
|
||||
Cancellable,
|
||||
}
|
||||
|
||||
/// General component displaying a set of items on the screen
|
||||
/// and allowing the user to select one of them.
|
||||
///
|
||||
@ -60,9 +71,6 @@ pub trait ChoiceFactory {
|
||||
/// Each `Choice` is responsible for setting the screen -
|
||||
/// choosing the button text, their duration, text displayed
|
||||
/// on screen etc.
|
||||
///
|
||||
/// `is_carousel` can be used to make the choice page "infinite" -
|
||||
/// after reaching one end, users will appear at the other end.
|
||||
pub struct ChoicePage<F, A>
|
||||
where
|
||||
F: ChoiceFactory<Action = A>,
|
||||
@ -73,8 +81,9 @@ where
|
||||
page_counter: usize,
|
||||
/// How many pixels are between the items.
|
||||
items_distance: i16,
|
||||
/// Whether the choice page is "infinite" (carousel).
|
||||
is_carousel: bool,
|
||||
/// Whether the choice page is cancelable
|
||||
/// or a carousel.
|
||||
controls: ChoiceControls,
|
||||
/// Whether we should show items on left/right even when they cannot
|
||||
/// be painted entirely (they would be cut off).
|
||||
show_incomplete: bool,
|
||||
@ -116,7 +125,7 @@ where
|
||||
),
|
||||
page_counter: 0,
|
||||
items_distance: DEFAULT_ITEMS_DISTANCE,
|
||||
is_carousel: false,
|
||||
controls: ChoiceControls::Cancellable,
|
||||
show_incomplete: false,
|
||||
show_only_one_item: false,
|
||||
inverse_selected_item: false,
|
||||
@ -138,9 +147,8 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Enabling the carousel mode.
|
||||
pub fn with_carousel(mut self, carousel: bool) -> Self {
|
||||
self.is_carousel = carousel;
|
||||
pub fn with_controls(mut self, controls: ChoiceControls) -> Self {
|
||||
self.controls = controls;
|
||||
self
|
||||
}
|
||||
|
||||
@ -172,13 +180,13 @@ where
|
||||
ctx: &mut EventCtx,
|
||||
new_choices: F,
|
||||
new_page_counter: Option<usize>,
|
||||
is_carousel: bool,
|
||||
controls: ChoiceControls,
|
||||
) {
|
||||
self.choices = new_choices;
|
||||
if let Some(new_counter) = new_page_counter {
|
||||
self.page_counter = new_counter;
|
||||
}
|
||||
self.is_carousel = is_carousel;
|
||||
self.controls = controls;
|
||||
self.update(ctx);
|
||||
}
|
||||
|
||||
@ -239,12 +247,12 @@ where
|
||||
let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
|
||||
|
||||
// Possibly drawing on the left side.
|
||||
if self.has_previous_choice() || self.is_carousel {
|
||||
if self.has_previous_choice() || self.controls == ChoiceControls::Carousel {
|
||||
self.show_left_choices(target, left_area);
|
||||
}
|
||||
|
||||
// Possibly drawing on the right side.
|
||||
if self.has_next_choice() || self.is_carousel {
|
||||
if self.has_next_choice() || self.controls == ChoiceControls::Carousel {
|
||||
self.show_right_choices(target, right_area);
|
||||
}
|
||||
}
|
||||
@ -307,7 +315,7 @@ where
|
||||
// Breaking out of the loop if we exhausted left items
|
||||
// and the carousel mode is not enabled.
|
||||
if page_index < 0 {
|
||||
if self.is_carousel {
|
||||
if self.controls == ChoiceControls::Carousel {
|
||||
// Moving to the last page.
|
||||
page_index = self.last_page_index() as i16;
|
||||
} else {
|
||||
@ -348,7 +356,7 @@ where
|
||||
// Breaking out of the loop if we exhausted right items
|
||||
// and the carousel mode is not enabled.
|
||||
if page_index > self.last_page_index() {
|
||||
if self.is_carousel {
|
||||
if self.controls == ChoiceControls::Carousel {
|
||||
// Moving to the first page.
|
||||
page_index = 0;
|
||||
} else {
|
||||
@ -428,8 +436,10 @@ where
|
||||
/// Check possibility of going left/right.
|
||||
fn can_move(&self, button: ButtonPos) -> bool {
|
||||
match button {
|
||||
ButtonPos::Left => self.has_previous_choice() || self.is_carousel,
|
||||
ButtonPos::Right => self.has_next_choice() || self.is_carousel,
|
||||
ButtonPos::Left => {
|
||||
self.has_previous_choice() || self.controls == ChoiceControls::Carousel
|
||||
}
|
||||
ButtonPos::Right => self.has_next_choice() || self.controls == ChoiceControls::Carousel,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -439,7 +449,7 @@ where
|
||||
if self.has_previous_choice() {
|
||||
self.decrease_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
} else if self.controls == ChoiceControls::Carousel {
|
||||
self.page_counter_to_max();
|
||||
self.update(ctx);
|
||||
}
|
||||
@ -450,7 +460,7 @@ where
|
||||
if self.has_next_choice() {
|
||||
self.increase_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
} else if self.controls == ChoiceControls::Carousel {
|
||||
self.page_counter_to_zero();
|
||||
self.update(ctx);
|
||||
}
|
||||
@ -486,7 +496,7 @@ impl<F, A> Component for ChoicePage<F, A>
|
||||
where
|
||||
F: ChoiceFactory<Action = A>,
|
||||
{
|
||||
type Msg = (A, bool);
|
||||
type Msg = ChoiceMsg<A>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
@ -558,9 +568,13 @@ where
|
||||
if let Some(ButtonControllerMsg::Triggered(pos, long_press)) = button_event {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
// Clicked BACK. Decrease the page counter.
|
||||
// In case of carousel going to the right end.
|
||||
self.move_left(ctx);
|
||||
if self.controls == ChoiceControls::Cancellable && self.page_counter == 0 {
|
||||
return Some(ChoiceMsg::<A>::Cancel);
|
||||
} else {
|
||||
// Clicked BACK. Decrease the page counter.
|
||||
// In case of carousel going to the right end.
|
||||
self.move_left(ctx);
|
||||
}
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
// Clicked NEXT. Increase the page counter.
|
||||
@ -570,7 +584,10 @@ where
|
||||
ButtonPos::Middle => {
|
||||
// Clicked SELECT. Send current choice index with information about long-press
|
||||
self.clear_and_repaint(ctx);
|
||||
return Some((self.get_current_action(), long_press));
|
||||
return Some(ChoiceMsg::Choice {
|
||||
item: self.get_current_action(),
|
||||
long_press,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -583,7 +600,10 @@ where
|
||||
buttons.reset_state(ctx);
|
||||
});
|
||||
self.clear_and_repaint(ctx);
|
||||
return Some((self.get_current_action(), true));
|
||||
return Some(ChoiceMsg::Choice {
|
||||
item: self.get_current_action(),
|
||||
long_press: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
// The middle button was pressed, highlighting the current choice by color
|
||||
@ -614,11 +634,11 @@ where
|
||||
t.component("ChoicePage");
|
||||
t.int("active_page", self.page_counter as i64);
|
||||
t.int("page_count", self.choices.count() as i64);
|
||||
t.bool("is_carousel", self.is_carousel);
|
||||
t.bool("is_carousel", self.controls == ChoiceControls::Carousel);
|
||||
|
||||
if self.has_previous_choice() {
|
||||
t.child("prev_choice", &self.choices.get(self.page_counter - 1).0);
|
||||
} else if self.is_carousel {
|
||||
} else if self.controls == ChoiceControls::Carousel {
|
||||
// In case of carousel going to the left end.
|
||||
t.child("prev_choice", &self.choices.get(self.last_page_index()).0);
|
||||
}
|
||||
@ -627,7 +647,7 @@ where
|
||||
|
||||
if self.has_next_choice() {
|
||||
t.child("next_choice", &self.choices.get(self.page_counter + 1).0);
|
||||
} else if self.is_carousel {
|
||||
} else if self.controls == ChoiceControls::Carousel {
|
||||
// In case of carousel going to the very left.
|
||||
t.child("next_choice", &self.choices.get(0).0);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage};
|
||||
use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoiceMsg, ChoicePage};
|
||||
|
||||
struct ChoiceFactoryNumberInput {
|
||||
min: u32,
|
||||
@ -69,14 +69,14 @@ impl NumberInput {
|
||||
}
|
||||
|
||||
impl Component for NumberInput {
|
||||
type Msg = u32;
|
||||
type Msg = ChoiceMsg<u32>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.choice_page.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.choice_page.event(ctx, event).map(|evt| evt.0)
|
||||
self.choice_page.event(ctx, event)
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
|
@ -12,8 +12,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
theme, ButtonDetails, ButtonLayout, CancelConfirmMsg, ChangingTextLine, ChoiceFactory,
|
||||
ChoiceItem, ChoicePage,
|
||||
theme, ButtonDetails, ButtonLayout, CancelConfirmMsg, ChangingTextLine, ChoiceControls,
|
||||
ChoiceFactory, ChoiceItem, ChoiceMsg, ChoicePage,
|
||||
};
|
||||
|
||||
/// Defines the choices currently available on the screen
|
||||
@ -279,8 +279,8 @@ impl PassphraseEntry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
choice_page: ChoicePage::new(ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, true))
|
||||
.with_carousel(true)
|
||||
.with_initial_page_counter(random_menu_position()),
|
||||
.with_initial_page_counter(random_menu_position())
|
||||
.with_controls(ChoiceControls::Carousel),
|
||||
passphrase_dots: Child::new(ChangingTextLine::center_mono("", MAX_PASSPHRASE_LENGTH)),
|
||||
show_plain_passphrase: false,
|
||||
show_last_digit: false,
|
||||
@ -334,8 +334,12 @@ impl PassphraseEntry {
|
||||
/// Displaying the MENU
|
||||
fn show_menu_page(&mut self, ctx: &mut EventCtx) {
|
||||
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, self.is_empty());
|
||||
self.choice_page
|
||||
.reset(ctx, menu_choices, Some(random_menu_position()), true);
|
||||
self.choice_page.reset(
|
||||
ctx,
|
||||
menu_choices,
|
||||
Some(random_menu_position()),
|
||||
ChoiceControls::Carousel,
|
||||
);
|
||||
}
|
||||
|
||||
/// Displaying the character category
|
||||
@ -345,7 +349,7 @@ impl PassphraseEntry {
|
||||
ctx,
|
||||
category_choices,
|
||||
Some(random_category_position(&self.current_category)),
|
||||
true,
|
||||
ChoiceControls::Carousel,
|
||||
);
|
||||
}
|
||||
|
||||
@ -396,54 +400,70 @@ impl Component for PassphraseEntry {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||
match action {
|
||||
PassphraseAction::CancelOrDelete => {
|
||||
if self.is_empty() {
|
||||
return Some(CancelConfirmMsg::Cancelled);
|
||||
match self.choice_page.event(ctx, event) {
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PassphraseAction::CancelOrDelete,
|
||||
long_press,
|
||||
}) => {
|
||||
if self.is_empty() {
|
||||
return Some(CancelConfirmMsg::Cancelled);
|
||||
} else {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
self.delete_all_digits(ctx);
|
||||
} else {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
self.delete_all_digits(ctx);
|
||||
} else {
|
||||
self.delete_last_digit(ctx);
|
||||
}
|
||||
self.update_passphrase_dots(ctx);
|
||||
if self.is_empty() {
|
||||
// Allowing for DELETE/CANCEL change
|
||||
self.show_menu_page(ctx);
|
||||
}
|
||||
ctx.request_paint();
|
||||
self.delete_last_digit(ctx);
|
||||
}
|
||||
}
|
||||
PassphraseAction::Enter => {
|
||||
return Some(CancelConfirmMsg::Confirmed);
|
||||
}
|
||||
PassphraseAction::Show => {
|
||||
self.show_plain_passphrase = true;
|
||||
self.update_passphrase_dots(ctx);
|
||||
if self.is_empty() {
|
||||
// Allowing for DELETE/CANCEL change
|
||||
self.show_menu_page(ctx);
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
PassphraseAction::Category(category) => {
|
||||
self.current_category = category;
|
||||
self.show_category_page(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
PassphraseAction::Menu => {
|
||||
self.current_category = ChoiceCategory::Menu;
|
||||
self.show_menu_page(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
PassphraseAction::Character(ch) if !self.is_full() => {
|
||||
self.append_char(ctx, ch);
|
||||
self.show_last_digit = true;
|
||||
self.update_passphrase_dots(ctx);
|
||||
self.randomize_category_position(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PassphraseAction::Enter,
|
||||
..
|
||||
}) => {
|
||||
return Some(CancelConfirmMsg::Confirmed);
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PassphraseAction::Show,
|
||||
..
|
||||
}) => {
|
||||
self.show_plain_passphrase = true;
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PassphraseAction::Category(category),
|
||||
..
|
||||
}) => {
|
||||
self.current_category = category;
|
||||
self.show_category_page(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PassphraseAction::Menu,
|
||||
..
|
||||
}) => {
|
||||
self.current_category = ChoiceCategory::Menu;
|
||||
self.show_menu_page(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PassphraseAction::Character(ch),
|
||||
..
|
||||
}) if !self.is_full() => {
|
||||
self.append_char(ctx, ch);
|
||||
self.show_last_digit = true;
|
||||
self.update_passphrase_dots(ctx);
|
||||
self.randomize_category_position(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ use crate::{
|
||||
|
||||
use super::super::{
|
||||
super::fonts, theme, ButtonDetails, ButtonLayout, CancelConfirmMsg, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage,
|
||||
ChoiceControls, ChoiceFactory, ChoiceItem, ChoiceMsg, ChoicePage,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@ -171,7 +171,7 @@ impl<'a> PinEntry<'a> {
|
||||
// Starting at a random digit.
|
||||
choice_page: ChoicePage::new(ChoiceFactoryPIN)
|
||||
.with_initial_page_counter(get_random_digit_position())
|
||||
.with_carousel(true),
|
||||
.with_controls(ChoiceControls::Carousel),
|
||||
header_line: Child::new(
|
||||
header_line_content
|
||||
.map(|s| ChangingTextLine::center_bold(s, MAX_PIN_LENGTH))
|
||||
@ -295,38 +295,49 @@ impl Component for PinEntry<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||
match action {
|
||||
PinAction::Delete => {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
self.textbox.clear(ctx);
|
||||
} else {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
self.update(ctx);
|
||||
match self.choice_page.event(ctx, event) {
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PinAction::Delete,
|
||||
long_press,
|
||||
}) => {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
self.textbox.clear(ctx);
|
||||
} else {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
PinAction::Show => {
|
||||
self.show_real_pin = true;
|
||||
self.update(ctx);
|
||||
}
|
||||
PinAction::Enter if !self.is_empty() => {
|
||||
// ENTER is not valid when the PIN is empty
|
||||
return Some(CancelConfirmMsg::Confirmed);
|
||||
}
|
||||
PinAction::Digit(ch) if !self.is_full() => {
|
||||
self.textbox.append(ctx, ch);
|
||||
// Choosing random digit to be shown next
|
||||
self.choice_page
|
||||
.set_page_counter(ctx, get_random_digit_position(), true);
|
||||
self.show_last_digit = true;
|
||||
self.timeout_timer
|
||||
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||
self.update(ctx);
|
||||
}
|
||||
_ => {}
|
||||
self.update(ctx);
|
||||
}
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PinAction::Show,
|
||||
..
|
||||
}) => {
|
||||
self.show_real_pin = true;
|
||||
self.update(ctx);
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PinAction::Enter,
|
||||
..
|
||||
}) if !self.is_empty() => {
|
||||
// ENTER is not valid when the PIN is empty
|
||||
return Some(CancelConfirmMsg::Confirmed);
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: PinAction::Digit(ch),
|
||||
..
|
||||
}) if !self.is_full() => {
|
||||
self.textbox.append(ctx, ch);
|
||||
// Choosing random digit to be shown next
|
||||
self.choice_page
|
||||
.set_page_counter(ctx, get_random_digit_position(), true);
|
||||
self.show_last_digit = true;
|
||||
self.timeout_timer
|
||||
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||
self.update(ctx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,9 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage};
|
||||
use super::super::{
|
||||
ButtonDetails, ButtonLayout, ChoiceControls, ChoiceFactory, ChoiceItem, ChoiceMsg, ChoicePage,
|
||||
};
|
||||
use heapless::Vec;
|
||||
|
||||
// So that there is only one implementation, and not multiple generic ones
|
||||
@ -17,12 +19,12 @@ const MAX_LENGTH: usize = 5;
|
||||
|
||||
struct ChoiceFactorySimple {
|
||||
choices: Vec<TString<'static>, MAX_LENGTH>,
|
||||
carousel: bool,
|
||||
controls: ChoiceControls,
|
||||
}
|
||||
|
||||
impl ChoiceFactorySimple {
|
||||
fn new(choices: Vec<TString<'static>, MAX_LENGTH>, carousel: bool) -> Self {
|
||||
Self { choices, carousel }
|
||||
fn new(choices: Vec<TString<'static>, MAX_LENGTH>, controls: ChoiceControls) -> Self {
|
||||
Self { choices, controls }
|
||||
}
|
||||
|
||||
fn get_string(&self, choice_index: usize) -> TString<'static> {
|
||||
@ -48,10 +50,14 @@ impl ChoiceFactory for ChoiceFactorySimple {
|
||||
});
|
||||
|
||||
// Disabling prev/next buttons for the first/last choice when not in carousel.
|
||||
// (could be done to the same button if there is only one)
|
||||
if !self.carousel {
|
||||
// (could be done to the same item if there is only one)
|
||||
if self.controls != ChoiceControls::Carousel {
|
||||
if choice_index == 0 {
|
||||
choice_item.set_left_btn(None);
|
||||
if self.controls == ChoiceControls::Cancellable {
|
||||
choice_item.set_left_btn(Some(ButtonDetails::cancel_icon()));
|
||||
} else {
|
||||
choice_item.set_left_btn(None);
|
||||
}
|
||||
}
|
||||
if choice_index == self.count() - 1 {
|
||||
choice_item.set_right_btn(None);
|
||||
@ -70,10 +76,11 @@ pub struct SimpleChoice {
|
||||
}
|
||||
|
||||
impl SimpleChoice {
|
||||
pub fn new(str_choices: Vec<TString<'static>, MAX_LENGTH>, carousel: bool) -> Self {
|
||||
let choices = ChoiceFactorySimple::new(str_choices, carousel);
|
||||
pub fn new(str_choices: Vec<TString<'static>, MAX_LENGTH>, controls: ChoiceControls) -> Self {
|
||||
let choices = ChoiceFactorySimple::new(str_choices, controls);
|
||||
let choice_page = ChoicePage::new(choices).with_controls(controls);
|
||||
Self {
|
||||
choice_page: ChoicePage::new(choices).with_carousel(carousel),
|
||||
choice_page,
|
||||
return_index: false,
|
||||
}
|
||||
}
|
||||
@ -103,14 +110,14 @@ impl SimpleChoice {
|
||||
}
|
||||
|
||||
impl Component for SimpleChoice {
|
||||
type Msg = usize;
|
||||
type Msg = ChoiceMsg<usize>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.choice_page.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.choice_page.event(ctx, event).map(|evt| evt.0)
|
||||
self.choice_page.event(ctx, event)
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
|
@ -9,7 +9,10 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{theme, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage};
|
||||
use super::super::{
|
||||
theme, ButtonLayout, ChangingTextLine, ChoiceControls, ChoiceFactory, ChoiceItem, ChoiceMsg,
|
||||
ChoicePage,
|
||||
};
|
||||
use heapless::Vec;
|
||||
|
||||
enum WordlistAction {
|
||||
@ -173,8 +176,8 @@ impl WordlistEntry {
|
||||
// Starting at random letter position
|
||||
choice_page: ChoicePage::new(choices)
|
||||
.with_incomplete(true)
|
||||
.with_carousel(true)
|
||||
.with_initial_page_counter(get_random_position(choices_count)),
|
||||
.with_initial_page_counter(get_random_position(choices_count))
|
||||
.with_controls(ChoiceControls::Carousel),
|
||||
chosen_letters: Child::new(ChangingTextLine::center_mono(PROMPT, LINE_CAPACITY)),
|
||||
textbox: TextBox::empty(MAX_WORD_LENGTH),
|
||||
offer_words: false,
|
||||
@ -253,8 +256,12 @@ impl WordlistEntry {
|
||||
let new_page_counter = self.get_new_page_counter(&new_choices);
|
||||
// Not using carousel in case of words, as that looks weird in case
|
||||
// there is only one word to choose from.
|
||||
self.choice_page
|
||||
.reset(ctx, new_choices, Some(new_page_counter), !self.offer_words);
|
||||
self.choice_page.reset(
|
||||
ctx,
|
||||
new_choices,
|
||||
Some(new_page_counter),
|
||||
ChoiceControls::Carousel,
|
||||
);
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
@ -280,31 +287,41 @@ impl Component for WordlistEntry {
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||
match action {
|
||||
WordlistAction::Previous => {
|
||||
if self.can_go_back {
|
||||
return Some("");
|
||||
}
|
||||
}
|
||||
WordlistAction::Delete => {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
self.textbox.clear(ctx);
|
||||
} else {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
self.update(ctx);
|
||||
}
|
||||
WordlistAction::Letter(letter) => {
|
||||
self.textbox.append(ctx, letter);
|
||||
self.update(ctx);
|
||||
}
|
||||
WordlistAction::Word(word) => {
|
||||
return Some(word);
|
||||
}
|
||||
match self.choice_page.event(ctx, event) {
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: WordlistAction::Previous,
|
||||
..
|
||||
}) if self.can_go_back => {
|
||||
return Some("");
|
||||
}
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: WordlistAction::Delete,
|
||||
long_press,
|
||||
}) => {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
self.textbox.clear(ctx);
|
||||
} else {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
self.update(ctx);
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: WordlistAction::Letter(letter),
|
||||
..
|
||||
}) => {
|
||||
self.textbox.append(ctx, letter);
|
||||
self.update(ctx);
|
||||
}
|
||||
Some(ChoiceMsg::Choice {
|
||||
item: WordlistAction::Word(word),
|
||||
..
|
||||
}) => {
|
||||
return Some(word);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ pub use common_messages::CancelConfirmMsg;
|
||||
pub use error::ErrorScreen;
|
||||
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
|
||||
pub use input_methods::{
|
||||
choice::{Choice, ChoiceFactory, ChoicePage},
|
||||
choice::{Choice, ChoiceControls, ChoiceFactory, ChoiceMsg, ChoicePage},
|
||||
choice_item::ChoiceItem,
|
||||
};
|
||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet, ProgressLoader};
|
||||
|
@ -117,17 +117,25 @@ impl ComponentMsgObj for CoinJoinProgress {
|
||||
|
||||
impl ComponentMsgObj for NumberInput {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
(CONFIRMED.as_obj(), msg.try_into()?).try_into()
|
||||
match msg {
|
||||
Self::Msg::Cancel => (CANCELLED.as_obj(), 0.try_into()?).try_into(),
|
||||
Self::Msg::Choice { item, .. } => (CONFIRMED.as_obj(), item.try_into()?).try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentMsgObj for SimpleChoice {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
if self.return_index {
|
||||
msg.try_into()
|
||||
} else {
|
||||
let text = self.result_by_index(msg);
|
||||
text.try_into()
|
||||
match msg {
|
||||
Self::Msg::Cancel => Ok(CANCELLED.as_obj()),
|
||||
Self::Msg::Choice { item, .. } => {
|
||||
if self.return_index {
|
||||
item.try_into()
|
||||
} else {
|
||||
let text = self.result_by_index(item);
|
||||
text.try_into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,10 +34,10 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
component::{
|
||||
AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, CoinJoinProgress,
|
||||
ConfirmHomescreen, Flow, FlowPages, Frame, Homescreen, Lockscreen, NumberInput, Page,
|
||||
PassphraseEntry, PinEntry, Progress, ScrollableFrame, ShareWords, ShowMore, SimpleChoice,
|
||||
WordlistEntry, WordlistType,
|
||||
AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, ChoiceControls,
|
||||
CoinJoinProgress, ConfirmHomescreen, Flow, FlowPages, Frame, Homescreen, Lockscreen,
|
||||
NumberInput, Page, PassphraseEntry, PinEntry, Progress, ScrollableFrame, ShareWords,
|
||||
ShowMore, SimpleChoice, WordlistEntry, WordlistType,
|
||||
},
|
||||
constant, fonts, theme, UICaesar,
|
||||
};
|
||||
@ -887,7 +887,7 @@ impl FirmwareUI for UICaesar {
|
||||
let layout = RootComponent::new(
|
||||
Frame::new(
|
||||
description,
|
||||
SimpleChoice::new(words, false)
|
||||
SimpleChoice::new(words, ChoiceControls::Carousel)
|
||||
.with_show_incomplete()
|
||||
.with_return_index(),
|
||||
)
|
||||
@ -904,12 +904,14 @@ impl FirmwareUI for UICaesar {
|
||||
} else {
|
||||
&["12", "18", "20", "24", "33"]
|
||||
};
|
||||
|
||||
nums.iter().map(|&num| num.into()).collect()
|
||||
};
|
||||
|
||||
let layout = RootComponent::new(
|
||||
Frame::new(title, SimpleChoice::new(choices, false)).with_title_centered(),
|
||||
Frame::new(
|
||||
title,
|
||||
SimpleChoice::new(choices, ChoiceControls::Cancellable),
|
||||
)
|
||||
.with_title_centered(),
|
||||
);
|
||||
Ok(layout)
|
||||
}
|
||||
|
@ -1,41 +1,116 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Alignment, Grid, GridCellSpan, Rect},
|
||||
shape::Renderer,
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Alignment, Grid, GridCellSpan, Rect},
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
use super::super::super::{
|
||||
component::{
|
||||
button::{Button, ButtonContent, ButtonMsg},
|
||||
BinarySelection, BinarySelectionMsg,
|
||||
},
|
||||
component::button::{Button, ButtonMsg},
|
||||
cshape, theme,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum SelectWordCountMsg {
|
||||
Selected(u32),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
// We allow large_enum_variant here because the code is simpler and the larger
|
||||
// variant (ValueKeypad) predates the smaller one.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum SelectWordCount {
|
||||
All(ValueKeypad),
|
||||
Multishare(BinarySelection),
|
||||
type Cell = (usize, usize);
|
||||
|
||||
struct Btn {
|
||||
text: TString<'static>,
|
||||
msg: SelectWordCountMsg,
|
||||
placement: GridCellSpan,
|
||||
}
|
||||
|
||||
impl Btn {
|
||||
pub const fn new(content: &'static str, value: u32, cell: Cell) -> Self {
|
||||
Self {
|
||||
text: TString::Str(content),
|
||||
msg: SelectWordCountMsg::Selected(value),
|
||||
placement: GridCellSpan {
|
||||
from: cell,
|
||||
to: (cell.0 + 1, cell.1 + 1),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelectWordCountLayout {
|
||||
choice_buttons: &'static [Btn],
|
||||
cancel_button_placement: GridCellSpan,
|
||||
}
|
||||
|
||||
impl SelectWordCountLayout {
|
||||
/*
|
||||
* 12 | 18
|
||||
* -------
|
||||
* 20 | 24
|
||||
* -------
|
||||
* x | 33
|
||||
*/
|
||||
pub const LAYOUT_ALL: SelectWordCountLayout = SelectWordCountLayout {
|
||||
choice_buttons: &[
|
||||
Btn::new("12", 12, (0, 0)),
|
||||
Btn::new("18", 18, (0, 2)),
|
||||
Btn::new("20", 20, (2, 0)),
|
||||
Btn::new("24", 24, (2, 2)),
|
||||
Btn::new("33", 33, (4, 2)),
|
||||
],
|
||||
cancel_button_placement: GridCellSpan {
|
||||
from: (4, 0),
|
||||
to: (5, 1),
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* 20 | 33
|
||||
* -------
|
||||
* x
|
||||
*/
|
||||
pub const LAYOUT_MULTISHARE: SelectWordCountLayout = SelectWordCountLayout {
|
||||
choice_buttons: &[Btn::new("20", 20, (0, 0)), Btn::new("33", 33, (0, 2))],
|
||||
cancel_button_placement: GridCellSpan {
|
||||
from: (2, 0),
|
||||
to: (3, 3),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub struct SelectWordCount {
|
||||
keypad_area: Rect,
|
||||
layout: SelectWordCountLayout,
|
||||
choice_buttons: Vec<Button, 5>,
|
||||
cancel_button: Button,
|
||||
}
|
||||
|
||||
impl SelectWordCount {
|
||||
pub fn new_all() -> Self {
|
||||
Self::All(ValueKeypad::new())
|
||||
}
|
||||
pub fn new(layout: SelectWordCountLayout) -> Self {
|
||||
let choice_buttons = layout
|
||||
.choice_buttons
|
||||
.iter()
|
||||
.map(|btn| {
|
||||
Button::with_text(btn.text)
|
||||
.styled(theme::button_keyboard())
|
||||
.with_text_align(Alignment::Center)
|
||||
})
|
||||
.collect();
|
||||
|
||||
pub fn new_multishare() -> Self {
|
||||
Self::Multishare(BinarySelection::new(
|
||||
ButtonContent::Text("20".into()),
|
||||
ButtonContent::Text("33".into()),
|
||||
theme::button_keyboard(),
|
||||
theme::button_keyboard(),
|
||||
))
|
||||
let cancel_button = Button::with_icon(theme::ICON_CLOSE)
|
||||
.styled(theme::button_cancel())
|
||||
.with_text_align(Alignment::Center);
|
||||
|
||||
Self {
|
||||
keypad_area: Rect::zero(),
|
||||
layout,
|
||||
choice_buttons,
|
||||
cancel_button,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,91 +118,42 @@ impl Component for SelectWordCount {
|
||||
type Msg = SelectWordCountMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
match self {
|
||||
SelectWordCount::All(full_selector) => full_selector.place(bounds),
|
||||
SelectWordCount::Multishare(bin_selector) => bin_selector.place(bounds),
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match self {
|
||||
SelectWordCount::All(full_selector) => full_selector.event(ctx, event),
|
||||
SelectWordCount::Multishare(bin_selector) => {
|
||||
if let Some(m) = bin_selector.event(ctx, event) {
|
||||
return match m {
|
||||
BinarySelectionMsg::Left => Some(SelectWordCountMsg::Selected(20)),
|
||||
BinarySelectionMsg::Right => Some(SelectWordCountMsg::Selected(33)),
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
match self {
|
||||
SelectWordCount::All(full_selector) => full_selector.render(target),
|
||||
SelectWordCount::Multishare(bin_selector) => bin_selector.render(target),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ValueKeypad {
|
||||
button: [Button; Self::NUMBERS.len()],
|
||||
keypad_area: Rect,
|
||||
}
|
||||
|
||||
impl ValueKeypad {
|
||||
const NUMBERS: [u32; 5] = [12, 18, 20, 24, 33];
|
||||
const LABELS: [&'static str; 5] = ["12", "18", "20", "24", "33"];
|
||||
const CELLS: [(usize, usize); 5] = [(0, 0), (0, 2), (1, 0), (1, 2), (2, 1)];
|
||||
|
||||
fn new() -> Self {
|
||||
ValueKeypad {
|
||||
button: Self::LABELS.map(|t| {
|
||||
Button::with_text(t.into())
|
||||
.styled(theme::button_keyboard())
|
||||
.with_text_align(Alignment::Center)
|
||||
}),
|
||||
keypad_area: Rect::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ValueKeypad {
|
||||
type Msg = SelectWordCountMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let n_rows: usize = 3;
|
||||
let n_rows: usize = self.layout.choice_buttons.len() + 1;
|
||||
let n_cols: usize = 4;
|
||||
|
||||
let (_, bounds) = bounds.split_bottom(
|
||||
n_rows as i16 * theme::BUTTON_HEIGHT + (n_rows as i16 - 1) * theme::BUTTON_SPACING,
|
||||
);
|
||||
let grid = Grid::new(bounds, n_rows, n_cols).with_spacing(theme::BUTTON_SPACING);
|
||||
for (btn, (x, y)) in self.button.iter_mut().zip(Self::CELLS) {
|
||||
btn.place(grid.cells(GridCellSpan {
|
||||
from: (x, y),
|
||||
to: (x, y + 1),
|
||||
}));
|
||||
for (i, button) in self.choice_buttons.iter_mut().enumerate() {
|
||||
button.place(grid.cells(self.layout.choice_buttons[i].placement));
|
||||
}
|
||||
self.cancel_button
|
||||
.place(grid.cells(self.layout.cancel_button_placement));
|
||||
self.keypad_area = grid.area;
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
for (i, btn) in self.button.iter_mut().enumerate() {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
return Some(SelectWordCountMsg::Selected(Self::NUMBERS[i]));
|
||||
for (i, button) in self.choice_buttons.iter_mut().enumerate() {
|
||||
if matches!(button.event(ctx, event), Some(ButtonMsg::Clicked)) {
|
||||
return Some(self.layout.choice_buttons[i].msg);
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
self.cancel_button.event(ctx, event),
|
||||
Some(ButtonMsg::Clicked)
|
||||
) {
|
||||
return Some(SelectWordCountMsg::Cancelled);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
for btn in self.button.iter() {
|
||||
btn.render(target)
|
||||
for button in self.choice_buttons.iter() {
|
||||
button.render(target);
|
||||
}
|
||||
self.cancel_button.render(target);
|
||||
|
||||
cshape::KeyboardOverlay::new(self.keypad_area).render(target);
|
||||
}
|
||||
@ -137,16 +163,5 @@ impl Component for ValueKeypad {
|
||||
impl crate::trace::Trace for SelectWordCount {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("SelectWordCount");
|
||||
match self {
|
||||
SelectWordCount::All(full_selector) => t.child("all", full_selector),
|
||||
SelectWordCount::Multishare(bin_selector) => t.child("multi-share", bin_selector),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for ValueKeypad {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ValueKeypad");
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ pub use keyboard::{
|
||||
passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
|
||||
pin::{PinKeyboard, PinKeyboardMsg},
|
||||
slip39::Slip39Input,
|
||||
word_count::{SelectWordCount, SelectWordCountMsg},
|
||||
word_count::{SelectWordCount, SelectWordCountLayout, SelectWordCountMsg},
|
||||
};
|
||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||
#[cfg(feature = "translations")]
|
||||
|
@ -28,6 +28,7 @@ impl TryFrom<SelectWordCountMsg> for Obj {
|
||||
fn try_from(value: SelectWordCountMsg) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
SelectWordCountMsg::Selected(i) => i.try_into(),
|
||||
SelectWordCountMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,6 +85,7 @@ where
|
||||
impl ComponentMsgObj for SelectWordCount {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
SelectWordCountMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
SelectWordCountMsg::Selected(n) => n.try_into(),
|
||||
}
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ use crate::{
|
||||
use super::{
|
||||
component::{
|
||||
check_homescreen_format, Bip39Input, CoinJoinProgress, Frame, Homescreen, Lockscreen,
|
||||
MnemonicKeyboard, PinKeyboard, Progress, SelectWordCount, Slip39Input, StatusScreen,
|
||||
SwipeContent, SwipeUpScreen, VerticalMenu,
|
||||
MnemonicKeyboard, PinKeyboard, Progress, SelectWordCount, SelectWordCountLayout,
|
||||
Slip39Input, StatusScreen, SwipeContent, SwipeUpScreen, VerticalMenu,
|
||||
},
|
||||
flow::{
|
||||
self, new_confirm_action_simple, ConfirmActionExtra, ConfirmActionMenuStrings,
|
||||
@ -752,14 +752,16 @@ impl FirmwareUI for UIDelizia {
|
||||
}
|
||||
|
||||
fn select_word_count(recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error> {
|
||||
let content = if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||
SelectWordCount::new_multishare()
|
||||
} else {
|
||||
SelectWordCount::new_all()
|
||||
};
|
||||
let selector = SelectWordCount::new(
|
||||
if matches!(recovery_type, RecoveryType::UnlockRepeatedBackup) {
|
||||
SelectWordCountLayout::LAYOUT_MULTISHARE
|
||||
} else {
|
||||
SelectWordCountLayout::LAYOUT_ALL
|
||||
},
|
||||
);
|
||||
let layout = RootComponent::new(Frame::left_aligned(
|
||||
TR::recovery__num_of_words.into(),
|
||||
content,
|
||||
selector,
|
||||
));
|
||||
Ok(layout)
|
||||
}
|
||||
|
@ -445,9 +445,9 @@ def select_word(
|
||||
def select_word_count(
|
||||
*,
|
||||
recovery_type: RecoveryType,
|
||||
) -> LayoutObj[int | str]: # TR returns str
|
||||
) -> LayoutObj[int | str | UIResult]: # TR returns str
|
||||
"""Select a mnemonic word count from the options: 12, 18, 20, 24, or 33.
|
||||
For unlocking a repeated backup, select from 20 or 33."""
|
||||
For unlocking a repeated backup, select between 20 and 33."""
|
||||
|
||||
|
||||
# rust/src/ui/api/firmware_micropython.rs
|
||||
|
@ -7,6 +7,7 @@ from trezor import TR, wire
|
||||
from trezor.messages import Success
|
||||
|
||||
from apps.common import backup_types
|
||||
from apps.management.recovery_device.recover import RecoveryAborted
|
||||
|
||||
from . import layout, recover
|
||||
|
||||
@ -103,7 +104,10 @@ async def _continue_recovery_process() -> Success:
|
||||
TR.buttons__continue, TR.recovery__num_of_words
|
||||
)
|
||||
# ask for the number of words
|
||||
word_count = await layout.request_word_count(recovery_type)
|
||||
try:
|
||||
word_count = await layout.request_word_count(recovery_type)
|
||||
except wire.ActionCancelled:
|
||||
raise RecoveryAborted
|
||||
# ...and only then show the starting screen with word count.
|
||||
await _request_share_first_screen(word_count, recovery_type)
|
||||
assert word_count is not None
|
||||
|
@ -63,6 +63,35 @@ def confirm_recovery(debug: "DebugLink", title: str = "recovery__title") -> None
|
||||
debug.press_right()
|
||||
|
||||
|
||||
def cancel_select_number_of_words(
|
||||
debug: "DebugLink",
|
||||
unlock_repeated_backup=False,
|
||||
) -> None:
|
||||
if debug.layout_type is LayoutType.Bolt:
|
||||
assert debug.read_layout().text_content() == TR.recovery__num_of_words
|
||||
# click the button from ValuePad
|
||||
if unlock_repeated_backup:
|
||||
coords = buttons.grid34(0, 2)
|
||||
else:
|
||||
coords = buttons.grid34(0, 3)
|
||||
debug.click(coords)
|
||||
elif debug.layout_type is LayoutType.Caesar:
|
||||
debug.press_right()
|
||||
layout = debug.read_layout()
|
||||
assert layout.title() == TR.word_count__title
|
||||
# navigate to the number and confirm it
|
||||
debug.press_left()
|
||||
elif debug.layout_type is LayoutType.Delizia:
|
||||
# click the button from ValuePad
|
||||
if unlock_repeated_backup:
|
||||
coords = buttons.grid34(0, 3)
|
||||
else:
|
||||
coords = buttons.grid34(0, 3)
|
||||
debug.click(coords)
|
||||
else:
|
||||
raise ValueError("Unknown model")
|
||||
|
||||
|
||||
def select_number_of_words(
|
||||
debug: "DebugLink",
|
||||
num_of_words: int = 20,
|
||||
@ -74,14 +103,14 @@ def select_number_of_words(
|
||||
def select_bolt() -> "LayoutContent":
|
||||
# click the button from ValuePad
|
||||
if unlock_repeated_backup:
|
||||
coords_map = {20: buttons.grid34(0, 2), 33: buttons.grid34(1, 2)}
|
||||
coords_map = {20: buttons.grid34(1, 2), 33: buttons.grid34(2, 2)}
|
||||
else:
|
||||
coords_map = {
|
||||
12: buttons.grid34(0, 2),
|
||||
18: buttons.grid34(1, 2),
|
||||
20: buttons.grid34(2, 2),
|
||||
24: buttons.grid34(0, 3),
|
||||
33: buttons.grid34(1, 3),
|
||||
24: buttons.grid34(1, 3),
|
||||
33: buttons.grid34(2, 3),
|
||||
}
|
||||
coords = coords_map.get(num_of_words)
|
||||
if coords is None:
|
||||
@ -101,14 +130,14 @@ def select_number_of_words(
|
||||
def select_delizia() -> "LayoutContent":
|
||||
# click the button from ValuePad
|
||||
if unlock_repeated_backup:
|
||||
coords_map = {20: buttons.NO_UI_DELIZIA, 33: buttons.YES_UI_DELIZIA}
|
||||
coords_map = {20: buttons.grid34(0, 1), 33: buttons.grid34(2, 1)}
|
||||
else:
|
||||
coords_map = {
|
||||
12: buttons.grid34(0, 1),
|
||||
18: buttons.grid34(2, 1),
|
||||
20: buttons.grid34(0, 2),
|
||||
24: buttons.grid34(2, 2),
|
||||
33: buttons.grid34(1, 3),
|
||||
33: buttons.grid34(2, 3),
|
||||
}
|
||||
coords = coords_map.get(num_of_words)
|
||||
if coords is None:
|
||||
|
@ -51,6 +51,25 @@ def prepare_recovery_and_evaluate(
|
||||
assert features.recovery_status == messages.RecoveryStatus.Nothing
|
||||
|
||||
|
||||
@contextmanager
|
||||
def prepare_recovery_and_evaluate_cancel(
|
||||
device_handler: "BackgroundDeviceHandler",
|
||||
) -> Generator["DebugLink", None, None]:
|
||||
features = device_handler.features()
|
||||
debug = device_handler.debuglink()
|
||||
assert features.initialized is False
|
||||
device_handler.run(device.recover, pin_protection=False) # type: ignore
|
||||
|
||||
yield debug
|
||||
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device_handler.result()
|
||||
|
||||
features = device_handler.features()
|
||||
assert features.initialized is False
|
||||
assert features.recovery_status == messages.RecoveryStatus.Nothing
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_recovery_slip39_basic(device_handler: "BackgroundDeviceHandler"):
|
||||
with prepare_recovery_and_evaluate(device_handler) as debug:
|
||||
@ -60,6 +79,13 @@ def test_recovery_slip39_basic(device_handler: "BackgroundDeviceHandler"):
|
||||
recovery.finalize(debug)
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_recovery_cancel_number_of_words(device_handler: "BackgroundDeviceHandler"):
|
||||
with prepare_recovery_and_evaluate_cancel(device_handler) as debug:
|
||||
recovery.confirm_recovery(debug)
|
||||
recovery.cancel_select_number_of_words(debug)
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_recovery_bip39(device_handler: "BackgroundDeviceHandler"):
|
||||
with prepare_recovery_and_evaluate(device_handler) as debug:
|
||||
|
@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import device, messages
|
||||
from trezorlib import device, exceptions, messages
|
||||
|
||||
from .. import buttons
|
||||
from ..common import MOCK_GET_ENTROPY
|
||||
@ -202,3 +202,25 @@ def test_repeated_backup(
|
||||
assert features.backup_availability == messages.BackupAvailability.NotAvailable
|
||||
assert features.no_backup is False
|
||||
assert features.recovery_status == messages.RecoveryStatus.Nothing
|
||||
|
||||
# try to unlock backup yet again...
|
||||
device_handler.run(
|
||||
device.recover,
|
||||
type=messages.RecoveryType.UnlockRepeatedBackup,
|
||||
)
|
||||
|
||||
recovery.confirm_recovery(debug, "recovery__title_unlock_repeated_backup")
|
||||
|
||||
# but cancel on the word count selection screen!
|
||||
recovery.cancel_select_number_of_words(debug, unlock_repeated_backup=True)
|
||||
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device_handler.result()
|
||||
|
||||
# ...we are out of recovery mode!
|
||||
features = device_handler.features()
|
||||
assert features.backup_type is messages.BackupType.Slip39_Basic_Extendable
|
||||
assert features.initialized is True
|
||||
assert features.backup_availability == messages.BackupAvailability.NotAvailable
|
||||
assert features.no_backup is False
|
||||
assert features.recovery_status == messages.RecoveryStatus.Nothing
|
||||
|
@ -29,6 +29,7 @@ from ...input_flows import (
|
||||
InputFlowSlip39BasicRecovery,
|
||||
InputFlowSlip39BasicRecoveryAbort,
|
||||
InputFlowSlip39BasicRecoveryAbortBetweenShares,
|
||||
InputFlowSlip39BasicRecoveryAbortOnNumberOfWords,
|
||||
InputFlowSlip39BasicRecoveryInvalidFirstShare,
|
||||
InputFlowSlip39BasicRecoveryInvalidSecondShare,
|
||||
InputFlowSlip39BasicRecoveryNoAbort,
|
||||
@ -118,6 +119,20 @@ def test_abort(client: Client):
|
||||
assert client.features.recovery_status is messages.RecoveryStatus.Nothing
|
||||
|
||||
|
||||
@pytest.mark.models(skip=["legacy", "safe3"])
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_abort_on_number_of_words(client: Client):
|
||||
# on Caesar, test_abort actually aborts on the # of words selection
|
||||
with client:
|
||||
IF = InputFlowSlip39BasicRecoveryAbortOnNumberOfWords(client)
|
||||
client.set_input_flow(IF.get())
|
||||
with pytest.raises(exceptions.Cancelled):
|
||||
device.recover(client, pin_protection=False, label="label")
|
||||
client.init_device()
|
||||
assert client.features.initialized is False
|
||||
assert client.features.recovery_status is messages.RecoveryStatus.Nothing
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_abort_between_shares(client: Client):
|
||||
with client:
|
||||
|
@ -2075,6 +2075,16 @@ class InputFlowSlip39BasicRecovery(InputFlowBase):
|
||||
yield from self.REC.success_wallet_recovered()
|
||||
|
||||
|
||||
class InputFlowSlip39BasicRecoveryAbortOnNumberOfWords(InputFlowBase):
|
||||
def __init__(self, client: Client):
|
||||
super().__init__(client)
|
||||
|
||||
def input_flow_common(self) -> BRGeneratorType:
|
||||
yield from self.REC.confirm_recovery()
|
||||
if self.client.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
|
||||
yield from self.REC.input_number_of_words(None)
|
||||
|
||||
|
||||
class InputFlowSlip39BasicRecoveryAbort(InputFlowBase):
|
||||
def __init__(self, client: Client):
|
||||
super().__init__(client)
|
||||
|
@ -192,7 +192,7 @@ class RecoveryFlow:
|
||||
assert TR.recovery__wanna_cancel_recovery in self._text_content()
|
||||
self.debug.press_yes()
|
||||
|
||||
def input_number_of_words(self, num_words: int) -> BRGeneratorType:
|
||||
def input_number_of_words(self, num_words: int | None) -> BRGeneratorType:
|
||||
br = yield
|
||||
assert br.code == B.MnemonicWordCount
|
||||
assert br.name == "recovery_word_count"
|
||||
@ -200,7 +200,11 @@ class RecoveryFlow:
|
||||
assert TR.word_count__title in self.debug.read_layout().title()
|
||||
else:
|
||||
assert TR.recovery__num_of_words in self._text_content()
|
||||
self.debug.input(str(num_words))
|
||||
|
||||
if num_words is None:
|
||||
self.debug.press_no()
|
||||
else:
|
||||
self.debug.input(str(num_words))
|
||||
|
||||
def warning_invalid_recovery_seed(self) -> BRGeneratorType:
|
||||
br = yield
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -23,17 +23,29 @@ def confirm_recovery(debug: "DebugLink") -> None:
|
||||
debug.read_layout(wait=True)
|
||||
|
||||
|
||||
def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None:
|
||||
debug.click(buttons.OK)
|
||||
debug.read_layout(wait=True)
|
||||
|
||||
# click the number
|
||||
word_option_offset = 6
|
||||
word_options = (12, 18, 20, 24, 33)
|
||||
index = word_option_offset + word_options.index(
|
||||
num_of_words
|
||||
) # raises if num of words is invalid
|
||||
coords = buttons.grid34(index % 3, index // 3)
|
||||
def select_number_of_words(
|
||||
debug: "DebugLink", tag_version: tuple | None, num_of_words: int = 20
|
||||
) -> None:
|
||||
if "SelectWordCount" not in debug.read_layout().all_components():
|
||||
debug.click(buttons.OK)
|
||||
debug.read_layout(wait=True)
|
||||
if tag_version is None or tag_version > (2, 8, 8):
|
||||
# layout changed after adding the cancel button
|
||||
coords_map = {
|
||||
12: buttons.grid34(0, 2),
|
||||
18: buttons.grid34(1, 2),
|
||||
20: buttons.grid34(2, 2),
|
||||
24: buttons.grid34(1, 3),
|
||||
33: buttons.grid34(2, 3),
|
||||
}
|
||||
coords = coords_map.get(num_of_words)
|
||||
else:
|
||||
word_option_offset = 6
|
||||
word_options = (12, 18, 20, 24, 33)
|
||||
index = word_option_offset + word_options.index(
|
||||
num_of_words
|
||||
) # raises if num of words is invalid
|
||||
coords = buttons.grid34(index % 3, index // 3)
|
||||
debug.click(coords)
|
||||
debug.read_layout(wait=True)
|
||||
|
||||
|
@ -30,6 +30,7 @@ from trezorlib.messages import (
|
||||
)
|
||||
from trezorlib.tools import H_
|
||||
|
||||
from ..click_tests import recovery
|
||||
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6, MNEMONIC_SLIP39_BASIC_20_3of6_SECRET
|
||||
from ..device_handler import BackgroundDeviceHandler
|
||||
from ..emulators import ALL_TAGS, EmulatorWrapper
|
||||
@ -308,7 +309,7 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]):
|
||||
device_handler.run(device.recover, pin_protection=False)
|
||||
|
||||
recovery_old.confirm_recovery(debug)
|
||||
recovery_old.select_number_of_words(debug)
|
||||
recovery_old.select_number_of_words(debug, version_from_tag(tag))
|
||||
layout = recovery_old.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[0])
|
||||
if not debug.legacy_ui and not debug.legacy_debug:
|
||||
assert (
|
||||
@ -327,14 +328,14 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]):
|
||||
emu.client.watch_layout(True)
|
||||
|
||||
# second share
|
||||
layout = recovery_old.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[2])
|
||||
layout = recovery.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[2])
|
||||
assert (
|
||||
"2 of 3 shares entered" in layout.text_content()
|
||||
or "1 more share" in layout.text_content()
|
||||
)
|
||||
|
||||
# last one
|
||||
layout = recovery_old.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[1])
|
||||
layout = recovery.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[1])
|
||||
assert (
|
||||
"Wallet recovery completed" in layout.text_content()
|
||||
or "finished recovering" in layout.text_content()
|
||||
|
Loading…
Reference in New Issue
Block a user