1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-03-03 16:56:07 +00:00

feat(core/ui): show success screens between shares

This commit also adds minor improvements to reset flow.

[no changelog]
This commit is contained in:
obrusvit 2024-06-02 12:47:24 +02:00 committed by matejcik
parent 72bdea6754
commit 5bd6996ae4
15 changed files with 210 additions and 100 deletions

View File

@ -97,9 +97,11 @@ impl ConfirmResetCreate {
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)) .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info))
.one_button_request(ButtonRequestCode::ResetDevice.with_type("setup_device")); .one_button_request(ButtonRequestCode::ResetDevice.with_type("setup_device"));
// FIXME: TR::reset__cancel_create_wallet should be used but Button text on
// multiple lines not supported yet
let content_menu = Frame::left_aligned( let content_menu = Frame::left_aligned(
"".into(), "".into(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, "Cancel".into()), // TODO: use TR VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::buttons__cancel.into()),
) )
.with_cancel_button() .with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) .with_swipe(SwipeDirection::Right, SwipeSettings::immediate())

View File

@ -112,13 +112,12 @@ impl ShowShareWords {
.map(|_| Some(FlowMsg::Confirmed)); .map(|_| Some(FlowMsg::Confirmed));
let content_check_backup_intro = Frame::left_aligned( let content_check_backup_intro = Frame::left_aligned(
TR::reset__check_backup_title.into(), TR::reset__check_wallet_backup_title.into(),
SwipeContent::new(Paragraphs::new(Paragraph::new( SwipeContent::new(Paragraphs::new(Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT, &theme::TEXT_MAIN_GREY_LIGHT,
TR::reset__check_backup_instructions, TR::reset__check_backup_instructions,
))), ))),
) )
.with_subtitle(TR::words__instructions.into())
.with_footer(TR::instructions__swipe_up.into(), None) .with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()) .with_swipe(SwipeDirection::Up, SwipeSettings::default())
.map(|_| Some(FlowMsg::Confirmed)); .map(|_| Some(FlowMsg::Confirmed));

View File

@ -788,10 +788,15 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| { let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: Option<TString> = kwargs
.get(Qstr::MP_QSTR_description)?
.try_into_option()?
.and_then(|desc: TString| if desc.is_empty() { None } else { Some(desc) });
let content = StatusScreen::new_success(); let content = StatusScreen::new_success();
let obj = LayoutObj::new(SwipeUpScreen::new( let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content).with_normal_attach(None)) Frame::left_aligned(title, SwipeContent::new(content).with_normal_attach(None))
.with_footer(TR::instructions__swipe_up.into(), None) .with_footer(TR::instructions__swipe_up.into(), description)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()), .with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?; ))?;
Ok(obj.into()) Ok(obj.into())
@ -1572,7 +1577,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0, /// time_ms: int = 0,
/// ) -> LayoutObj[UiResult]: /// ) -> LayoutObj[UiResult]:
/// """Success modal. No buttons shown when `button` is empty string.""" /// """Success screen. Description is used in the footer."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
/// def show_info( /// def show_info(

View File

@ -278,7 +278,7 @@ def show_success(
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0, time_ms: int = 0,
) -> LayoutObj[UiResult]: ) -> LayoutObj[UiResult]:
"""Success modal. No buttons shown when `button` is empty string.""" """Success screen. Description is used in the footer."""
# rust/src/ui/model_mercury/layout.rs # rust/src/ui/model_mercury/layout.rs

View File

