mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-04-24 19:19:03 +00:00
feat(eckhart): confirm output flow
This commit is contained in:
parent
06437cea27
commit
00aa5774cd
core
embed/rust
mocks
src/trezor/ui/layouts/eckhart
translations
@ -622,6 +622,8 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_send__maximum_fee;
|
||||
MP_QSTR_send__receiving_to_multisig;
|
||||
MP_QSTR_send__send_from;
|
||||
MP_QSTR_send__send_in_the_app;
|
||||
MP_QSTR_send__sign_cancelled;
|
||||
MP_QSTR_send__sign_transaction;
|
||||
MP_QSTR_send__title_confirm_sending;
|
||||
MP_QSTR_send__title_joint_transaction;
|
||||
@ -787,6 +789,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_words__receive;
|
||||
MP_QSTR_words__recipient;
|
||||
MP_QSTR_words__recovery_share;
|
||||
MP_QSTR_words__send;
|
||||
MP_QSTR_words__settings;
|
||||
MP_QSTR_words__sign;
|
||||
MP_QSTR_words__signer;
|
||||
@ -802,6 +805,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_words__title_threshold;
|
||||
MP_QSTR_words__try_again;
|
||||
MP_QSTR_words__unknown;
|
||||
MP_QSTR_words__wallet;
|
||||
MP_QSTR_words__warning;
|
||||
MP_QSTR_words__writable;
|
||||
MP_QSTR_words__yes;
|
||||
|
@ -1264,7 +1264,7 @@ pub enum TranslatedString {
|
||||
pin__cancel_description = 870, // "Continue without PIN"
|
||||
pin__cancel_info = 871, // "Without a PIN, anyone can access this device."
|
||||
pin__cancel_setup = 872, // "Cancel PIN setup"
|
||||
send__cancel_sign = 873, // "Cancel sign"
|
||||
send__cancel_sign = 873, // {"Bolt": "Cancel sign", "Caesar": "Cancel sign", "Delizia": "Cancel sign", "Eckhart": "Cancel sign?"}
|
||||
send__send_from = 874, // "Send from"
|
||||
instructions__hold_to_sign = 875, // "Hold to sign"
|
||||
confirm_total__fee_rate = 876, // "Fee rate"
|
||||
@ -1412,6 +1412,10 @@ pub enum TranslatedString {
|
||||
reset__recovery_share_description = 998, // "A recovery share is a list of words you wrote down when setting up your Trezor."
|
||||
reset__recovery_share_number = 999, // "Your wallet backup consists of 1 to 16 shares."
|
||||
words__recovery_share = 1000, // "Recovery share"
|
||||
send__send_in_the_app = 1001, // "After signing, send the transaction in the app."
|
||||
send__sign_cancelled = 1002, // "Sign cancelled."
|
||||
words__send = 1003, // "Send"
|
||||
words__wallet = 1004, // "Wallet"
|
||||
}
|
||||
|
||||
impl TranslatedString {
|
||||
@ -2735,7 +2739,14 @@ impl TranslatedString {
|
||||
Self::pin__cancel_description => "Continue without PIN",
|
||||
Self::pin__cancel_info => "Without a PIN, anyone can access this device.",
|
||||
Self::pin__cancel_setup => "Cancel PIN setup",
|
||||
#[cfg(feature = "layout_bolt")]
|
||||
Self::send__cancel_sign => "Cancel sign",
|
||||
#[cfg(feature = "layout_caesar")]
|
||||
Self::send__cancel_sign => "Cancel sign",
|
||||
#[cfg(feature = "layout_delizia")]
|
||||
Self::send__cancel_sign => "Cancel sign",
|
||||
#[cfg(feature = "layout_eckhart")]
|
||||
Self::send__cancel_sign => "Cancel sign?",
|
||||
Self::send__send_from => "Send from",
|
||||
Self::instructions__hold_to_sign => "Hold to sign",
|
||||
Self::confirm_total__fee_rate => "Fee rate",
|
||||
@ -2918,6 +2929,10 @@ impl TranslatedString {
|
||||
Self::reset__recovery_share_description => "A recovery share is a list of words you wrote down when setting up your Trezor.",
|
||||
Self::reset__recovery_share_number => "Your wallet backup consists of 1 to 16 shares.",
|
||||
Self::words__recovery_share => "Recovery share",
|
||||
Self::send__send_in_the_app => "After signing, send the transaction in the app.",
|
||||
Self::send__sign_cancelled => "Sign cancelled.",
|
||||
Self::words__send => "Send",
|
||||
Self::words__wallet => "Wallet",
|
||||
}
|
||||
}
|
||||
|
||||
@ -4325,6 +4340,10 @@ impl TranslatedString {
|
||||
Qstr::MP_QSTR_reset__recovery_share_description => Some(Self::reset__recovery_share_description),
|
||||
Qstr::MP_QSTR_reset__recovery_share_number => Some(Self::reset__recovery_share_number),
|
||||
Qstr::MP_QSTR_words__recovery_share => Some(Self::words__recovery_share),
|
||||
Qstr::MP_QSTR_send__send_in_the_app => Some(Self::send__send_in_the_app),
|
||||
Qstr::MP_QSTR_send__sign_cancelled => Some(Self::send__sign_cancelled),
|
||||
Qstr::MP_QSTR_words__send => Some(Self::words__send),
|
||||
Qstr::MP_QSTR_words__wallet => Some(Self::words__wallet),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
520
core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
Normal file
520
core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
Normal file
@ -0,0 +1,520 @@
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::{
|
||||
error,
|
||||
micropython::obj::Obj,
|
||||
strutil::TString,
|
||||
translations::TR,
|
||||
ui::{
|
||||
button_request::ButtonRequest,
|
||||
component::{
|
||||
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs},
|
||||
ButtonRequestExt, ComponentExt, MsgMap,
|
||||
},
|
||||
flow::{
|
||||
base::{Decision, DecisionBuilder as _},
|
||||
FlowController, FlowMsg, SwipeFlow,
|
||||
},
|
||||
geometry::{Alignment, Direction, LinearPlacement, Offset},
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
component::Button,
|
||||
firmware::{
|
||||
ActionBar, Header, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
|
||||
VerticalMenuScreenMsg,
|
||||
},
|
||||
theme,
|
||||
};
|
||||
|
||||
const MENU_ITEM_CANCEL: usize = 0;
|
||||
const MENU_ITEM_FEE_INFO: usize = 1;
|
||||
const MENU_ITEM_ADDRESS_INFO: usize = 2;
|
||||
const MENU_ITEM_ACCOUNT_INFO: usize = 3;
|
||||
const MENU_ITEM_EXTRA_INFO: usize = 4;
|
||||
|
||||
const TIMEOUT_MS: u32 = 2000;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ConfirmOutput {
|
||||
Address,
|
||||
Menu,
|
||||
AccountInfo,
|
||||
Cancel,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl FlowController for ConfirmOutput {
|
||||
#[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::Address, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
|
||||
(Self::Address, FlowMsg::Info) => Self::Menu.goto(),
|
||||
(Self::Menu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::Cancel.goto(),
|
||||
(Self::Menu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => Self::AccountInfo.goto(),
|
||||
(Self::Menu, FlowMsg::Cancelled) => Self::Address.goto(),
|
||||
(Self::AccountInfo, FlowMsg::Cancelled) => Self::Menu.goto(),
|
||||
(Self::Cancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
|
||||
(Self::Cancel, FlowMsg::Cancelled) => Self::Menu.goto(),
|
||||
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
|
||||
_ => self.do_nothing(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ConfirmOutputWithAmount {
|
||||
Address,
|
||||
AddressMenu,
|
||||
AddressAccountInfo,
|
||||
AddressCancel,
|
||||
Amount,
|
||||
AmountMenu,
|
||||
AmountAccountInfo,
|
||||
AmountCancel,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl FlowController for ConfirmOutputWithAmount {
|
||||
#[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::Address, FlowMsg::Confirmed) => Self::Amount.goto(),
|
||||
(Self::Address, FlowMsg::Info) => Self::AddressMenu.goto(),
|
||||
(Self::AddressMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::AddressCancel.goto(),
|
||||
(Self::AddressMenu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => {
|
||||
Self::AddressAccountInfo.goto()
|
||||
}
|
||||
(Self::AddressAccountInfo, FlowMsg::Cancelled) => Self::AddressMenu.goto(),
|
||||
(Self::AddressMenu, FlowMsg::Cancelled) => Self::Address.goto(),
|
||||
(Self::AddressCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
|
||||
(Self::AddressCancel, FlowMsg::Cancelled) => Self::AddressMenu.goto(),
|
||||
(Self::Amount, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
|
||||
(Self::Amount, FlowMsg::Cancelled) => Self::Address.goto(),
|
||||
(Self::Amount, FlowMsg::Info) => Self::AmountMenu.goto(),
|
||||
(Self::AmountMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::AmountCancel.goto(),
|
||||
(Self::AmountMenu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => {
|
||||
Self::AmountAccountInfo.goto()
|
||||
}
|
||||
(Self::AmountAccountInfo, FlowMsg::Cancelled) => Self::AmountMenu.goto(),
|
||||
(Self::AmountMenu, FlowMsg::Cancelled) => Self::Amount.goto(),
|
||||
(Self::AmountCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
|
||||
(Self::AmountCancel, FlowMsg::Cancelled) => Self::AmountMenu.goto(),
|
||||
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
|
||||
_ => self.do_nothing(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ConfirmOutputWithSummary {
|
||||
Main,
|
||||
MainMenu,
|
||||
MainMenuCancel,
|
||||
MainMenuAddresInfo,
|
||||
MainMenuAccountInfo,
|
||||
Summary,
|
||||
SummaryMenu,
|
||||
SummaryMenuCancel,
|
||||
SummaryMenuFeeInfo,
|
||||
SummaryMenuAccountInfo,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl FlowController for ConfirmOutputWithSummary {
|
||||
#[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::Main, FlowMsg::Confirmed) => Self::Summary.goto(),
|
||||
(Self::Main, FlowMsg::Info) => Self::MainMenu.goto(),
|
||||
(Self::MainMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::MainMenuCancel.goto(),
|
||||
(Self::MainMenu, FlowMsg::Choice(MENU_ITEM_ADDRESS_INFO)) => {
|
||||
Self::MainMenuAddresInfo.goto()
|
||||
}
|
||||
(Self::MainMenu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => {
|
||||
Self::MainMenuAccountInfo.goto()
|
||||
}
|
||||
(Self::MainMenu, FlowMsg::Cancelled) => Self::Main.goto(),
|
||||
(Self::MainMenuAccountInfo | Self::MainMenuAddresInfo, FlowMsg::Cancelled) => {
|
||||
Self::MainMenu.goto()
|
||||
}
|
||||
(Self::MainMenuCancel, FlowMsg::Cancelled) => Self::MainMenu.goto(),
|
||||
(Self::MainMenuCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
|
||||
(Self::Summary, FlowMsg::Info) => Self::SummaryMenu.goto(),
|
||||
(Self::Summary, FlowMsg::Cancelled) => Self::Main.goto(),
|
||||
(Self::Summary, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
|
||||
(Self::SummaryMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => {
|
||||
Self::SummaryMenuCancel.goto()
|
||||
}
|
||||
(Self::SummaryMenu, FlowMsg::Choice(MENU_ITEM_FEE_INFO)) => {
|
||||
Self::SummaryMenuFeeInfo.goto()
|
||||
}
|
||||
(Self::SummaryMenu, FlowMsg::Choice(MENU_ITEM_EXTRA_INFO)) => {
|
||||
Self::SummaryMenuAccountInfo.goto()
|
||||
}
|
||||
(Self::SummaryMenu, FlowMsg::Cancelled) => Self::Summary.goto(),
|
||||
(Self::SummaryMenuCancel, FlowMsg::Cancelled) => Self::SummaryMenu.goto(),
|
||||
(Self::SummaryMenuCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
|
||||
(Self::SummaryMenuAccountInfo | Self::SummaryMenuFeeInfo, FlowMsg::Cancelled) => {
|
||||
Self::SummaryMenu.goto()
|
||||
}
|
||||
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
|
||||
_ => self.do_nothing(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn content_cancel(
|
||||
) -> MsgMap<TextScreen<Paragraphs<Paragraph<'static>>>, impl Fn(TextScreenMsg) -> Option<FlowMsg>> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
fn content_main_menu(
|
||||
address_title: TString<'static>,
|
||||
address_params: bool,
|
||||
account_params: bool,
|
||||
cancel_menu_label: TString<'static>,
|
||||
) -> MsgMap<VerticalMenuScreen, impl Fn(VerticalMenuScreenMsg) -> Option<FlowMsg>> {
|
||||
let mut main_menu = VerticalMenu::empty();
|
||||
let mut main_menu_items = Vec::<usize, 3>::new();
|
||||
if address_params {
|
||||
main_menu = main_menu.item(
|
||||
Button::with_text(address_title)
|
||||
.styled(theme::menu_item_title())
|
||||
.with_text_align(Alignment::Start),
|
||||
);
|
||||
unwrap!(main_menu_items.push(MENU_ITEM_ADDRESS_INFO));
|
||||
}
|
||||
if account_params {
|
||||
main_menu = main_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!(main_menu_items.push(MENU_ITEM_ACCOUNT_INFO));
|
||||
}
|
||||
main_menu = main_menu.item(
|
||||
Button::with_text(cancel_menu_label)
|
||||
.styled(theme::menu_item_title_orange())
|
||||
.with_text_align(Alignment::Start)
|
||||
.with_content_offset(Offset::x(12)),
|
||||
);
|
||||
unwrap!(main_menu_items.push(MENU_ITEM_CANCEL));
|
||||
|
||||
VerticalMenuScreen::new(main_menu)
|
||||
.with_header(Header::new(TString::empty()).with_close_button())
|
||||
.map(move |msg| match msg {
|
||||
VerticalMenuScreenMsg::Selected(i) => {
|
||||
let selected_item = main_menu_items[i];
|
||||
Some(FlowMsg::Choice(selected_item))
|
||||
}
|
||||
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
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_output(
|
||||
title: Option<TString<'static>>,
|
||||
subtitle: Option<TString<'static>>,
|
||||
chunkify: bool,
|
||||
message: Obj,
|
||||
amount: Option<Obj>,
|
||||
br_name: TString<'static>,
|
||||
br_code: u16,
|
||||
account_title: TString<'static>,
|
||||
account_paragraphs: Option<ParagraphVecShort<'static>>,
|
||||
address_title: Option<TString<'static>>,
|
||||
address_paragraphs: Option<ParagraphVecShort<'static>>,
|
||||
summary_title: Option<TString<'static>>,
|
||||
summary_paragraphs: Option<ParagraphVecShort<'static>>,
|
||||
summary_br_code: Option<u16>,
|
||||
summary_br_name: Option<TString<'static>>,
|
||||
fee_params: Option<ParagraphVecShort<'static>>,
|
||||
cancel_menu_label: Option<TString<'static>>,
|
||||
) -> Result<SwipeFlow, error::Error> {
|
||||
let cancel_menu_label = cancel_menu_label.unwrap_or(TR::buttons__cancel.into());
|
||||
let address_menu_item = address_paragraphs.is_some();
|
||||
let account_menu_item = account_paragraphs.is_some();
|
||||
let fee_menu_item = fee_params.is_some();
|
||||
let address_title = address_title.unwrap_or(TR::words__address.into());
|
||||
let account_subtitle = Some(TR::send__send_from.into());
|
||||
|
||||
// Main
|
||||
let main_paragraphs = Paragraph::new(
|
||||
if chunkify {
|
||||
&theme::TEXT_MONO_ADDRESS_CHUNKS
|
||||
} else {
|
||||
&theme::TEXT_MONO_LIGHT
|
||||
},
|
||||
message.try_into().unwrap_or(TString::empty()),
|
||||
);
|
||||
let content_main = TextScreen::new(
|
||||
main_paragraphs
|
||||
.into_paragraphs()
|
||||
.with_placement(LinearPlacement::vertical()),
|
||||
)
|
||||
.with_header(Header::new(title.unwrap_or(TString::empty())).with_menu_button())
|
||||
.with_action_bar(ActionBar::new_single(Button::with_text(
|
||||
TR::buttons__continue.into(),
|
||||
)))
|
||||
.with_subtitle(subtitle.unwrap_or(TString::empty()))
|
||||
.map(|msg| match msg {
|
||||
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
|
||||
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
|
||||
TextScreenMsg::Menu => Some(FlowMsg::Info),
|
||||
})
|
||||
.one_button_request(ButtonRequest::from_num(br_code, br_name))
|
||||
.with_pages(|_| 1);
|
||||
|
||||
// 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 res = if let Some(amount) = amount {
|
||||
let amount_paragraphs = ParagraphVecShort::from_iter([
|
||||
Paragraph::new(&theme::TEXT_SMALL_LIGHT, TR::words__amount).no_break(),
|
||||
Paragraph::new(
|
||||
&theme::TEXT_MONO_MEDIUM_LIGHT,
|
||||
amount.try_into().unwrap_or(TString::empty()),
|
||||
),
|
||||
]);
|
||||
|
||||
let content_amount = TextScreen::new(
|
||||
amount_paragraphs
|
||||
.into_paragraphs()
|
||||
.with_placement(LinearPlacement::vertical()),
|
||||
)
|
||||
.with_header(Header::new(TR::words__amount.into()).with_menu_button())
|
||||
.with_action_bar(ActionBar::new_double(
|
||||
Button::with_icon(theme::ICON_CHEVRON_UP),
|
||||
Button::with_text(TR::buttons__confirm.into()).styled(theme::button_confirm()),
|
||||
))
|
||||
.with_subtitle(subtitle.unwrap_or(TString::empty()))
|
||||
.map(|msg| match msg {
|
||||
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
|
||||
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
|
||||
TextScreenMsg::Menu => Some(FlowMsg::Info),
|
||||
})
|
||||
.one_button_request(ButtonRequest::from_num(br_code, br_name))
|
||||
.with_pages(|_| 1);
|
||||
|
||||
let mut flow = SwipeFlow::new(&ConfirmOutputWithAmount::Address)?;
|
||||
flow.add_page(&ConfirmOutputWithAmount::Address, content_main)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithAmount::AddressMenu,
|
||||
content_main_menu(
|
||||
address_title,
|
||||
address_menu_item,
|
||||
account_menu_item,
|
||||
cancel_menu_label,
|
||||
),
|
||||
)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithAmount::AddressAccountInfo,
|
||||
content_menu_info(
|
||||
TR::address_details__account_info.into(),
|
||||
account_subtitle,
|
||||
account_paragraphs.clone(),
|
||||
),
|
||||
)?
|
||||
.add_page(&ConfirmOutputWithAmount::AddressCancel, content_cancel())?
|
||||
.add_page(&ConfirmOutputWithAmount::Amount, content_amount)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithAmount::AmountMenu,
|
||||
content_main_menu(
|
||||
address_title,
|
||||
address_menu_item,
|
||||
account_menu_item,
|
||||
cancel_menu_label,
|
||||
),
|
||||
)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithAmount::AmountAccountInfo,
|
||||
content_menu_info(account_title, account_subtitle, account_paragraphs.clone()),
|
||||
)?
|
||||
.add_page(&ConfirmOutputWithAmount::AmountCancel, content_cancel())?
|
||||
.add_page(&ConfirmOutputWithAmount::Cancelled, content_cancelled)?;
|
||||
flow
|
||||
} else if let Some(summary_paragraphs) = summary_paragraphs {
|
||||
// Summary
|
||||
let content_summary = TextScreen::new(
|
||||
summary_paragraphs
|
||||
.into_paragraphs()
|
||||
.with_placement(LinearPlacement::vertical())
|
||||
.with_spacing(12),
|
||||
)
|
||||
.with_header(
|
||||
Header::new(summary_title.unwrap_or(TR::words__title_summary.into()))
|
||||
.with_menu_button(),
|
||||
)
|
||||
.with_action_bar(ActionBar::new_double(
|
||||
Button::with_icon(theme::ICON_CHEVRON_UP),
|
||||
Button::with_text(TR::instructions__hold_to_sign.into())
|
||||
.styled(theme::button_confirm()),
|
||||
))
|
||||
.map(|msg| match msg {
|
||||
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
|
||||
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
|
||||
TextScreenMsg::Menu => Some(FlowMsg::Info),
|
||||
})
|
||||
.one_button_request(ButtonRequest::from_num(
|
||||
summary_br_code.unwrap(),
|
||||
summary_br_name.unwrap(),
|
||||
))
|
||||
.with_pages(|_| 1);
|
||||
|
||||
// SummaryMenu
|
||||
let mut summary_menu = VerticalMenu::empty();
|
||||
let mut summary_menu_items = Vec::<usize, 3>::new();
|
||||
if account_menu_item {
|
||||
summary_menu = summary_menu.item(Button::with_text(account_title));
|
||||
unwrap!(summary_menu_items.push(MENU_ITEM_EXTRA_INFO));
|
||||
}
|
||||
if fee_menu_item {
|
||||
summary_menu =
|
||||
summary_menu.item(Button::with_text(TR::confirm_total__title_fee.into()));
|
||||
unwrap!(summary_menu_items.push(MENU_ITEM_FEE_INFO));
|
||||
}
|
||||
summary_menu = summary_menu
|
||||
.item(Button::with_text(cancel_menu_label).styled(theme::menu_item_title_orange()));
|
||||
unwrap!(summary_menu_items.push(MENU_ITEM_CANCEL));
|
||||
let content_summary_menu = VerticalMenuScreen::new(summary_menu)
|
||||
.with_header(Header::new(TString::empty()).with_close_button())
|
||||
.map(move |msg| match msg {
|
||||
VerticalMenuScreenMsg::Selected(i) => {
|
||||
let selected_item = summary_menu_items[i];
|
||||
Some(FlowMsg::Choice(selected_item))
|
||||
}
|
||||
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let mut flow = SwipeFlow::new(&ConfirmOutputWithSummary::Main)?;
|
||||
flow.add_page(&ConfirmOutputWithSummary::Main, content_main)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithSummary::MainMenu,
|
||||
content_main_menu(
|
||||
address_title,
|
||||
address_menu_item,
|
||||
account_menu_item,
|
||||
cancel_menu_label,
|
||||
),
|
||||
)?
|
||||
.add_page(&ConfirmOutputWithSummary::MainMenuCancel, content_cancel())?
|
||||
.add_page(
|
||||
&ConfirmOutputWithSummary::MainMenuAddresInfo,
|
||||
content_menu_info(address_title, None, address_paragraphs),
|
||||
)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithSummary::MainMenuAccountInfo,
|
||||
content_menu_info(account_title, account_subtitle, account_paragraphs.clone()),
|
||||
)?
|
||||
.add_page(&ConfirmOutputWithSummary::Summary, content_summary)?
|
||||
.add_page(&ConfirmOutputWithSummary::SummaryMenu, content_summary_menu)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithSummary::SummaryMenuCancel,
|
||||
content_cancel(),
|
||||
)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithSummary::SummaryMenuFeeInfo,
|
||||
content_menu_info(TR::confirm_total__title_fee.into(), None, fee_params),
|
||||
)?
|
||||
.add_page(
|
||||
&ConfirmOutputWithSummary::SummaryMenuAccountInfo,
|
||||
content_menu_info(account_title, account_subtitle, account_paragraphs),
|
||||
)?
|
||||
.add_page(&ConfirmOutputWithSummary::Cancelled, content_cancelled)?;
|
||||
flow
|
||||
} else {
|
||||
let mut flow = SwipeFlow::new(&ConfirmOutput::Address)?;
|
||||
flow.add_page(&ConfirmOutput::Address, content_main)?
|
||||
.add_page(
|
||||
&ConfirmOutput::Menu,
|
||||
content_main_menu(
|
||||
address_title,
|
||||
address_menu_item,
|
||||
account_menu_item,
|
||||
cancel_menu_label,
|
||||
),
|
||||
)?
|
||||
.add_page(
|
||||
&ConfirmOutput::AccountInfo,
|
||||
content_menu_info(account_title, account_subtitle, account_paragraphs),
|
||||
)?
|
||||
.add_page(&ConfirmOutput::Cancel, content_cancel())?
|
||||
.add_page(&ConfirmOutput::Cancelled, content_cancelled)?;
|
||||
flow
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod confirm_output;
|
||||
pub mod confirm_reset;
|
||||
pub mod confirm_set_new_pin;
|
||||
pub mod confirm_summary;
|
||||
@ -8,6 +9,7 @@ pub mod request_passphrase;
|
||||
pub mod show_danger;
|
||||
pub mod show_share_words;
|
||||
|
||||
pub use confirm_output::new_confirm_output;
|
||||
pub use confirm_reset::new_confirm_reset;
|
||||
pub use confirm_set_new_pin::new_set_new_pin;
|
||||
pub use confirm_summary::new_confirm_summary;
|
||||
|
@ -1,5 +1,4 @@
|
||||
use core::cmp::Ordering;
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
@ -393,29 +392,114 @@ impl FirmwareUI for UIEckhart {
|
||||
}
|
||||
|
||||
fn flow_confirm_output(
|
||||
_title: Option<TString<'static>>,
|
||||
_subtitle: Option<TString<'static>>,
|
||||
title: Option<TString<'static>>,
|
||||
subtitle: Option<TString<'static>>,
|
||||
_description: Option<TString<'static>>,
|
||||
_extra: Option<TString<'static>>,
|
||||
_message: Obj,
|
||||
_amount: Option<Obj>,
|
||||
_chunkify: bool,
|
||||
message: Obj,
|
||||
amount: Option<Obj>,
|
||||
chunkify: bool,
|
||||
_text_mono: bool,
|
||||
_account_title: TString<'static>,
|
||||
_account: Option<TString<'static>>,
|
||||
_account_path: Option<TString<'static>>,
|
||||
_br_code: u16,
|
||||
_br_name: TString<'static>,
|
||||
_address_item: Option<(TString<'static>, Obj)>,
|
||||
account_title: TString<'static>,
|
||||
account: Option<TString<'static>>,
|
||||
account_path: Option<TString<'static>>,
|
||||
br_code: u16,
|
||||
br_name: TString<'static>,
|
||||
address_item: Option<(TString<'static>, Obj)>,
|
||||
_extra_item: Option<(TString<'static>, Obj)>,
|
||||
_summary_items: Option<Obj>,
|
||||
_fee_items: Option<Obj>,
|
||||
_summary_title: Option<TString<'static>>,
|
||||
_summary_br_code: Option<u16>,
|
||||
_summary_br_name: Option<TString<'static>>,
|
||||
_cancel_text: Option<TString<'static>>,
|
||||
summary_items: Option<Obj>,
|
||||
fee_items: Option<Obj>,
|
||||
summary_title: Option<TString<'static>>,
|
||||
summary_br_code: Option<u16>,
|
||||
summary_br_name: Option<TString<'static>>,
|
||||
cancel_text: Option<TString<'static>>,
|
||||
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
|
||||
let (address_title, address_paragraphs) = if let Some(address_item) = address_item {
|
||||
let mut paragraphs = ParagraphVecShort::new();
|
||||
for pair in IterBuf::new().try_iterate(address_item.1)? {
|
||||
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_MEDIUM_LIGHT, value)));
|
||||
}
|
||||
(Some(address_item.0), Some(paragraphs))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// collect available info
|
||||
let account_paragraphs = {
|
||||
let mut paragraphs = ParagraphVecShort::new();
|
||||
if let Some(account) = account {
|
||||
unwrap!(paragraphs.push(
|
||||
Paragraph::new(
|
||||
&theme::TEXT_SMALL_LIGHT,
|
||||
TString::from_translation(TR::words__wallet)
|
||||
)
|
||||
.no_break()
|
||||
));
|
||||
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, account)));
|
||||
}
|
||||
if let Some(path) = account_path {
|
||||
unwrap!(paragraphs.push(
|
||||
Paragraph::new(
|
||||
&theme::TEXT_SMALL_LIGHT,
|
||||
TString::from_translation(TR::address_details__derivation_path)
|
||||
)
|
||||
.no_break()
|
||||
));
|
||||
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, path)));
|
||||
}
|
||||
if paragraphs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(paragraphs)
|
||||
}
|
||||
};
|
||||
|
||||
let summary_paragraphs = if let Some(items) = summary_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_MEDIUM_LIGHT, value)));
|
||||
}
|
||||
Some(paragraphs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let fee_paragraphs = if let Some(items) = fee_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::confirm_output::new_confirm_output(
|
||||
title,
|
||||
subtitle,
|
||||
chunkify,
|
||||
message,
|
||||
amount,
|
||||
br_name,
|
||||
br_code,
|
||||
account_title,
|
||||
account_paragraphs,
|
||||
address_title,
|
||||
address_paragraphs,
|
||||
summary_title,
|
||||
summary_paragraphs,
|
||||
summary_br_code,
|
||||
summary_br_name,
|
||||
fee_paragraphs,
|
||||
cancel_text,
|
||||
)?;
|
||||
Ok(flow)
|
||||
}
|
||||
|
||||
fn flow_confirm_set_new_pin(
|
||||
|
@ -753,6 +753,8 @@ class TR:
|
||||
send__maximum_fee: str = "Maximum fee"
|
||||
send__receiving_to_multisig: str = "Receiving to a multisig address."
|
||||
send__send_from: str = "Send from"
|
||||
send__send_in_the_app: str = "After signing, send the transaction in the app."
|
||||
send__sign_cancelled: str = "Sign cancelled."
|
||||
send__sign_transaction: str = "Sign transaction"
|
||||
send__title_confirm_sending: str = "Confirm sending"
|
||||
send__title_joint_transaction: str = "Joint transaction"
|
||||
@ -974,6 +976,7 @@ class TR:
|
||||
words__receive: str = "Receive"
|
||||
words__recipient: str = "Recipient"
|
||||
words__recovery_share: str = "Recovery share"
|
||||
words__send: str = "Send"
|
||||
words__settings: str = "Settings"
|
||||
words__sign: str = "Sign"
|
||||
words__signer: str = "Signer"
|
||||
@ -989,6 +992,7 @@ class TR:
|
||||
words__title_threshold: str = "Threshold"
|
||||
words__try_again: str = "Try again."
|
||||
words__unknown: str = "Unknown"
|
||||
words__wallet: str = "Wallet"
|
||||
words__warning: str = "Warning"
|
||||
words__writable: str = "Writable"
|
||||
words__yes: str = "Yes"
|
||||
|
@ -392,6 +392,7 @@ async def confirm_output(
|
||||
source_account: str | None = None,
|
||||
source_account_path: str | None = None,
|
||||
cancel_text: str | None = None,
|
||||
description: str | None = None,
|
||||
) -> None:
|
||||
if address_label is not None:
|
||||
title = address_label
|
||||
@ -402,9 +403,32 @@ async def confirm_output(
|
||||
else:
|
||||
title = TR.send__title_sending_to
|
||||
|
||||
# FIXME: not implemented
|
||||
# use `flow_confirm_output` or `TT/TR` style?
|
||||
raise NotImplementedError
|
||||
await raise_if_not_confirmed(
|
||||
trezorui_api.flow_confirm_output(
|
||||
title=TR.words__address,
|
||||
subtitle=title,
|
||||
message=address,
|
||||
extra=None,
|
||||
amount=amount,
|
||||
chunkify=chunkify,
|
||||
text_mono=True,
|
||||
account_title=TR.send__send_from,
|
||||
account=source_account,
|
||||
account_path=source_account_path,
|
||||
address_item=None,
|
||||
extra_item=None,
|
||||
br_code=br_code,
|
||||
br_name="confirm_output",
|
||||
summary_items=None,
|
||||
fee_items=None,
|
||||
summary_title=None,
|
||||
summary_br_name=None,
|
||||
summary_br_code=None,
|
||||
cancel_text=cancel_text,
|
||||
description=description,
|
||||
),
|
||||
br_name=None,
|
||||
)
|
||||
|
||||
|
||||
async def should_show_payment_request_details(
|
||||
|
@ -812,7 +812,13 @@
|
||||
"sd_card__wanna_format": "Do you really want to format the SD card?",
|
||||
"sd_card__wrong_sd_card": "Wrong SD card.",
|
||||
"send__address_path": "address path",
|
||||
"send__cancel_sign": "Cancel sign",
|
||||
"send__cancel_sign": {
|
||||
"Bolt": "Cancel sign",
|
||||
"Caesar": "Cancel sign",
|
||||
"Delizia": "Cancel sign",
|
||||
"Eckhart": "Cancel sign?"
|
||||
},
|
||||
"send__sign_cancelled": "Sign cancelled.",
|
||||
"send__confirm_sending": "Sending amount",
|
||||
"send__from_multiple_accounts": "Sending from multiple accounts.",
|
||||
"send__incl_transaction_fee": "incl. Transaction fee",
|
||||
@ -821,6 +827,7 @@
|
||||
"send__receiving_to_multisig": "Receiving to a multisig address.",
|
||||
"send__send_from": "Send from",
|
||||
"send__sign_transaction": "Sign transaction",
|
||||
"send__send_in_the_app": "After signing, send the transaction in the app.",
|
||||
"send__title_confirm_sending": "Confirm sending",
|
||||
"send__title_joint_transaction": "Joint transaction",
|
||||
"send__title_receiving_to": "Receiving to",
|
||||
@ -1041,6 +1048,7 @@
|
||||
"words__receive": "Receive",
|
||||
"words__recipient": "Recipient",
|
||||
"words__recovery_share": "Recovery share",
|
||||
"words__send": "Send",
|
||||
"words__settings": "Settings",
|
||||
"words__sign": "Sign",
|
||||
"words__signer": "Signer",
|
||||
@ -1061,6 +1069,7 @@
|
||||
"Eckhart": "Try again"
|
||||
},
|
||||
"words__unknown": "Unknown",
|
||||
"words__wallet": "Wallet",
|
||||
"words__warning": "Warning",
|
||||
"words__writable": "Writable",
|
||||
"words__yes": "Yes"
|
||||
|
@ -999,5 +999,9 @@
|
||||
"997": "words__receive",
|
||||
"998": "reset__recovery_share_description",
|
||||
"999": "reset__recovery_share_number",
|
||||
"1000": "words__recovery_share"
|
||||
"1000": "words__recovery_share",
|
||||
"1001": "send__send_in_the_app",
|
||||
"1002": "send__sign_cancelled",
|
||||
"1003": "words__send",
|
||||
"1004": "words__wallet"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"current": {
|
||||
"merkle_root": "ff202420b22b94cfa9975b944d0c852f812142957abff7444d85979d039547fb",
|
||||
"datetime": "2025-03-26T11:49:54.296786",
|
||||
"commit": "0d126a51dd42476e785a551bfde802f46a5809df"
|
||||
"merkle_root": "509a95ff1557b28e4b5d3e884edcaecc80f71cf980c636c42458a5dbf3b4fca5",
|
||||
"datetime": "2025-03-30T13:35:55.278368",
|
||||
"commit": "191a81ae4072b1298839120ee2301e120d1cd660"
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user