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))
.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(
"".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_swipe(SwipeDirection::Right, SwipeSettings::immediate())

View File

@ -112,13 +112,12 @@ impl ShowShareWords {
.map(|_| Some(FlowMsg::Confirmed));
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(
&theme::TEXT_MAIN_GREY_LIGHT,
TR::reset__check_backup_instructions,
))),
)
.with_subtitle(TR::words__instructions.into())
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.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 {
let block = move |_args: &[Obj], kwargs: &Map| {
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 obj = LayoutObj::new(SwipeUpScreen::new(
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()),
))?;
Ok(obj.into())
@ -1572,7 +1577,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> 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(),
/// def show_info(

View File

@ -278,7 +278,7 @@ def show_success(
allow_cancel: bool = False,
time_ms: int = 0,
) -> 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

View File

@ -69,17 +69,20 @@ async def _share_words_confirmed(
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):
await _show_confirmation_success(
await show_share_confirmation_success(
share_index,
num_of_shares,
group_index,
)
return True
else:
await _show_confirmation_failure()
await show_share_confirmation_failure()
return False
@ -105,52 +108,6 @@ async def _do_confirm_share_words(
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:
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:
# TODO: should we move this to `flow_prompt_backup`?
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",
ButtonRequestType.ResetDevice,
)
@ -562,7 +566,7 @@ async def show_success(
RustLayout(
trezorui2.show_success(
title=content,
description="",
description=subheader,
)
),
br_type,

View File

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

View File

@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from ..common import interact
from . import RustLayout, raise_if_not_confirmed
from . import RustLayout, raise_if_not_confirmed, show_success
if TYPE_CHECKING:
pass
@ -57,11 +57,11 @@ async def select_word(
group_index: int | None = None,
) -> str:
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:
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:
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
)
@ -74,10 +74,10 @@ async def select_word(
result = await RustLayout(
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
),
description=description,
words=(words[0], words[1], words[2]),
)
)
@ -316,12 +316,9 @@ async def show_warning_backup() -> None:
async def show_success_backup() -> None:
from . import show_success
await show_success(
"success_backup",
TR.reset__use_your_backup,
TR.reset__your_backup_is_done,
TR.backup__title_backup_completed,
)
@ -332,15 +329,14 @@ async def show_reset_warning(
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_warning(
title=content or TR.words__warning,
description="",
value=subheader or "",
button=button,
title=subheader or "",
description=content,
value="",
button="",
allow_cancel=False,
)
),
@ -348,3 +344,49 @@ async def show_reset_warning(
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 ..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
@ -291,3 +291,47 @@ async def show_reset_warning(
button,
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 ..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
@ -326,8 +326,6 @@ async def show_warning_backup() -> None:
async def show_success_backup() -> None:
from . import show_success
await show_success(
"success_backup",
TR.reset__use_your_backup,
@ -357,3 +355,47 @@ async def show_reset_warning(
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": {
"merkle_root": "57561824d2bed19054d5570b0181b5907c7ae5546e4d6c2e589a727898a69237",
"datetime": "2024-05-31T09:09:13.124759",
"commit": "6000eaf8280116d0cc947aa8d2aceb568dca56f8"
"merkle_root": "7a115e582a5f5f09b1850946030762360f55e516a60cf960dffc9ba174c2e4d2",
"datetime": "2024-06-02T11:07:12.183601",
"commit": "66496206ccbe9583203fbfebf8c9222e8c6379b8"
},
"history": [
{

View File

@ -171,27 +171,32 @@ class LayoutContent(UnstructuredJSONReader):
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:
"""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
title_parts: List[str] = []
def _get_str_or_dict_text(key: str) -> str:
value = self.find_unique_value_by_key(key, "")
if isinstance(value, dict):
return value["text"]
return value
title = _get_str_or_dict_text("title")
title = self._get_str_or_dict_text("title")
if title:
title_parts.append(title)
subtitle = _get_str_or_dict_text("subtitle")
subtitle = self.subtitle()
if subtitle:
title_parts.append(subtitle)
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:
"""What is on the screen, in one long string, so content can be
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:
three rounds of `Select word X of Y` choices.
"""
re_num_of_word = r"\d+"
for _ in range(3):
if debug.model is models.T2B1:
# T2B1 has the instruction in the title
word_pos_match = re.search(r"\d+", debug.wait_layout().title())
if debug.model is models.T2T1:
# T2T1 has position as the first number in the text
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:
# Other models has position as the first number in the text
word_pos_match = re.search(r"\d+", debug.wait_layout().text_content())
word_pos_match = None
assert word_pos_match is not None
word_pos = int(word_pos_match.group(0))

View File

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

View File

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