@ -69,17 +69,20 @@ async def _share_words_confirmed(
Return true if the words are confirmed successfully. Return true if the words are confirmed successfully.
""" """
# TODO: confirm_action("Select the words bla bla") from trezor.ui.layouts.reset import (
show_share_confirmation_success,
show_share_confirmation_failure,
)
if await _do_confirm_share_words(share_index, share_words, group_index): if await _do_confirm_share_words(share_index, share_words, group_index):
await _show_confirmation_success( await show_share_confirmation_success(
share_index, share_index,
num_of_shares, num_of_shares,
group_index, group_index,
) )
return True return True
else: else:
await _show_confirmation_failure() await show_share_confirmation_failure()
return False return False
@ -105,52 +108,6 @@ async def _do_confirm_share_words(
return True return True
async def _show_confirmation_success(
share_index: int | None = None,
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
subheader = TR.reset__finished_verifying_wallet_backup
text = ""
elif share_index == num_of_shares - 1:
if group_index is None:
subheader = TR.reset__finished_verifying_shares
else:
subheader = TR.reset__finished_verifying_group_template.format(
group_index + 1
)
text = ""
else:
if group_index is None:
subheader = TR.reset__share_checked_successfully_template.format(
share_index + 1
)
text = TR.reset__continue_with_share_template.format(share_index + 2)
else:
subheader = TR.reset__group_share_checked_successfully_template.format(
group_index + 1, share_index + 1
)
text = TR.reset__continue_with_next_share
return await show_success("success_recovery", text, subheader)
async def _show_confirmation_failure() -> None:
from trezor.ui.layouts.reset import show_reset_warning
await show_reset_warning(
"warning_backup_check",
TR.words__please_check_again,
TR.reset__wrong_word_selected,
TR.buttons__check_again,
ButtonRequestType.ResetDevice,
)
async def show_backup_warning() -> None: async def show_backup_warning() -> None:
from trezor.ui.layouts.reset import show_warning_backup from trezor.ui.layouts.reset import show_warning_backup

View File

@ -369,7 +369,11 @@ async def confirm_reset_device(_title: str, recovery: bool = False) -> None:
async def prompt_backup() -> bool: async def prompt_backup() -> bool:
# TODO: should we move this to `flow_prompt_backup`? # TODO: should we move this to `flow_prompt_backup`?
await interact( await interact(
RustLayout(trezorui2.show_success(title=TR.backup__new_wallet_created)), RustLayout(
trezorui2.show_success(
title=TR.backup__new_wallet_created, description=None
)
),
"backup_device", "backup_device",
ButtonRequestType.ResetDevice, ButtonRequestType.ResetDevice,
) )
@ -562,7 +566,7 @@ async def show_success(
RustLayout( RustLayout(
trezorui2.show_success( trezorui2.show_success(
title=content, title=content,
description="", description=subheader,
) )
), ),
br_type, br_type,

View File

@ -123,7 +123,7 @@ async def continue_recovery(
if show_info: if show_info:
# Show this just one-time # Show this just one-time
description = TR.recovery__only_first_n_letters description = TR.recovery__enter_each_word
else: else:
description = subtext or "" description = subtext or ""

View File

@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled from trezor.wire import ActionCancelled
from ..common import interact from ..common import interact
from . import RustLayout, raise_if_not_confirmed from . import RustLayout, raise_if_not_confirmed, show_success
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -57,11 +57,11 @@ async def select_word(
group_index: int | None = None, group_index: int | None = None,
) -> str: ) -> str:
if share_index is None: if share_index is None:
description: str = TR.reset__check_wallet_backup_title title: str = TR.reset__check_wallet_backup_title
elif group_index is None: elif group_index is None:
description: str = TR.reset__check_share_title_template.format(share_index + 1) title: str = TR.reset__check_share_title_template.format(share_index + 1)
else: else:
description: str = TR.reset__check_group_share_title_template.format( title: str = TR.reset__check_group_share_title_template.format(
group_index + 1, share_index + 1 group_index + 1, share_index + 1
) )
@ -74,10 +74,10 @@ async def select_word(
result = await RustLayout( result = await RustLayout(
trezorui2.select_word( trezorui2.select_word(
title=TR.reset__select_word_x_of_y_template.format( title=title,
description=TR.reset__select_word_x_of_y_template.format(
checked_index + 1, count checked_index + 1, count
), ),
description=description,
words=(words[0], words[1], words[2]), words=(words[0], words[1], words[2]),
) )
) )
@ -316,12 +316,9 @@ async def show_warning_backup() -> None:
async def show_success_backup() -> None: async def show_success_backup() -> None:
from . import show_success
await show_success( await show_success(
"success_backup", "success_backup",
TR.reset__use_your_backup, TR.backup__title_backup_completed,
TR.reset__your_backup_is_done,
) )
@ -332,15 +329,14 @@ async def show_reset_warning(
button: str | None = None, button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning, br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None: ) -> None:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed( await raise_if_not_confirmed(
interact( interact(
RustLayout( RustLayout(
trezorui2.show_warning( trezorui2.show_warning(
title=content or TR.words__warning, title=subheader or "",
description="", description=content,
value=subheader or "", value="",
button=button, button="",
allow_cancel=False, allow_cancel=False,
) )
), ),
@ -348,3 +344,49 @@ async def show_reset_warning(
br_code, br_code,
) )
) )
async def show_share_confirmation_success(
share_index: int | None = None,
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
# mercury UI shows only final wallet backup confirmation screen later
return
# TODO: super-shamir copy not done
if share_index == num_of_shares - 1:
title = TR.reset__share_completed_template.format(share_index + 1)
if group_index is None:
footer_description = ""
else:
footer_description = TR.reset__finished_verifying_group_template.format(
group_index + 1
)
else:
if group_index is None:
title = TR.reset__share_completed_template.format(share_index + 1)
footer_description = (
TR.instructions__shares_continue_with_x_template.format(share_index + 2)
)
else:
title = TR.reset__continue_with_next_share
footer_description = (
TR.reset__group_share_checked_successfully_template.format(
group_index + 1, share_index + 1
)
)
return await show_success("success_recovery", title, subheader=footer_description)
async def show_share_confirmation_failure() -> None:
await show_reset_warning(
"warning_backup_check",
TR.words__try_again,
TR.reset__incorrect_word_selected,
"",
ButtonRequestType.ResetDevice,
)

View File

@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled from trezor.wire import ActionCancelled
from ..common import interact from ..common import interact
from . import RustLayout, confirm_action, show_warning from . import RustLayout, confirm_action, show_warning, show_success
CONFIRMED = trezorui2.CONFIRMED # global_import_cache CONFIRMED = trezorui2.CONFIRMED # global_import_cache
@ -291,3 +291,47 @@ async def show_reset_warning(
button, button,
br_code=br_code, br_code=br_code,
) )
async def show_share_confirmation_success(
share_index: int | None = None,
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
subheader = TR.reset__finished_verifying_wallet_backup
text = ""
elif share_index == num_of_shares - 1:
if group_index is None:
subheader = TR.reset__finished_verifying_shares
else:
subheader = TR.reset__finished_verifying_group_template.format(
group_index + 1
)
text = ""
else:
if group_index is None:
subheader = TR.reset__share_checked_successfully_template.format(
share_index + 1
)
text = TR.reset__continue_with_share_template.format(share_index + 2)
else:
subheader = TR.reset__group_share_checked_successfully_template.format(
group_index + 1, share_index + 1
)
text = TR.reset__continue_with_next_share
return await show_success("success_recovery", text, subheader)
async def show_share_confirmation_failure() -> None:
await show_reset_warning(
"warning_backup_check",
TR.words__please_check_again,
TR.reset__wrong_word_selected,
TR.buttons__check_again,
ButtonRequestType.ResetDevice,
)

View File

@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled from trezor.wire import ActionCancelled
from ..common import interact from ..common import interact
from . import RustLayout, raise_if_not_confirmed from . import RustLayout, raise_if_not_confirmed, show_success
CONFIRMED = trezorui2.CONFIRMED # global_import_cache CONFIRMED = trezorui2.CONFIRMED # global_import_cache
@ -326,8 +326,6 @@ async def show_warning_backup() -> None:
async def show_success_backup() -> None: async def show_success_backup() -> None:
from . import show_success
await show_success( await show_success(
"success_backup", "success_backup",
TR.reset__use_your_backup, TR.reset__use_your_backup,
@ -357,3 +355,47 @@ async def show_reset_warning(
br_code, br_code,
) )
) )
async def show_share_confirmation_success(
share_index: int | None = None,
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
subheader = TR.reset__finished_verifying_wallet_backup
text = ""
elif share_index == num_of_shares - 1:
if group_index is None:
subheader = TR.reset__finished_verifying_shares
else:
subheader = TR.reset__finished_verifying_group_template.format(
group_index + 1
)
text = ""
else:
if group_index is None:
subheader = TR.reset__share_checked_successfully_template.format(
share_index + 1
)
text = TR.reset__continue_with_share_template.format(share_index + 2)
else:
subheader = TR.reset__group_share_checked_successfully_template.format(
group_index + 1, share_index + 1
)
text = TR.reset__continue_with_next_share
return await show_success("success_recovery", text, subheader)
async def show_share_confirmation_failure() -> None:
await show_reset_warning(
"warning_backup_check",
TR.words__please_check_again,
TR.reset__wrong_word_selected,
TR.buttons__check_again,
ButtonRequestType.ResetDevice,
)

View File

@ -1,8 +1,8 @@
{ {
"current": { "current": {
"merkle_root": "57561824d2bed19054d5570b0181b5907c7ae5546e4d6c2e589a727898a69237", "merkle_root": "7a115e582a5f5f09b1850946030762360f55e516a60cf960dffc9ba174c2e4d2",
"datetime": "2024-05-31T09:09:13.124759", "datetime": "2024-06-02T11:07:12.183601",
"commit": "6000eaf8280116d0cc947aa8d2aceb568dca56f8" "commit": "66496206ccbe9583203fbfebf8c9222e8c6379b8"
}, },
"history": [ "history": [
{ {

View File

@ -171,27 +171,32 @@ class LayoutContent(UnstructuredJSONReader):
return visible return visible
def _get_str_or_dict_text(self, key: str) -> str:
value = self.find_unique_value_by_key(key, "")
if isinstance(value, dict):
return value["text"]
return value
def title(self) -> str: def title(self) -> str:
"""Getting text that is displayed as a title.""" """Getting text that is displayed as a title and potentially subtitle."""
# There could be possibly subtitle as well # There could be possibly subtitle as well
title_parts: List[str] = [] title_parts: List[str] = []
def _get_str_or_dict_text(key: str) -> str: title = self._get_str_or_dict_text("title")
value = self.find_unique_value_by_key(key, "")
if isinstance(value, dict):
return value["text"]
return value
title = _get_str_or_dict_text("title")
if title: if title:
title_parts.append(title) title_parts.append(title)
subtitle = _get_str_or_dict_text("subtitle") subtitle = self.subtitle()
if subtitle: if subtitle:
title_parts.append(subtitle) title_parts.append(subtitle)
return "\n".join(title_parts) return "\n".join(title_parts)
def subtitle(self) -> str:
"""Getting text that is displayed as a subtitle."""
subtitle = self._get_str_or_dict_text("subtitle")
return subtitle
def text_content(self) -> str: def text_content(self) -> str:
"""What is on the screen, in one long string, so content can be """What is on the screen, in one long string, so content can be
asserted regardless of newlines. Also getting rid of possible ellipsis. asserted regardless of newlines. Also getting rid of possible ellipsis.

View File

@ -277,13 +277,21 @@ def check_share(
Given the mnemonic word list, proceed with the backup check: Given the mnemonic word list, proceed with the backup check:
three rounds of `Select word X of Y` choices. three rounds of `Select word X of Y` choices.
""" """
re_num_of_word = r"\d+"
for _ in range(3): for _ in range(3):
if debug.model is models.T2B1: if debug.model is models.T2T1:
# T2B1 has the instruction in the title # T2T1 has position as the first number in the text
word_pos_match = re.search(r"\d+", debug.wait_layout().title()) word_pos_match = re.search(
re_num_of_word, debug.wait_layout().text_content()
)
elif debug.model is models.T2B1:
# other models have the instruction in the title/subtitle
word_pos_match = re.search(re_num_of_word, debug.wait_layout().title())
elif debug.model is models.T3T1:
word_pos_match = re.search(re_num_of_word, debug.wait_layout().subtitle())
else: else:
# Other models has position as the first number in the text word_pos_match = None
word_pos_match = re.search(r"\d+", debug.wait_layout().text_content())
assert word_pos_match is not None assert word_pos_match is not None
word_pos = int(word_pos_match.group(0)) word_pos = int(word_pos_match.group(0))

View File

@ -1255,9 +1255,11 @@ def get_mnemonic_and_confirm_success(
# mnemonic phrases # mnemonic phrases
mnemonic = yield from read_and_confirm_mnemonic(debug) mnemonic = yield from read_and_confirm_mnemonic(debug)
br = yield # confirm recovery seed check is_slip39 = len(mnemonic.split()) in (20, 33)
assert br.code == B.Success if debug.model in (models.T2T1, models.T2B1) or is_slip39:
debug.press_yes() br = yield # confirm recovery share check
assert br.code == B.Success
debug.press_yes()
br = yield # confirm success br = yield # confirm success
assert br.code == B.Success assert br.code == B.Success

View File

@ -93,7 +93,7 @@ class RecoveryFlow:
def enter_your_backup(self) -> BRGeneratorType: def enter_your_backup(self) -> BRGeneratorType:
yield yield
if self.debug.model is models.T3T1: if self.debug.model is models.T3T1:
TR.assert_in(self._text_content(), "recovery__only_first_n_letters") TR.assert_in(self._text_content(), "recovery__enter_each_word")
else: else:
TR.assert_in(self._text_content(), "recovery__enter_backup") TR.assert_in(self._text_content(), "recovery__enter_backup")
is_dry_run = any( is_dry_run = any(
@ -110,7 +110,7 @@ class RecoveryFlow:
yield yield
TR.assert_in_multiple( TR.assert_in_multiple(
self._text_content(), self._text_content(),
["recovery__enter_any_share", "recovery__only_first_n_letters"], ["recovery__enter_any_share", "recovery__enter_each_word"],
) )
is_dry_run = any( is_dry_run = any(
title in self.debug.wait_layout().title().lower() title in self.debug.wait_layout().title().lower()
@ -127,7 +127,7 @@ class RecoveryFlow:
if self.client.model is models.T2B1: if self.client.model is models.T2B1:
TR.assert_in(self._text_content(), "recovery__num_of_words") TR.assert_in(self._text_content(), "recovery__num_of_words")
elif self.client.model is models.T3T1: elif self.client.model is models.T3T1:
TR.assert_in(self._text_content(), "recovery__only_first_n_letters") TR.assert_in(self._text_content(), "recovery__enter_each_word")
else: else:
TR.assert_in(self._text_content(), "recovery__enter_any_share") TR.assert_in(self._text_content(), "recovery__enter_any_share")
self.debug.press_no() self.debug.press_no()