mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-03-17 16:46:05 +00:00
feat(core/ui): highlight repeated words on T3T1
ShowShareWords flow now informs the user if the word is repeated. The most typical usecase in 1-of-1 shamir (SingleShare) where 3rd and 4th word is "academic".
This commit is contained in:
parent
3a039c505f
commit
aaa2ece3ba
@ -246,6 +246,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_haptic_feedback__enable;
|
||||
MP_QSTR_haptic_feedback__subtitle;
|
||||
MP_QSTR_haptic_feedback__title;
|
||||
MP_QSTR_highlight_repeated;
|
||||
MP_QSTR_hold;
|
||||
MP_QSTR_hold_danger;
|
||||
MP_QSTR_homescreen__click_to_connect;
|
||||
@ -528,6 +529,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_reset__slip39_checklist_write_down;
|
||||
MP_QSTR_reset__slip39_checklist_write_down_recovery;
|
||||
MP_QSTR_reset__the_threshold_sets_the_number_of_shares;
|
||||
MP_QSTR_reset__the_word_is_repeated;
|
||||
MP_QSTR_reset__threshold_info;
|
||||
MP_QSTR_reset__title_backup_is_done;
|
||||
MP_QSTR_reset__title_create_wallet;
|
||||
|
@ -1339,6 +1339,7 @@ pub enum TranslatedString {
|
||||
reset__repeat_for_all_shares = 938, // "Repeat for all shares."
|
||||
homescreen__settings_subtitle = 939, // "Settings"
|
||||
homescreen__settings_title = 940, // "Homescreen"
|
||||
reset__the_word_is_repeated = 941, // "The word is repeated"
|
||||
}
|
||||
|
||||
impl TranslatedString {
|
||||
@ -2672,6 +2673,7 @@ impl TranslatedString {
|
||||
Self::reset__repeat_for_all_shares => "Repeat for all shares.",
|
||||
Self::homescreen__settings_subtitle => "Settings",
|
||||
Self::homescreen__settings_title => "Homescreen",
|
||||
Self::reset__the_word_is_repeated => "The word is repeated",
|
||||
}
|
||||
}
|
||||
|
||||
@ -4006,6 +4008,7 @@ impl TranslatedString {
|
||||
Qstr::MP_QSTR_reset__repeat_for_all_shares => Some(Self::reset__repeat_for_all_shares),
|
||||
Qstr::MP_QSTR_homescreen__settings_subtitle => Some(Self::homescreen__settings_subtitle),
|
||||
Qstr::MP_QSTR_homescreen__settings_title => Some(Self::homescreen__settings_title),
|
||||
Qstr::MP_QSTR_reset__the_word_is_repeated => Some(Self::reset__the_word_is_repeated),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,10 @@ impl<'a> Label<'a> {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: TextStyle) {
|
||||
self.layout.style = style;
|
||||
}
|
||||
|
||||
pub fn font(&self) -> Font {
|
||||
self.layout.style.text_font
|
||||
}
|
||||
|
@ -86,11 +86,6 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_styled(mut self, style: TextStyle) -> Self {
|
||||
self.title = self.title.styled(style);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self {
|
||||
let style = theme::TEXT_SUB_GREY;
|
||||
@ -125,6 +120,18 @@ where
|
||||
.button_styled(theme::button_danger())
|
||||
}
|
||||
|
||||
pub fn title_styled(mut self, style: TextStyle) -> Self {
|
||||
self.title = self.title.styled(style);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn subtitle_styled(mut self, style: TextStyle) -> Self {
|
||||
if let Some(subtitle) = self.subtitle.take() {
|
||||
self.subtitle = Some(subtitle.styled(style))
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn button_styled(mut self, style: ButtonStyleSheet) -> Self {
|
||||
if self.button.is_some() {
|
||||
self.button = Some(self.button.unwrap().styled(style));
|
||||
@ -160,6 +167,25 @@ where
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
pub fn update_subtitle(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
new_subtitle: TString<'static>,
|
||||
new_style: Option<TextStyle>,
|
||||
) {
|
||||
let style = new_style.unwrap_or(theme::TEXT_SUB_GREY);
|
||||
match &mut self.subtitle {
|
||||
Some(subtitle) => {
|
||||
subtitle.set_style(style);
|
||||
subtitle.set_text(new_subtitle);
|
||||
}
|
||||
None => {
|
||||
self.subtitle = Some(Label::new(new_subtitle, self.title.alignment(), style));
|
||||
}
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
|
||||
where
|
||||
F: Fn(&mut EventCtx, &mut T) -> R,
|
||||
|
@ -5,10 +5,13 @@ use crate::{
|
||||
translations::TR,
|
||||
ui::{
|
||||
animation::Animation,
|
||||
component::{Component, Event, EventCtx, Never, SwipeDirection},
|
||||
component::{
|
||||
swipe_detect::{SwipeConfig, SwipeSettings},
|
||||
Component, Event, EventCtx, Never, SwipeDirection,
|
||||
},
|
||||
event::SwipeEvent,
|
||||
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
|
||||
model_mercury::component::Footer,
|
||||
model_mercury::component::{Footer, Frame, FrameMsg},
|
||||
shape::{self, Renderer},
|
||||
util,
|
||||
},
|
||||
@ -17,12 +20,112 @@ use heapless::Vec;
|
||||
|
||||
const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less
|
||||
const ANIMATION_DURATION_MS: Duration = Duration::from_millis(166);
|
||||
type IndexVec = Vec<u8, MAX_WORDS>;
|
||||
|
||||
/// Component showing mnemonic/share words during backup procedure. Model T3T1
|
||||
/// contains one word per screen. A user is instructed to swipe up/down to see
|
||||
/// next/previous word.
|
||||
/// This is a wrapper around a Frame so that the subtitle and Footer of the
|
||||
/// Frame can be updated based on the index of the word shown. Actual share
|
||||
/// words are rendered within `ShareWordsInner` component,
|
||||
pub struct ShareWords<'a> {
|
||||
area: Rect,
|
||||
subtitle: TString<'static>,
|
||||
frame: Frame<ShareWordsInner<'a>>,
|
||||
repeated_indices: Option<IndexVec>,
|
||||
}
|
||||
|
||||
impl<'a> ShareWords<'a> {
|
||||
pub fn new(
|
||||
title: TString<'static>,
|
||||
subtitle: TString<'static>,
|
||||
share_words: Vec<TString<'a>, MAX_WORDS>,
|
||||
highlight_repeated: bool,
|
||||
) -> Self {
|
||||
let repeated_indices = if highlight_repeated {
|
||||
Some(Self::find_repeated(share_words.as_slice()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
subtitle,
|
||||
frame: Frame::left_aligned(title, ShareWordsInner::new(share_words))
|
||||
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
|
||||
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
|
||||
.with_vertical_pages()
|
||||
.with_subtitle(subtitle),
|
||||
repeated_indices,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_repeated(share_words: &[TString]) -> IndexVec {
|
||||
let mut repeated_indices = IndexVec::new();
|
||||
for i in (0..share_words.len()).rev() {
|
||||
let word = share_words[i];
|
||||
if share_words[..i].contains(&word) {
|
||||
unwrap!(repeated_indices.push(i as u8));
|
||||
}
|
||||
}
|
||||
repeated_indices.reverse();
|
||||
repeated_indices
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for ShareWords<'a> {
|
||||
type Msg = FrameMsg<Never>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.frame.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let page_index = self.frame.inner().page_index as u8;
|
||||
if let Some(repeated_indices) = &self.repeated_indices {
|
||||
if repeated_indices.contains(&(page_index as usize)) {
|
||||
let updated_subtitle = TString::from_translation(TR::reset__the_word_is_repeated);
|
||||
self.frame
|
||||
.update_subtitle(ctx, updated_subtitle, Some(theme::TEXT_SUB_GREEN_LIME));
|
||||
} else {
|
||||
self.frame
|
||||
.update_subtitle(ctx, self.subtitle, Some(theme::TEXT_SUB_GREY));
|
||||
}
|
||||
}
|
||||
self.frame.update_footer_counter(ctx, page_index + 1);
|
||||
self.frame.event(ctx, event)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// TODO: remove when ui-t3t1 done
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
self.frame.render(target);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_bounds")]
|
||||
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "micropython")]
|
||||
impl<'a> crate::ui::flow::Swipable for ShareWords<'a> {
|
||||
fn get_swipe_config(&self) -> SwipeConfig {
|
||||
self.frame.get_swipe_config()
|
||||
}
|
||||
|
||||
fn get_internal_page_count(&self) -> usize {
|
||||
self.frame.get_internal_page_count()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<'a> crate::trace::Trace for ShareWords<'a> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ShareWords");
|
||||
t.child("inner", &self.frame);
|
||||
}
|
||||
}
|
||||
|
||||
struct ShareWordsInner<'a> {
|
||||
share_words: Vec<TString<'a>, MAX_WORDS>,
|
||||
page_index: i16,
|
||||
next_index: i16,
|
||||
@ -35,12 +138,11 @@ pub struct ShareWords<'a> {
|
||||
progress: i16,
|
||||
}
|
||||
|
||||
impl<'a> ShareWords<'a> {
|
||||
impl<'a> ShareWordsInner<'a> {
|
||||
const AREA_WORD_HEIGHT: i16 = 91;
|
||||
|
||||
pub fn new(share_words: Vec<TString<'a>, MAX_WORDS>) -> Self {
|
||||
fn new(share_words: Vec<TString<'a>, MAX_WORDS>) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
share_words,
|
||||
page_index: 0,
|
||||
next_index: 0,
|
||||
@ -76,25 +178,21 @@ impl<'a> ShareWords<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for ShareWords<'a> {
|
||||
impl<'a> Component for ShareWordsInner<'a> {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
let used_area = bounds
|
||||
.inset(Insets::sides(theme::SPACING))
|
||||
.inset(Insets::bottom(theme::SPACING));
|
||||
|
||||
self.area_word = Rect::snap(
|
||||
used_area.center(),
|
||||
Offset::new(used_area.width(), ShareWords::AREA_WORD_HEIGHT),
|
||||
Offset::new(used_area.width(), ShareWordsInner::AREA_WORD_HEIGHT),
|
||||
Alignment2D::CENTER,
|
||||
);
|
||||
|
||||
self.footer
|
||||
.place(used_area.split_bottom(Footer::HEIGHT_SIMPLE).1);
|
||||
|
||||
self.area
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
@ -199,12 +297,61 @@ impl<'a> Component for ShareWords<'a> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<'a> crate::trace::Trace for ShareWords<'a> {
|
||||
impl<'a> crate::trace::Trace for ShareWordsInner<'a> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ShareWords");
|
||||
t.component("ShareWordsInner");
|
||||
let word = &self.share_words[self.page_index as usize];
|
||||
let content = word.map(|w| uformat!("{}. {}\n", self.page_index + 1, w));
|
||||
t.string("screen_content", content.as_str().into());
|
||||
t.int("page_count", self.share_words.len() as i64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_find_repeated_indices() {
|
||||
let words0 = [];
|
||||
let words1 = [
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("bbb"),
|
||||
TString::from_str("ccc"),
|
||||
];
|
||||
let words2 = [
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("bbb"),
|
||||
];
|
||||
let words3 = [
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("bbb"),
|
||||
TString::from_str("bbb"),
|
||||
TString::from_str("aaa"),
|
||||
];
|
||||
let words4 = [
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("aaa"),
|
||||
TString::from_str("aaa"),
|
||||
];
|
||||
|
||||
assert_eq!(ShareWords::find_repeated(&words0), IndexVec::new());
|
||||
assert_eq!(ShareWords::find_repeated(&words1), IndexVec::new());
|
||||
assert_eq!(
|
||||
ShareWords::find_repeated(&words2),
|
||||
IndexVec::from_slice(&[1]).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
ShareWords::find_repeated(&words3),
|
||||
IndexVec::from_slice(&[1, 3, 4]).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
ShareWords::find_repeated(&words4),
|
||||
IndexVec::from_slice(&[1, 2, 3, 4]).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ impl ShowShareWords {
|
||||
.and_then(|desc: TString| if desc.is_empty() { None } else { Some(desc) });
|
||||
let text_info: Obj = kwargs.get(Qstr::MP_QSTR_text_info)?;
|
||||
let text_confirm: TString = kwargs.get(Qstr::MP_QSTR_text_confirm)?.try_into()?;
|
||||
let highlight_repeated: bool = kwargs.get(Qstr::MP_QSTR_highlight_repeated)?.try_into()?;
|
||||
let nwords = share_words_vec.len();
|
||||
|
||||
let mut instructions_paragraphs = ParagraphVecShort::new();
|
||||
@ -108,12 +109,8 @@ impl ShowShareWords {
|
||||
.one_button_request(ButtonRequestCode::ResetDevice.with_type("share_words"))
|
||||
.with_pages(move |_| nwords + 2);
|
||||
|
||||
let content_words = Frame::left_aligned(title, ShareWords::new(share_words_vec))
|
||||
.with_subtitle(subtitle)
|
||||
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
|
||||
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
|
||||
.with_vertical_pages()
|
||||
.map(|_| None);
|
||||
let content_words =
|
||||
ShareWords::new(title, subtitle, share_words_vec, highlight_repeated).map(|_| None);
|
||||
|
||||
let content_confirm =
|
||||
Frame::left_aligned(text_confirm, PromptScreen::new_hold_to_confirm())
|
||||
|
@ -1710,6 +1710,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// description: str,
|
||||
/// text_info: Iterable[str],
|
||||
/// text_confirm: str,
|
||||
/// highlight_repeated: bool,
|
||||
/// ) -> LayoutObj[UiResult]:
|
||||
/// """Show wallet backup words preceded by an instruction screen and followed by
|
||||
/// confirmation."""
|
||||
|
@ -717,6 +717,8 @@ pub const TEXT_MAIN_GREY_LIGHT: TextStyle =
|
||||
TextStyle::new(Font::NORMAL, GREY_LIGHT, BG, GREY, GREY);
|
||||
pub const TEXT_SUB_GREY_LIGHT: TextStyle = TextStyle::new(Font::SUB, GREY_LIGHT, BG, GREY, GREY);
|
||||
pub const TEXT_SUB_GREY: TextStyle = TextStyle::new(Font::SUB, GREY, BG, GREY, GREY);
|
||||
pub const TEXT_SUB_GREEN_LIME: TextStyle =
|
||||
TextStyle::new(Font::SUB, GREEN_LIME, BG, GREEN_LIME, GREEN_LIME);
|
||||
pub const TEXT_WARNING: TextStyle = TextStyle::new(Font::NORMAL, ORANGE_LIGHT, BG, GREY, GREY);
|
||||
pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, GREY_EXTRA_LIGHT, BG, GREY, GREY)
|
||||
.with_line_breaking(LineBreaking::BreakWordsNoHyphen)
|
||||
|
@ -405,6 +405,7 @@ def flow_show_share_words(
|
||||
description: str,
|
||||
text_info: Iterable[str],
|
||||
text_confirm: str,
|
||||
highlight_repeated: bool,
|
||||
) -> LayoutObj[UiResult]:
|
||||
"""Show wallet backup words preceded by an instruction screen and followed by
|
||||
confirmation."""
|
||||
|
@ -664,6 +664,7 @@ class TR:
|
||||
reset__slip39_checklist_write_down: str = "Write down and check all shares"
|
||||
reset__slip39_checklist_write_down_recovery: str = "Write down & check all wallet backup shares"
|
||||
reset__the_threshold_sets_the_number_of_shares: str = "The threshold sets the number of shares "
|
||||
reset__the_word_is_repeated: str = "The word is repeated"
|
||||
reset__threshold_info: str = "= minimum number of unique word lists used for recovery."
|
||||
reset__title_backup_is_done: str = "Backup is done"
|
||||
reset__title_create_wallet: str = "Create wallet"
|
||||
|
@ -23,6 +23,7 @@ async def show_share_words(
|
||||
) -> None:
|
||||
|
||||
title = TR.reset__recovery_wallet_backup_title
|
||||
highlight_repeated = True
|
||||
if share_index is None:
|
||||
subtitle = ""
|
||||
elif group_index is None:
|
||||
@ -51,6 +52,7 @@ async def show_share_words(
|
||||
description=description,
|
||||
text_info=text_info,
|
||||
text_confirm=text_confirm,
|
||||
highlight_repeated=highlight_repeated,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -666,6 +666,7 @@
|
||||
"reset__slip39_checklist_write_down": "Write down and check all shares",
|
||||
"reset__slip39_checklist_write_down_recovery": "Write down & check all wallet backup shares",
|
||||
"reset__the_threshold_sets_the_number_of_shares": "The threshold sets the number of shares ",
|
||||
"reset__the_word_is_repeated": "The word is repeated",
|
||||
"reset__threshold_info": "= minimum number of unique word lists used for recovery.",
|
||||
"reset__title_backup_is_done": "Backup is done",
|
||||
"reset__title_create_wallet": "Create wallet",
|
||||
|
@ -939,5 +939,6 @@
|
||||
"937": "reset__words_may_repeat",
|
||||
"938": "reset__repeat_for_all_shares",
|
||||
"939": "homescreen__settings_subtitle",
|
||||
"940": "homescreen__settings_title"
|
||||
"940": "homescreen__settings_title",
|
||||
"941": "reset__the_word_is_repeated"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"current": {
|
||||
"merkle_root": "5b5781c3374ff27125228c121ab97436a2ba1f0581657ee56b9fa601fe3bde97",
|
||||
"datetime": "2024-06-24T13:47:55.544267",
|
||||
"commit": "d79768cec726c26ac1f82e62fc71b6d4568786a2"
|
||||
"merkle_root": "e283dd87dd502e2b2c3e2cbe1f52efcbe99c2bf6cd8ed883ed26800a6885e4e7",
|
||||
"datetime": "2024-06-24T12:47:16.181365",
|
||||
"commit": "c66a73b895b37f2a9de3ac6427f372c649ecea8d"
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user