diff --git a/core/.changelog.d/4006.fixed b/core/.changelog.d/4006.fixed new file mode 100644 index 0000000000..ecbf6c3c3f --- /dev/null +++ b/core/.changelog.d/4006.fixed @@ -0,0 +1 @@ +[T3T1] Fix More info screen during multi-share backup creation diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index ffd4fac157..3a9ebdc115 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -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; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 49364f3029..7aeb7f6eb1 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -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, } } diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index bb58e3bf26..864a20357d 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -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; diff --git a/core/embed/rust/src/ui/model_mercury/component/number_input.rs b/core/embed/rust/src/ui/model_mercury/component/number_input.rs index 01872670fc..1730e6f301 100644 --- a/core/embed/rust/src/ui/model_mercury/component/number_input.rs +++ b/core/embed/rust/src/ui/model_mercury/component/number_input.rs @@ -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 -where - F: Fn(u32) -> TString<'static>, -{ +pub struct NumberInputDialog { area: Rect, - description_func: F, input: Child, paragraphs: Child>>, paragraphs_pad: Pad, } -impl NumberInputDialog -where - F: Fn(u32) -> TString<'static>, -{ - pub fn new(min: u32, max: u32, init_value: u32, description_func: F) -> Result { - let text = description_func(init_value); +impl NumberInputDialog { + pub fn new(min: u32, max: u32, init_value: u32, text: TString<'static>) -> Result { 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 Component for NumberInputDialog -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 { 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 crate::trace::Trace for NumberInputDialog -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); diff --git a/core/embed/rust/src/ui/model_mercury/component/updatable_more_info.rs b/core/embed/rust/src/ui/model_mercury/component/updatable_more_info.rs new file mode 100644 index 0000000000..4a3824d9d0 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/updatable_more_info.rs @@ -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 +where + F: Fn() -> TString<'static>, +{ + info_func: F, + paragraphs: Paragraphs>, +} + +impl UpdatableMoreInfo +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 Component for UpdatableMoreInfo +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 { + 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 crate::trace::Trace for UpdatableMoreInfo +where + F: Fn() -> TString<'static>, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("UpdatableMoreInfo"); + t.child("paragraphs", &self.paragraphs); + } +} diff --git a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs index 5d1c645fc9..4b5a48d122 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs @@ -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)? diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index d20fc21f72..523c491938 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -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, diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 623a91711b..6bdb6d423c 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -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, diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 98f9f07982..16e3d948c9 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -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" diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 94974dae24..f2ae0efc0b 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -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) diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 5a2538370f..8b94907324 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -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, diff --git a/core/translations/en.json b/core/translations/en.json index bc33245d40..a42c5d1747 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -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", diff --git a/core/translations/order.json b/core/translations/order.json index 796ca2f279..422d2b589e 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -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" }