1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-22 01:59:02 +00:00

feat(eckhart): confirm summary flow

This commit is contained in:
Lukas Bielesch 2025-03-28 13:10:36 +01:00 committed by M1nd3r
parent 6f2f59ddaf
commit 51c917bfed
4 changed files with 277 additions and 13 deletions

View File

@ -0,0 +1,226 @@
use heapless::Vec;
use crate::{
error::{self},
strutil::TString,
translations::TR,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs},
ComponentExt, MsgMap,
},
flow::{
base::{Decision, DecisionBuilder as _},
FlowController, FlowMsg, SwipeFlow,
},
geometry::{Alignment, Direction, LinearPlacement, Offset},
},
};
use super::super::{
component::Button,
firmware::{
ActionBar, Header, Hint, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
VerticalMenuScreenMsg,
},
theme,
};
const MENU_ITEM_CANCEL: usize = 0;
const MENU_ITEM_EXTRA_INFO: usize = 1;
const MENU_ITEM_ACCOUNT_INFO: usize = 2;
const TIMEOUT_MS: u32 = 2000;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmSummary {
Summary,
Menu,
ExtraInfo,
AccountInfo,
Cancel,
Cancelled,
}
impl FlowController for ConfirmSummary {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, _direction: Direction) -> Decision {
self.do_nothing()
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Summary, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Summary, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::Cancel.goto(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_EXTRA_INFO)) => Self::ExtraInfo.goto(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => Self::AccountInfo.goto(),
(Self::Menu, FlowMsg::Cancelled) => Self::Summary.goto(),
(_, FlowMsg::Cancelled) => Self::Menu.goto(),
(Self::Cancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}
fn content_menu_info(
title: TString<'static>,
subtitle: Option<TString<'static>>,
paragraphs: Option<ParagraphVecShort<'static>>,
) -> MsgMap<
TextScreen<Paragraphs<ParagraphVecShort<'static>>>,
impl Fn(TextScreenMsg) -> Option<FlowMsg>,
> {
TextScreen::new(
paragraphs
.map_or_else(ParagraphVecShort::new, |p| p)
.into_paragraphs()
.with_placement(LinearPlacement::vertical())
.with_spacing(12),
)
.with_header(Header::new(title).with_close_button())
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.map(|_| Some(FlowMsg::Cancelled))
}
#[allow(clippy::too_many_arguments)]
pub fn new_confirm_summary(
title: TString<'static>,
amount: TString<'static>,
amount_label: TString<'static>,
fee: TString<'static>,
fee_label: TString<'static>,
account_paragraphs: Option<ParagraphVecShort<'static>>,
extra_title: Option<TString<'static>>,
extra_paragraphs: Option<ParagraphVecShort<'static>>,
verb_cancel: Option<TString<'static>>,
) -> Result<SwipeFlow, error::Error> {
// Summary
let summary_paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_SMALL_LIGHT, amount_label),
Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, amount),
Paragraph::new(&theme::TEXT_SMALL_LIGHT, fee_label),
Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, fee),
]);
let content_summary = TextScreen::new(
summary_paragraphs
.into_paragraphs()
.with_placement(LinearPlacement::vertical())
.with_spacing(12),
)
.with_header(Header::new(title).with_menu_button())
.with_action_bar(ActionBar::new_single(
Button::with_text(TR::instructions__hold_to_sign.into())
.with_long_press(theme::CONFIRM_HOLD_DURATION)
.styled(theme::button_confirm()),
))
.with_hint(Hint::new_instruction(
TR::send__send_in_the_app,
Some(theme::ICON_INFO),
))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
TextScreenMsg::Menu => Some(FlowMsg::Info),
});
// Menu
let mut menu = VerticalMenu::empty();
let mut menu_items = Vec::<usize, 3>::new();
if account_paragraphs.is_some() {
menu = menu.item(
Button::with_text(TR::address_details__account_info.into())
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
unwrap!(menu_items.push(MENU_ITEM_ACCOUNT_INFO));
}
if extra_paragraphs.is_some() {
menu = menu.item(
Button::with_text(extra_title.unwrap_or(TR::buttons__more_info.into()))
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
unwrap!(menu_items.push(MENU_ITEM_EXTRA_INFO));
}
menu = menu.item(
Button::with_text(verb_cancel.unwrap_or(TR::buttons__cancel.into()))
.styled(theme::menu_item_title_orange())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
unwrap!(menu_items.push(MENU_ITEM_CANCEL));
let content_menu = VerticalMenuScreen::new(menu)
.with_header(Header::new(title).with_close_button())
.map(move |msg| match msg {
VerticalMenuScreenMsg::Selected(i) => {
let selected_item = menu_items[i];
Some(FlowMsg::Choice(selected_item))
}
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
_ => None,
});
// ExtraInfo
let content_extra = content_menu_info(
extra_title.unwrap_or(TR::buttons__more_info.into()),
None,
extra_paragraphs,
);
// AccountInfo
let content_account = content_menu_info(
TR::address_details__account_info.into(),
Some(TR::send__send_from.into()),
account_paragraphs,
);
// Cancel
let content_cancel = TextScreen::new(
Paragraph::new(&theme::TEXT_REGULAR, TR::send__cancel_sign)
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(TR::words__send.into()))
.with_action_bar(ActionBar::new_double(
Button::with_icon(theme::ICON_CHEVRON_LEFT),
Button::with_text(TR::buttons__cancel.into()).styled(theme::button_cancel()),
))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
_ => None,
});
// Cancelled
let content_cancelled = TextScreen::new(
Paragraph::new(&theme::TEXT_REGULAR, TR::send__sign_cancelled)
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(TR::words__title_done.into()).with_icon(theme::ICON_DONE, theme::GREY))
.with_action_bar(ActionBar::new_timeout(
Button::with_text(TR::instructions__continue_in_app.into()),
TIMEOUT_MS,
))
.map(|_| Some(FlowMsg::Confirmed));
let mut res = SwipeFlow::new(&ConfirmSummary::Summary)?;
res.add_page(&ConfirmSummary::Summary, content_summary)?
.add_page(&ConfirmSummary::Menu, content_menu)?
.add_page(&ConfirmSummary::ExtraInfo, content_extra)?
.add_page(&ConfirmSummary::AccountInfo, content_account)?
.add_page(&ConfirmSummary::Cancel, content_cancel)?
.add_page(&ConfirmSummary::Cancelled, content_cancelled)?;
Ok(res)
}

