1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-09 03:45:44 +00:00

fix(core/mercury): multi-share backup more info

The commit fixes More Info screen hidden behind context menu during a
prompt for number of shares and shares threshold. It removes wrong title
and enables changing the info text based on currently selected number.
This commit is contained in:
obrusvit 2024-07-17 09:55:43 +02:00 committed by Vít Obrusník
parent b567a91c22
commit 0d987a68f4
14 changed files with 169 additions and 118 deletions

View File

@ -0,0 +1 @@
[T3T1] Fix More info screen during multi-share backup creation

View File

@ -525,6 +525,8 @@ static void _librust_qstrs(void) {
MP_QSTR_reset__share_checked_successfully_template;
MP_QSTR_reset__share_completed_template;
MP_QSTR_reset__share_words_title;
MP_QSTR_reset__slip39_checklist_more_info_threshold;
MP_QSTR_reset__slip39_checklist_more_info_threshold_example_template;
MP_QSTR_reset__slip39_checklist_num_groups;
MP_QSTR_reset__slip39_checklist_num_groups_x_template;
MP_QSTR_reset__slip39_checklist_num_shares;

View File

@ -1355,6 +1355,8 @@ pub enum TranslatedString {
brightness__changed_title = 954, // "Display brightness changed"
brightness__change_title = 955, // "Change display brightness"
words__title_done = 956, // "Done"
reset__slip39_checklist_more_info_threshold = 957, // "The threshold sets the minumum number of shares needed to recover your wallet."
reset__slip39_checklist_more_info_threshold_example_template = 958, // "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet."
}
impl TranslatedString {
@ -2704,6 +2706,8 @@ impl TranslatedString {
Self::brightness__changed_title => "Display brightness changed",
Self::brightness__change_title => "Change display brightness",
Self::words__title_done => "Done",
Self::reset__slip39_checklist_more_info_threshold => "The threshold sets the minumum number of shares needed to recover your wallet.",
Self::reset__slip39_checklist_more_info_threshold_example_template => "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet.",
}
}
@ -4054,6 +4058,8 @@ impl TranslatedString {
Qstr::MP_QSTR_brightness__changed_title => Some(Self::brightness__changed_title),
Qstr::MP_QSTR_brightness__change_title => Some(Self::brightness__change_title),
Qstr::MP_QSTR_words__title_done => Some(Self::words__title_done),
Qstr::MP_QSTR_reset__slip39_checklist_more_info_threshold => Some(Self::reset__slip39_checklist_more_info_threshold),
Qstr::MP_QSTR_reset__slip39_checklist_more_info_threshold_example_template => Some(Self::reset__slip39_checklist_more_info_threshold_example_template),
_ => None,
}
}

View File

@ -36,6 +36,7 @@ mod swipe_content;
mod swipe_up_screen;
#[cfg(feature = "translations")]
mod tap_to_confirm;
mod updatable_more_info;
mod welcome_screen;
#[cfg(feature = "translations")]
@ -82,6 +83,7 @@ pub use swipe_content::{InternallySwipable, InternallySwipableContent, SwipeCont
pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg};
#[cfg(feature = "translations")]
pub use tap_to_confirm::TapToConfirm;
pub use updatable_more_info::UpdatableMoreInfo;
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
pub use welcome_screen::WelcomeScreen;

View File

