Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core/ui): T3T1 success screens between shares #3992

Merged
merged 6 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/.changelog.d/3992.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[T3T1] Improve instruction screens during multi-share recovery process
4 changes: 4 additions & 0 deletions core/embed/rust/librust_qstr.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,12 @@ static void _librust_qstrs(void) {
MP_QSTR_fingerprint;
MP_QSTR_firmware_update__title;
MP_QSTR_firmware_update__title_fingerprint;
MP_QSTR_first_screen;
MP_QSTR_flow_confirm_output;
MP_QSTR_flow_confirm_reset;
MP_QSTR_flow_confirm_set_new_pin;
MP_QSTR_flow_confirm_summary;
MP_QSTR_flow_continue_recovery;
MP_QSTR_flow_get_address;
MP_QSTR_flow_prompt_backup;
MP_QSTR_flow_request_number;
Expand Down Expand Up @@ -659,7 +661,9 @@ static void _librust_qstrs(void) {
MP_QSTR_storage_msg__verifying_pin;
MP_QSTR_storage_msg__wrong_pin;
MP_QSTR_subprompt;
MP_QSTR_subtext;
MP_QSTR_subtitle;
MP_QSTR_text;
MP_QSTR_text_confirm;
MP_QSTR_text_info;
MP_QSTR_text_mono;
Expand Down
51 changes: 40 additions & 11 deletions core/embed/rust/src/ui/component/button_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,36 @@ use crate::ui::{
use crate::ui::component::swipe_detect::SwipeConfig;

/// Component that sends a ButtonRequest after receiving Event::Attach. The
/// request is only sent once.
/// request is either sent only once or on every Event::Attach configured by
/// `policy`.
#[derive(Clone)]
pub struct OneButtonRequest<T> {
pub struct SendButtonRequest<T> {
button_request: Option<ButtonRequest>,
pub inner: T,
policy: SendButtonRequestPolicy,
}

impl<T> OneButtonRequest<T> {
pub const fn new(button_request: ButtonRequest, inner: T) -> Self {
#[derive(Clone)]
pub enum SendButtonRequestPolicy {
OnAttachOnce,
OnAttachAlways,
}

impl<T> SendButtonRequest<T> {
pub const fn new(
button_request: ButtonRequest,
inner: T,
policy: SendButtonRequestPolicy,
) -> Self {
Self {
button_request: Some(button_request),
inner,
policy,
}
}
}

impl<T: Component> Component for OneButtonRequest<T> {
impl<T: Component> Component for SendButtonRequest<T> {
type Msg = T::Msg;

fn place(&mut self, bounds: Rect) -> Rect {
Expand All @@ -33,8 +46,17 @@ impl<T: Component> Component for OneButtonRequest<T> {

fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if matches!(event, Event::Attach(_)) {
if let Some(button_request) = self.button_request.take() {
ctx.send_button_request(button_request.code, button_request.name)
match self.policy {
SendButtonRequestPolicy::OnAttachOnce => {
if let Some(br) = self.button_request.take() {
ctx.send_button_request(br.code, br.name)
}
}
SendButtonRequestPolicy::OnAttachAlways => {
if let Some(br) = self.button_request.clone() {
ctx.send_button_request(br.code, br.name);
}
}
}
}
self.inner.event(ctx, event)
Expand All @@ -50,7 +72,7 @@ impl<T: Component> Component for OneButtonRequest<T> {
}

#[cfg(all(feature = "micropython", feature = "touch", feature = "new_rendering"))]
impl<T: crate::ui::flow::Swipable> crate::ui::flow::Swipable for OneButtonRequest<T> {
impl<T: crate::ui::flow::Swipable> crate::ui::flow::Swipable for SendButtonRequest<T> {
fn get_swipe_config(&self) -> SwipeConfig {
self.inner.get_swipe_config()
}
Expand All @@ -61,18 +83,25 @@ impl<T: crate::ui::flow::Swipable> crate::ui::flow::Swipable for OneButtonReques
}

#[cfg(feature = "ui_debug")]
impl<T: crate::trace::Trace> crate::trace::Trace for OneButtonRequest<T> {
impl<T: crate::trace::Trace> crate::trace::Trace for SendButtonRequest<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.inner.trace(t)
}
}

pub trait ButtonRequestExt {
fn one_button_request(self, br: ButtonRequest) -> OneButtonRequest<Self>
fn one_button_request(self, br: ButtonRequest) -> SendButtonRequest<Self>
where
Self: Sized,
{
SendButtonRequest::new(br, self, SendButtonRequestPolicy::OnAttachOnce)
}

fn repeated_button_request(self, br: ButtonRequest) -> SendButtonRequest<Self>
where
Self: Sized,
{
OneButtonRequest::new(br, self)
SendButtonRequest::new(br, self, SendButtonRequestPolicy::OnAttachAlways)
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/embed/rust/src/ui/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub mod timeout;
pub use bar::Bar;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
pub use border::Border;
pub use button_request::{ButtonRequestExt, OneButtonRequest};
pub use button_request::{ButtonRequestExt, SendButtonRequest};
#[cfg(all(feature = "jpeg", feature = "ui_image_buffer", feature = "micropython"))]
pub use cached_jpeg::CachedJpeg;
pub use empty::Empty;
Expand Down
233 changes: 233 additions & 0 deletions core/embed/rust/src/ui/model_mercury/flow/continue_recovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
use crate::{
error,
micropython::{map::Map, obj::Obj, qstr::Qstr, util},
strutil::TString,
translations::TR,
ui::{
button_request::{ButtonRequest, ButtonRequestCode},
component::{
button_request::ButtonRequestExt,
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
ComponentExt, SwipeDirection,
},
flow::{
base::{DecisionBuilder as _, StateChange},
FlowMsg, FlowState, SwipeFlow,
},
layout::obj::LayoutObj,
model_mercury::component::{CancelInfoConfirmMsg, PromptScreen, SwipeContent},
},
};

use super::super::{
component::{Frame, FrameMsg, VerticalMenu, VerticalMenuChoiceMsg},
theme,
};

const RECOVERY_TYPE_NORMAL: u32 = 0;
const RECOVERY_TYPE_DRY_RUN: u32 = 1;
const RECOVERY_TYPE_UNLOCK_REPEATED_BACKUP: u32 = 2;

#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ContinueRecoveryBeforeShares {
Main,
Menu,
}

#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ContinueRecoveryBetweenShares {
Main,
Menu,
CancelIntro,
CancelConfirm,
}

impl FlowState for ContinueRecoveryBeforeShares {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}

fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange {
match (self, direction) {
(Self::Main, SwipeDirection::Left) => Self::Menu.swipe(direction),
(Self::Menu, SwipeDirection::Right) => Self::Main.swipe(direction),
(Self::Main, SwipeDirection::Up) => self.return_msg(FlowMsg::Confirmed),
_ => self.do_nothing(),
}
}

fn handle_event(&'static self, msg: FlowMsg) -> StateChange {
match (self, msg) {
(Self::Main, FlowMsg::Info) => Self::Menu.transit(),
(Self::Menu, FlowMsg::Cancelled) => Self::Main.swipe_right(),
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}

impl FlowState for ContinueRecoveryBetweenShares {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}

fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange {
match (self, direction) {
(Self::Main, SwipeDirection::Left) => Self::Menu.swipe(direction),
(Self::Menu, SwipeDirection::Right) => Self::Main.swipe(direction),
(Self::Main, SwipeDirection::Up) => self.return_msg(FlowMsg::Confirmed),
(Self::CancelIntro, SwipeDirection::Up) => Self::CancelConfirm.swipe(direction),
(Self::CancelIntro, SwipeDirection::Right) => Self::Menu.swipe(direction),
(Self::CancelConfirm, SwipeDirection::Down) => Self::CancelIntro.swipe(direction),
_ => self.do_nothing(),
}
}

fn handle_event(&'static self, msg: FlowMsg) -> StateChange {
match (self, msg) {
(Self::Main, FlowMsg::Info) => Self::Menu.transit(),
(Self::Menu, FlowMsg::Choice(0)) => Self::CancelIntro.swipe_left(),
(Self::Menu, FlowMsg::Cancelled) => Self::Main.swipe_right(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gonna send ButtonRequest too right? Is Suite OK with it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will send BR too.

Yes, I think Suite should be fine with it because sending BRs behaves in the same way as in the old implementation of homescreen_dialog, i.e. the success/instruction screen sends BR and the confirmation of flow cancellation also sends BR. These two screens are called in a loop (until you confirm the success/instruction screen) so they possibly send the BRs repeatedly. Anyway, I'll ask QA for thorough testing of these scenarios.

(Self::CancelIntro, FlowMsg::Cancelled) => Self::Menu.transit(),
(Self::CancelConfirm, FlowMsg::Cancelled) => Self::CancelIntro.swipe_right(),
(Self::CancelConfirm, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}

#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_continue_recovery(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe {
util::try_with_args_and_kwargs(n_args, args, kwargs, ContinueRecoveryBeforeShares::new_obj)
}
}

impl ContinueRecoveryBeforeShares {
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let first_screen: bool = kwargs.get(Qstr::MP_QSTR_first_screen)?.try_into()?;
let recovery_type: u32 = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
let text: TString = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?; // #shares entered
let subtext: Option<TString> = kwargs.get(Qstr::MP_QSTR_subtext)?.try_into_option()?; // #shares remaining

let (title, cancel_btn, cancel_title, cancel_intro) =
if recovery_type == RECOVERY_TYPE_NORMAL {
(
TR::recovery__title,
TR::recovery__title_cancel_recovery,
TR::recovery__title_cancel_recovery,
TR::recovery__wanna_cancel_recovery,
)
} else {
// dry-run
(
TR::recovery__title_dry_run,
TR::recovery__cancel_dry_run,
TR::recovery__title_cancel_dry_run,
TR::recovery__wanna_cancel_dry_run,
)
};

let mut pars = ParagraphVecShort::new();
let footer_instruction;
let footer_description;
if first_screen {
pars.add(Paragraph::new(
&theme::TEXT_MAIN_GREY_EXTRA_LIGHT,
TR::recovery__enter_each_word,
));
footer_instruction = TR::instructions__swipe_up.into();
footer_description = None;
} else {
pars.add(Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, text));
if let Some(sub) = subtext {
pars.add(Paragraph::new(&theme::TEXT_SUB_GREY, sub));
}
footer_instruction = TR::instructions__swipe_up.into();
footer_description = Some(TR::instructions__enter_next_share.into());
}

let paragraphs_main = Paragraphs::new(pars);
let content_main = Frame::left_aligned(title.into(), SwipeContent::new(paragraphs_main))
.with_subtitle(TR::words__instructions.into())
.with_menu_button()
.with_footer(footer_instruction, footer_description)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info))
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::RecoveryHomepage,
"recovery".into(),
));

let content_menu = Frame::left_aligned(
TString::empty(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, cancel_btn.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});

let paragraphs_cancel = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, cancel_intro).with_bottom_padding(17),
Paragraph::new(&theme::TEXT_WARNING, TR::recovery__progress_will_be_lost),
])
.into_paragraphs();
let content_cancel_intro =
Frame::left_aligned(cancel_title.into(), SwipeContent::new(paragraphs_cancel))
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::words__continue_anyway.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
})
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::ProtectCall,
"abort_recovery".into(),
));

let content_cancel_confirm = Frame::left_aligned(
cancel_title.into(),
SwipeContent::new(PromptScreen::new_tap_to_cancel()),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});

let res = if first_screen {
SwipeFlow::new(&ContinueRecoveryBeforeShares::Main)?
.with_page(&ContinueRecoveryBeforeShares::Main, content_main)?
.with_page(&ContinueRecoveryBeforeShares::Menu, content_menu)?
} else {
SwipeFlow::new(&ContinueRecoveryBetweenShares::Main)?
.with_page(&ContinueRecoveryBetweenShares::Main, content_main)?
.with_page(&ContinueRecoveryBetweenShares::Menu, content_menu)?
.with_page(
&ContinueRecoveryBetweenShares::CancelIntro,
content_cancel_intro,
)?
.with_page(
&ContinueRecoveryBetweenShares::CancelConfirm,
content_cancel_confirm,
)?
};
Ok(LayoutObj::new(res)?.into())
}
}
2 changes: 2 additions & 0 deletions core/embed/rust/src/ui/model_mercury/flow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod confirm_output;
pub mod confirm_reset;
pub mod confirm_set_new_pin;
pub mod confirm_summary;
pub mod continue_recovery;
pub mod get_address;
pub mod prompt_backup;
pub mod request_number;
Expand All @@ -20,6 +21,7 @@ pub use confirm_output::new_confirm_output;
pub use confirm_reset::new_confirm_reset;
pub use confirm_set_new_pin::SetNewPin;
pub use confirm_summary::new_confirm_summary;
pub use continue_recovery::new_continue_recovery;
pub use get_address::GetAddress;
pub use prompt_backup::PromptBackup;
pub use request_number::RequestNumber;
Expand Down
Loading
Loading