View File

@ -1,5 +1,6 @@
pub mod confirm_reset;
pub mod confirm_set_new_pin;
pub mod confirm_summary;
pub mod continue_recovery_homepage;
pub mod get_address;
pub mod prompt_backup;
@ -9,6 +10,7 @@ pub mod show_share_words;
pub use confirm_reset::new_confirm_reset;
pub use confirm_set_new_pin::new_set_new_pin;
pub use confirm_summary::new_confirm_summary;
pub use continue_recovery_homepage::new_continue_recovery_homepage;
pub use get_address::GetAddress;
pub use prompt_backup::PromptBackup;

View File

@ -1,4 +1,5 @@
use core::cmp::Ordering;
use heapless::Vec;
use crate::{
error::Error,
@ -217,17 +218,52 @@ impl FirmwareUI for UIEckhart {
}
fn confirm_summary(
_amount: TString<'static>,
_amount_label: TString<'static>,
_fee: TString<'static>,
_fee_label: TString<'static>,
_title: Option<TString<'static>>,
_account_items: Option<Obj>,
_extra_items: Option<Obj>,
_extra_title: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
amount: TString<'static>,
amount_label: TString<'static>,
fee: TString<'static>,
fee_label: TString<'static>,
title: Option<TString<'static>>,
account_items: Option<Obj>,
extra_items: Option<Obj>,
extra_title: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
// collect available info
let account_paragraphs = if let Some(items) = account_items {
let mut paragraphs = ParagraphVecShort::new();
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_SMALL_LIGHT, label).no_break()));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, value)));
}
Some(paragraphs)
} else {
None
};
let extra_paragraphs = if let Some(items) = extra_items {
let mut paragraphs = ParagraphVecShort::new();
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_SMALL_LIGHT, label).no_break()));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, value)));
}
Some(paragraphs)
} else {
None
};
let flow = flow::new_confirm_summary(
title.unwrap_or(TString::empty()),
amount,
amount_label,
fee,
fee_label,
account_paragraphs,
extra_title,
extra_paragraphs,
verb_cancel,
)?;
Ok(flow)
}
fn confirm_properties(

View File

@ -677,14 +677,14 @@ def confirm_total(
br_name: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> Awaitable[None]:
title = title or TR.words__title_summary # def_arg
title = title or TR.words__send # def_arg
total_label = total_label or TR.send__total_amount # def_arg
fee_label = fee_label or TR.send__incl_transaction_fee # def_arg
fee_items = []
account_items = []
if source_account:
account_items.append((TR.confirm_total__sending_from_account, source_account))
account_items.append((TR.words__account, source_account))
if source_account_path:
account_items.append((TR.address_details__derivation_path, source_account_path))
if fee_rate_amount:
@ -718,7 +718,7 @@ def _confirm_summary(
br_name: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> Awaitable[None]:
title = title or TR.words__title_summary # def_arg
title = title or TR.words__send # def_arg
return raise_if_not_confirmed(
trezorui_api.confirm_summary(