@ -4,7 +4,6 @@ use crate::{
ui::{
component::{
base::ComponentExt,
paginated::Paginate,
text::paragraphs::{Paragraph, Paragraphs},
Child, Component, Event, EventCtx, Pad, SwipeDirection,
},
@ -17,28 +16,22 @@ use crate::{
use super::{theme, Button, ButtonMsg};
pub struct NumberInputDialogMsg(pub u32);
pub enum NumberInputDialogMsg {
Confirmed(u32),
Changed(u32),
}
pub struct NumberInputDialog<F>
where
F: Fn(u32) -> TString<'static>,
{
pub struct NumberInputDialog {
area: Rect,
description_func: F,
input: Child<NumberInput>,
paragraphs: Child<Paragraphs<Paragraph<'static>>>,
paragraphs_pad: Pad,
}
impl<F> NumberInputDialog<F>
where
F: Fn(u32) -> TString<'static>,
{
pub fn new(min: u32, max: u32, init_value: u32, description_func: F) -> Result<Self, Error> {
let text = description_func(init_value);
impl NumberInputDialog {
pub fn new(min: u32, max: u32, init_value: u32, text: TString<'static>) -> Result<Self, Error> {
Ok(Self {
area: Rect::zero(),
description_func,
input: NumberInput::new(min, max, init_value).into_child(),
paragraphs: Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, text))
.into_child(),
@ -46,27 +39,12 @@ where
})
}
fn update_text(&mut self, ctx: &mut EventCtx, value: u32) {
let text = (self.description_func)(value);
self.paragraphs.mutate(ctx, move |ctx, para| {
para.inner_mut().update(text);
// Recompute bounding box.
para.change_page(0);
ctx.request_paint()
});
self.paragraphs_pad.clear();
ctx.request_paint();
}
pub fn value(&self) -> u32 {
self.input.inner().value
}
}
impl<F> Component for NumberInputDialog<F>
where
F: Fn(u32) -> TString<'static>,
{
impl Component for NumberInputDialog {
type Msg = NumberInputDialogMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@ -87,11 +65,11 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(NumberInputMsg::Changed(i)) = self.input.event(ctx, event) {
self.update_text(ctx, i);
return Some(NumberInputDialogMsg::Changed(i));
}
if let Event::Swipe(SwipeEvent::End(SwipeDirection::Up)) = event {
return Some(NumberInputDialogMsg(self.input.inner().value));
return Some(NumberInputDialogMsg::Confirmed(self.input.inner().value));
}
self.paragraphs.event(ctx, event);
None
@ -109,10 +87,7 @@ where
}
#[cfg(feature = "ui_debug")]
impl<F> crate::trace::Trace for NumberInputDialog<F>
where
F: Fn(u32) -> TString<'static>,
{
impl crate::trace::Trace for NumberInputDialog {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("NumberInputDialog");
t.child("input", &self.input);

View File

@ -0,0 +1,82 @@
use crate::{
strutil::TString,
ui::{
component::{
paginated::Paginate,
text::paragraphs::{Paragraph, Paragraphs},
Component, Event, EventCtx, Never,
},
geometry::Rect,
shape::Renderer,
},
};
use super::theme;
pub struct UpdatableMoreInfo<F>
where
F: Fn() -> TString<'static>,
{
info_func: F,
paragraphs: Paragraphs<Paragraph<'static>>,
}
impl<F> UpdatableMoreInfo<F>
where
F: Fn() -> TString<'static>,
{
pub fn new(info_func: F) -> Self {
Self {
info_func,
paragraphs: Paragraphs::new(Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
TString::empty(),
)),
}
}
fn update_text(&mut self, ctx: &mut EventCtx) {
let text = (self.info_func)();
self.paragraphs.inner_mut().update(text);
self.paragraphs.change_page(0);
ctx.request_paint();
}
}
impl<F> Component for UpdatableMoreInfo<F>
where
F: Fn() -> TString<'static>,
{
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.paragraphs.place(bounds);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Attach(_) = event {
self.update_text(ctx);
}
None
}
fn paint(&mut self) {
todo!("remove when ui-t3t1 done");
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.paragraphs.render(target);
}
}
#[cfg(feature = "ui_debug")]
impl<F> crate::trace::Trace for UpdatableMoreInfo<F>
where
F: Fn() -> TString<'static>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("UpdatableMoreInfo");
t.child("paragraphs", &self.paragraphs);
}
}

View File

@ -5,24 +5,21 @@ use crate::{
translations::TR,
ui::{
button_request::ButtonRequest,
component::{
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, Paragraphs},
ButtonRequestExt, ComponentExt, SwipeDirection,
},
component::{swipe_detect::SwipeSettings, ButtonRequestExt, ComponentExt, SwipeDirection},
flow::{
base::{DecisionBuilder as _, StateChange},
FlowMsg, FlowState, SwipeFlow,
},
layout::obj::LayoutObj,
model_mercury::component::SwipeContent,
},
};
use core::sync::atomic::{AtomicU16, Ordering};
use super::super::{
component::{
CancelInfoConfirmMsg, Frame, FrameMsg, NumberInputDialog, NumberInputDialogMsg,
VerticalMenu, VerticalMenuChoiceMsg,
SwipeContent, UpdatableMoreInfo, VerticalMenu, VerticalMenuChoiceMsg,
},
theme,
};
@ -61,6 +58,8 @@ impl FlowState for RequestNumber {
}
}
static NUM_DISPLAYED: AtomicU16 = AtomicU16::new(0);
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, RequestNumber::new_obj) }
@ -72,27 +71,22 @@ impl RequestNumber {
let count: u32 = kwargs.get(Qstr::MP_QSTR_count)?.try_into()?;
let min_count: u32 = kwargs.get(Qstr::MP_QSTR_min_count)?.try_into()?;
let max_count: u32 = kwargs.get(Qstr::MP_QSTR_max_count)?.try_into()?;
let description: Obj = kwargs.get(Qstr::MP_QSTR_description)?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let info: Obj = kwargs.get(Qstr::MP_QSTR_info)?;
assert!(description != Obj::const_none());
assert!(info != Obj::const_none());
let br_name: TString = kwargs.get(Qstr::MP_QSTR_br_name)?.try_into()?;
let br_code: u16 = kwargs.get(Qstr::MP_QSTR_br_code)?.try_into()?;
let description_cb = move |i: u32| {
TString::try_from(
description
.call_with_n_args(&[i.try_into().unwrap()])
.unwrap(),
)
.unwrap()
};
let info_cb = move |i: u32| {
TString::try_from(info.call_with_n_args(&[i.try_into().unwrap()]).unwrap()).unwrap()
NUM_DISPLAYED.store(count as u16, Ordering::Relaxed);
let info_cb = move || {
let curr_number = NUM_DISPLAYED.load(Ordering::Relaxed) as u32;
let text = info
.call_with_n_args(&[curr_number.try_into().unwrap()])
.unwrap();
TString::try_from(text).unwrap()
};
let number_input_dialog =
NumberInputDialog::new(min_count, max_count, count, description_cb)?;
let number_input_dialog = NumberInputDialog::new(min_count, max_count, count, description)?;
let content_number_input =
Frame::left_aligned(title, SwipeContent::new(number_input_dialog))
.with_menu_button()
@ -101,12 +95,19 @@ impl RequestNumber {
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| match msg {
FrameMsg::Button(_) => Some(FlowMsg::Info),
FrameMsg::Content(NumberInputDialogMsg(n)) => Some(FlowMsg::Choice(n as usize)),
FrameMsg::Content(NumberInputDialogMsg::Changed(n)) => {
NUM_DISPLAYED.store(n as u16, Ordering::Relaxed);
None
}
FrameMsg::Content(NumberInputDialogMsg::Confirmed(n)) => {
NUM_DISPLAYED.store(n as u16, Ordering::Relaxed);
Some(FlowMsg::Choice(n as usize))
}
})
.one_button_request(ButtonRequest::from_num(br_code, br_name));
let content_menu = Frame::left_aligned(
"".into(),
TString::empty(),
VerticalMenu::empty().item(theme::ICON_CHEVRON_RIGHT, TR::buttons__more_info.into()),
)
.with_cancel_button()
@ -117,20 +118,14 @@ impl RequestNumber {
FrameMsg::Button(_) => None,
});
let paragraphs_info = Paragraphs::new(Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
info_cb(0), // TODO: get the value
));
let content_info = Frame::left_aligned(
TR::backup__title_skip.into(),
SwipeContent::new(paragraphs_info),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let updatable_info = UpdatableMoreInfo::new(info_cb);
let content_info = Frame::left_aligned(TString::empty(), SwipeContent::new(updatable_info))
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let res = SwipeFlow::new(&RequestNumber::Number)?
.with_page(&RequestNumber::Number, content_number_input)?

View File

@ -1704,7 +1704,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// count: int,
/// min_count: int,
/// max_count: int,
/// description: Callable[[int], str] | None = None,
/// description: str,
/// info: Callable[[int], str] | None = None,
/// br_code: ButtonRequestType,
/// br_name: str,

View File

@ -420,7 +420,7 @@ def flow_request_number(
count: int,
min_count: int,
max_count: int,
description: Callable[[int], str] | None = None,
description: str,
info: Callable[[int], str] | None = None,
br_code: ButtonRequestType,
br_name: str,

View File

@ -652,6 +652,8 @@ class TR:
reset__share_checked_successfully_template: str = "Share #{0} checked successfully."
reset__share_completed_template: str = "Share #{0} completed"
reset__share_words_title: str = "Standard backup"
reset__slip39_checklist_more_info_threshold: str = "The threshold sets the minumum number of shares needed to recover your wallet."
reset__slip39_checklist_more_info_threshold_example_template: str = "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet."
reset__slip39_checklist_num_groups: str = "Number of groups"
reset__slip39_checklist_num_groups_x_template: str = "Number of groups: {0}"
reset__slip39_checklist_num_shares: str = "Number of shares"

View File

@ -183,11 +183,13 @@ async def _backup_slip39_advanced(
groups_count = await layout.slip39_advanced_prompt_number_of_groups()
# get group threshold
await layout.slip39_show_checklist(1, advanced=True)
await layout.slip39_show_checklist(1, advanced=True, count=groups_count)
group_threshold = await layout.slip39_advanced_prompt_group_threshold(groups_count)
# get shares and thresholds
await layout.slip39_show_checklist(2, advanced=True)
await layout.slip39_show_checklist(
2, advanced=True, count=groups_count, threshold=group_threshold
)
groups = []
for i in range(groups_count):
share_count = await layout.slip39_prompt_number_of_shares(i)

View File

@ -157,7 +157,7 @@ def _slip_39_checklist_items(
async def _prompt_number(
title: str,
description: Callable[[int], str],
description: str,
info: Callable[[int], str],
count: int,
min_count: int,
@ -200,38 +200,20 @@ async def slip39_prompt_threshold(
min_count = min(2, num_of_shares)
max_count = num_of_shares
def description(count: int) -> str:
if group_id is None:
return TR.reset__select_threshold
else:
return TR.reset__num_shares_for_group_template.format(group_id + 1)
description = (
TR.reset__select_threshold
if group_id is None
else TR.reset__num_shares_for_group_template.format(group_id + 1)
)
def info(count: int) -> str:
# TODO: this is madness...
text = TR.reset__the_threshold_sets_the_number_of_shares
if group_id is None:
# FIXME: need to propagate the argument in rust, temporary hack to show plausible value
count = num_of_shares - 1
text += TR.reset__needed_to_recover_your_wallet
text += TR.reset__set_it_to_count_template.format(count)
if num_of_shares == 1:
text += TR.reset__one_share
elif num_of_shares == count:
text += TR.reset__all_x_of_y_template.format(count, num_of_shares)
else:
text += TR.reset__any_x_of_y_template.format(count, num_of_shares)
text += "."
else:
text += TR.reset__needed_to_form_a_group
text += TR.reset__set_it_to_count_template.format(count)
if num_of_shares == 1:
text += TR.reset__one_share + " "
elif num_of_shares == count:
text += TR.reset__all_x_of_y_template.format(count, num_of_shares)
else:
text += TR.reset__any_x_of_y_template.format(count, num_of_shares)
text += " " + TR.reset__to_form_group_template.format(group_id + 1)
return text
return (
TR.reset__slip39_checklist_more_info_threshold
+ "\n"
+ TR.reset__slip39_checklist_more_info_threshold_example_template.format(
count, num_of_shares, count
)
)
return await _prompt_number(
TR.reset__title_set_threshold,
@ -249,13 +231,11 @@ async def slip39_prompt_number_of_shares(group_id: int | None = None) -> int:
min_count = 1
max_count = 16
def description(i: int):
if group_id is None:
return TR.reset__num_of_shares_how_many
else:
return TR.reset__total_number_of_shares_in_group_template.format(
group_id + 1
)
description = (
TR.reset__num_of_shares_how_many
if group_id is None
else TR.reset__total_number_of_shares_in_group_template.format(group_id + 1)
)
if group_id is None:
info = TR.reset__num_of_shares_long_info
@ -282,7 +262,7 @@ async def slip39_advanced_prompt_number_of_groups() -> int:
return await _prompt_number(
TR.reset__title_set_number_of_groups,
lambda i: description,
description,
lambda i: info,
count,
min_count,
@ -300,7 +280,7 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
return await _prompt_number(
TR.reset__title_set_group_threshold,
lambda i: description,
description,
lambda i: info,
count,
min_count,

View File

@ -654,6 +654,8 @@
"reset__share_checked_successfully_template": "Share #{0} checked successfully.",
"reset__share_completed_template": "Share #{0} completed",
"reset__share_words_title": "Standard backup",
"reset__slip39_checklist_more_info_threshold": "The threshold sets the minumum number of shares needed to recover your wallet.",
"reset__slip39_checklist_more_info_threshold_example_template": "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet.",
"reset__slip39_checklist_num_groups": "Number of groups",
"reset__slip39_checklist_num_groups_x_template": "Number of groups: {0}",
"reset__slip39_checklist_num_shares": "Number of shares",

View File

@ -955,5 +955,7 @@
"953": "setting__apply",
"954": "brightness__changed_title",
"955": "brightness__change_title",
"956": "words__title_done"
"956": "words__title_done",
"957": "reset__slip39_checklist_more_info_threshold",
"958": "reset__slip39_checklist_more_info_threshold_example_template"
}