feat(core/ui): T3T1 send flow

[no changelog]
mmilata/ui-t3t1-preview
Martin Milata 2 weeks ago
parent fa8d6a4a60
commit b322a9b976

@ -19,7 +19,9 @@ static void _librust_qstrs(void) {
MP_QSTR___dict__;
MP_QSTR___name__;
MP_QSTR_account;
MP_QSTR_account_items;
MP_QSTR_account_label;
MP_QSTR_account_path;
MP_QSTR_accounts;
MP_QSTR_action;
MP_QSTR_active;
@ -204,6 +206,7 @@ static void _librust_qstrs(void) {
MP_QSTR_experimental_mode__title;
MP_QSTR_extra;
MP_QSTR_fee_amount;
MP_QSTR_fee_items;
MP_QSTR_fee_label;
MP_QSTR_fee_rate_amount;
MP_QSTR_fee_title;
@ -211,8 +214,10 @@ static void _librust_qstrs(void) {
MP_QSTR_fingerprint;
MP_QSTR_firmware_update__title;
MP_QSTR_firmware_update__title_fingerprint;
MP_QSTR_flow_confirm_output;
MP_QSTR_flow_confirm_reset_create;
MP_QSTR_flow_confirm_reset_recover;
MP_QSTR_flow_confirm_summary;
MP_QSTR_flow_get_address;
MP_QSTR_flow_prompt_backup;
MP_QSTR_flow_show_share_words;
@ -246,6 +251,7 @@ static void _librust_qstrs(void) {
MP_QSTR_inputs__space;
MP_QSTR_instructions__continue_in_app;
MP_QSTR_instructions__hold_to_confirm;
MP_QSTR_instructions__hold_to_sign;
MP_QSTR_instructions__swipe_up;
MP_QSTR_instructions__tap_to_confirm;
MP_QSTR_is_type_of;
@ -513,11 +519,13 @@ static void _librust_qstrs(void) {
MP_QSTR_select_word;
MP_QSTR_select_word_count;
MP_QSTR_send__address_path;
MP_QSTR_send__cancel_sign;
MP_QSTR_send__confirm_sending;
MP_QSTR_send__from_multiple_accounts;
MP_QSTR_send__including_fee;
MP_QSTR_send__maximum_fee;
MP_QSTR_send__receiving_to_multisig;
MP_QSTR_send__send_from;
MP_QSTR_send__title_confirm_sending;
MP_QSTR_send__title_joint_transaction;
MP_QSTR_send__title_receiving_to;

@ -353,7 +353,7 @@ pub enum TranslatedString {
coinjoin__waiting_for_others = 219, // "Waiting for others"
confirm_total__fee_rate = 220, // "Fee rate:"
confirm_total__sending_from_account = 221, // "Sending from account:"
confirm_total__title_fee = 222, // "FEE INFORMATION"
confirm_total__title_fee = 222, // "Fee info"
confirm_total__title_sending_from = 223, // "SENDING FROM"
debug__loading_seed = 224, // "Loading seed"
debug__loading_seed_not_recommended = 225, // "Loading private seed is not recommended."
@ -1210,7 +1210,7 @@ pub enum TranslatedString {
words__title_share = 823, // "SHARE"
words__title_shares = 824, // "SHARES"
words__title_success = 825, // "SUCCESS"
words__title_summary = 826, // "SUMMARY"
words__title_summary = 826, // "Summary"
words__title_threshold = 827, // "THRESHOLD"
words__unknown = 828, // "Unknown"
words__warning = 829, // "Warning"
@ -1254,6 +1254,9 @@ pub enum TranslatedString {
instructions__continue_in_app = 859, // "Continue in the app"
words__cancel_and_exit = 860, // "Cancel and exit"
address__confirmed = 861, // "Receive address confirmed"
send__cancel_sign = 862, // "Cancel sign"
send__send_from = 863, // "Send from"
instructions__hold_to_sign = 864, // "Hold to sign"
}
impl TranslatedString {
@ -1602,7 +1605,7 @@ impl TranslatedString {
Self::coinjoin__waiting_for_others => "Waiting for others",
Self::confirm_total__fee_rate => "Fee rate:",
Self::confirm_total__sending_from_account => "Sending from account:",
Self::confirm_total__title_fee => "FEE INFORMATION",
Self::confirm_total__title_fee => "Fee info",
Self::confirm_total__title_sending_from => "SENDING FROM",
Self::debug__loading_seed => "Loading seed",
Self::debug__loading_seed_not_recommended => "Loading private seed is not recommended.",
@ -2459,7 +2462,7 @@ impl TranslatedString {
Self::words__title_share => "SHARE",
Self::words__title_shares => "SHARES",
Self::words__title_success => "SUCCESS",
Self::words__title_summary => "SUMMARY",
Self::words__title_summary => "Summary",
Self::words__title_threshold => "THRESHOLD",
Self::words__unknown => "Unknown",
Self::words__warning => "Warning",
@ -2503,6 +2506,9 @@ impl TranslatedString {
Self::instructions__continue_in_app => "Continue in the app",
Self::words__cancel_and_exit => "Cancel and exit",
Self::address__confirmed => "Receive address confirmed",
Self::send__cancel_sign => "Cancel sign",
Self::send__send_from => "Send from",
Self::instructions__hold_to_sign => "Hold to sign",
}
}
@ -3753,6 +3759,9 @@ impl TranslatedString {
Qstr::MP_QSTR_instructions__continue_in_app => Some(Self::instructions__continue_in_app),
Qstr::MP_QSTR_words__cancel_and_exit => Some(Self::words__cancel_and_exit),
Qstr::MP_QSTR_address__confirmed => Some(Self::address__confirmed),
Qstr::MP_QSTR_send__cancel_sign => Some(Self::send__cancel_sign),
Qstr::MP_QSTR_send__send_from => Some(Self::send__send_from),
Qstr::MP_QSTR_instructions__hold_to_sign => Some(Self::instructions__hold_to_sign),
_ => None,
}
}

@ -104,6 +104,7 @@ where
pub fn with_warning_button(self) -> Self {
self.with_button(theme::ICON_WARNING, CancelInfoConfirmMsg::Info, false)
.button_styled(theme::button_danger())
}
pub fn button_styled(mut self, style: ButtonStyleSheet) -> Self {

@ -0,0 +1,158 @@
use crate::{
error,
micropython::qstr::Qstr,
strutil::TString,
translations::TR,
ui::{
component::{ComponentExt, SwipeDirection},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
},
};
use super::super::{
component::{
AddressDetails, Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg,
},
theme,
};
use super::util::ConfirmBlobParams;
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
pub enum ConfirmOutput {
Address,
Amount,
// Tap,
Menu,
AccountInfo,
CancelTap,
}
impl FlowState for ConfirmOutput {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(ConfirmOutput::Address | ConfirmOutput::Amount, SwipeDirection::Left) => {
Decision::Goto(ConfirmOutput::Menu, direction)
}
(ConfirmOutput::Address, SwipeDirection::Up) => {
Decision::Goto(ConfirmOutput::Amount, direction)
}
(ConfirmOutput::Amount, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
(ConfirmOutput::Amount, SwipeDirection::Down) => {
Decision::Goto(ConfirmOutput::Address, direction)
}
(ConfirmOutput::Menu, SwipeDirection::Right) => {
Decision::Goto(ConfirmOutput::Address, direction)
}
(ConfirmOutput::Menu, SwipeDirection::Left) => {
Decision::Goto(ConfirmOutput::AccountInfo, direction)
}
(ConfirmOutput::AccountInfo | ConfirmOutput::CancelTap, SwipeDirection::Right) => {
Decision::Goto(ConfirmOutput::Menu, direction)
}
_ => Decision::Nothing,
}
}
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
match (self, msg) {
(_, FlowMsg::Info) => Decision::Goto(ConfirmOutput::Menu, SwipeDirection::Left),
(ConfirmOutput::Menu, FlowMsg::Choice(0)) => {
Decision::Goto(ConfirmOutput::AccountInfo, SwipeDirection::Left)
}
(ConfirmOutput::Menu, FlowMsg::Choice(1)) => {
Decision::Goto(ConfirmOutput::CancelTap, SwipeDirection::Left)
}
(ConfirmOutput::Menu, FlowMsg::Cancelled) => {
Decision::Goto(ConfirmOutput::Address, SwipeDirection::Right)
}
(ConfirmOutput::CancelTap, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Cancelled),
(_, FlowMsg::Cancelled) => Decision::Goto(ConfirmOutput::Menu, SwipeDirection::Right),
_ => Decision::Nothing,
}
}
}
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ConfirmOutput::new_obj) }
}
impl ConfirmOutput {
const EXTRA_PADDING: i16 = 6;
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let title: Option<TString> = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?;
let account: Option<TString> = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?;
let account_path: Option<TString> =
kwargs.get(Qstr::MP_QSTR_account_path)?.try_into_option()?;
let address: Obj = kwargs.get(Qstr::MP_QSTR_address)?;
let amount: Obj = kwargs.get(Qstr::MP_QSTR_amount)?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
let text_mono: bool = kwargs.get_or(Qstr::MP_QSTR_text_mono, true)?;
// Address
let content_address = ConfirmBlobParams::new(TR::words__address.into(), address, None)
.with_subtitle(title)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_chunkify(chunkify)
.with_text_mono(text_mono)
.into_layout()?;
// .one_button_request(ButtonRequestCode::ConfirmOutput, br_type);
// Amount
let content_amount = ConfirmBlobParams::new(TR::words__amount.into(), amount, None)
.with_subtitle(title)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_text_mono(text_mono)
.into_layout()?;
// .one_button_request(ButtonRequestCode::ConfirmOutput, br_type);
// Menu
let content_menu = Frame::left_aligned(
"".into(),
VerticalMenu::empty()
.item(theme::ICON_CHEVRON_RIGHT, "Account info".into())
.danger(theme::ICON_CANCEL, "Cancel sign".into()),
)
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
// AccountInfo
let ad = AddressDetails::new(TR::send__send_from.into(), account, account_path)?;
let content_account = SwipePage::horizontal(ad).map(|_| Some(FlowMsg::Cancelled));
// CancelTap
let content_cancel_tap = Frame::left_aligned(
TR::send__cancel_sign.into(),
PromptScreen::new_tap_to_cancel(),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let store = flow_store()
.add(content_address)?
.add(content_amount)?
.add(content_menu)?
.add(content_account)?
.add(content_cancel_tap)?;
let res = SwipeFlow::new(ConfirmOutput::Address, store)?;
Ok(LayoutObj::new(res)?.into())
}
}

@ -0,0 +1,169 @@
use crate::{
error,
micropython::{iter::IterBuf, qstr::Qstr},
strutil::TString,
translations::TR,
ui::{
component::{ComponentExt, SwipeDirection},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow},
},
};
use super::super::{
component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg},
theme,
};
use super::util::ShowInfoParams;
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
pub enum ConfirmSummary {
Summary,
Hold,
Menu,
FeeInfo,
AccountInfo,
CancelTap,
}
impl FlowState for ConfirmSummary {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(ConfirmSummary::Summary | ConfirmSummary::Hold, SwipeDirection::Left) => {
Decision::Goto(ConfirmSummary::Menu, direction)
}
(ConfirmSummary::Summary, SwipeDirection::Up) => {
Decision::Goto(ConfirmSummary::Hold, direction)
}
(ConfirmSummary::Hold, SwipeDirection::Down) => {
Decision::Goto(ConfirmSummary::Summary, direction)
}
(ConfirmSummary::Menu, SwipeDirection::Right) => {
Decision::Goto(ConfirmSummary::Summary, direction)
}
(ConfirmSummary::Menu, SwipeDirection::Left) => {
Decision::Goto(ConfirmSummary::FeeInfo, direction)
}
(
ConfirmSummary::AccountInfo | ConfirmSummary::FeeInfo | ConfirmSummary::CancelTap,
SwipeDirection::Right,
) => Decision::Goto(ConfirmSummary::Menu, direction),
_ => Decision::Nothing,
}
}
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
match (self, msg) {
(_, FlowMsg::Info) => Decision::Goto(ConfirmSummary::Menu, SwipeDirection::Left),
(ConfirmSummary::Hold, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Confirmed),
(ConfirmSummary::Menu, FlowMsg::Choice(0)) => {
Decision::Goto(ConfirmSummary::FeeInfo, SwipeDirection::Left)
}
(ConfirmSummary::Menu, FlowMsg::Choice(1)) => {
Decision::Goto(ConfirmSummary::AccountInfo, SwipeDirection::Left)
}
(ConfirmSummary::Menu, FlowMsg::Choice(2)) => {
Decision::Goto(ConfirmSummary::CancelTap, SwipeDirection::Left)
}
(ConfirmSummary::Menu, FlowMsg::Cancelled) => {
Decision::Goto(ConfirmSummary::Summary, SwipeDirection::Right)
}
(ConfirmSummary::CancelTap, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Cancelled),
(_, FlowMsg::Cancelled) => Decision::Goto(ConfirmSummary::Menu, SwipeDirection::Right),
_ => Decision::Nothing,
}
}
}
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_confirm_summary(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ConfirmSummary::new_obj) }
}
impl ConfirmSummary {
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let account_items: Obj = kwargs.get(Qstr::MP_QSTR_account_items)?;
let fee_items: Obj = kwargs.get(Qstr::MP_QSTR_fee_items)?;
// Summary
let mut summary = ShowInfoParams::new(title)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None);
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
summary = unwrap!(summary.add(label, value));
}
let content_summary = summary.into_layout()?;
// Hold to confirm
let content_hold = Frame::left_aligned(
TR::eos__sign_transaction.into(),
PromptScreen::new_hold_to_confirm(),
)
.with_menu_button()
.with_footer(TR::instructions__hold_to_sign.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Info),
});
// Menu
let content_menu = Frame::left_aligned(
"".into(),
VerticalMenu::empty()
.item(theme::ICON_CHEVRON_RIGHT, "Fee info".into())
.item(theme::ICON_CHEVRON_RIGHT, "Account info".into())
.danger(theme::ICON_CANCEL, "Cancel sign".into()),
)
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
// FeeInfo
let mut fee = ShowInfoParams::new(TR::confirm_total__title_fee.into()).with_cancel_button();
for pair in IterBuf::new().try_iterate(fee_items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
fee = unwrap!(fee.add(label, value));
}
let content_fee = fee.into_layout()?;
// AccountInfo
let mut account = ShowInfoParams::new(TR::send__send_from.into()).with_cancel_button();
for pair in IterBuf::new().try_iterate(account_items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
account = unwrap!(account.add(label, value));
}
let content_account = account.into_layout()?;
// CancelTap
let content_cancel_tap = Frame::left_aligned(
TR::send__cancel_sign.into(),
PromptScreen::new_tap_to_cancel(),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let store = flow_store()
.add(content_summary)?
.add(content_hold)?
.add(content_menu)?
.add(content_fee)?
.add(content_account)?
.add(content_cancel_tap)?;
let res = SwipeFlow::new(ConfirmSummary::Summary, store)?;
Ok(LayoutObj::new(res)?.into())
}
}

@ -1,12 +1,18 @@
pub mod confirm_output;
pub mod confirm_reset_create;
pub mod confirm_reset_recover;
pub mod confirm_summary;
pub mod get_address;
pub mod prompt_backup;
pub mod show_share_words;
pub mod warning_hi_prio;
mod util;
pub use confirm_output::new_confirm_output;
pub use confirm_reset_create::ConfirmResetCreate;
pub use confirm_reset_recover::ConfirmResetRecover;
pub use confirm_summary::new_confirm_summary;
pub use get_address::GetAddress;
pub use prompt_backup::PromptBackup;
pub use show_share_words::ShowShareWords;

@ -0,0 +1,232 @@
use super::super::{
component::{Frame, FrameMsg},
theme,
};
use crate::{
error::Error,
micropython::obj::Obj,
strutil::TString,
trace::Trace,
ui::{
component::{
base::ComponentExt,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecLong, VecExt},
Component,
},
flow::{FlowMsg, Swipable, SwipePage},
layout::util::ConfirmBlob,
},
};
use heapless::Vec;
pub struct ConfirmBlobParams {
title: TString<'static>,
subtitle: Option<TString<'static>>,
footer_instruction: Option<TString<'static>>,
footer_description: Option<TString<'static>>,
data: Obj,
description: Option<TString<'static>>,
extra: Option<TString<'static>>,
menu_button: bool,
chunkify: bool,
text_mono: bool,
}
impl ConfirmBlobParams {
pub const fn new(
title: TString<'static>,
data: Obj,
description: Option<TString<'static>>,
) -> Self {
Self {
title,
subtitle: None,
footer_instruction: None,
footer_description: None,
data,
description,
extra: None,
menu_button: false,
chunkify: false,
text_mono: true,
}
}
pub const fn with_extra(mut self, extra: Option<TString<'static>>) -> Self {
self.extra = extra;
self
}
pub const fn with_subtitle(mut self, subtitle: Option<TString<'static>>) -> Self {
self.subtitle = subtitle;
self
}
pub const fn with_menu_button(mut self) -> Self {
self.menu_button = true;
self
}
pub const fn with_footer(
mut self,
instruction: TString<'static>,
description: Option<TString<'static>>,
) -> Self {
self.footer_instruction = Some(instruction);
self.footer_description = description;
self
}
pub const fn with_chunkify(mut self, chunkify: bool) -> Self {
self.chunkify = chunkify;
self
}
pub const fn with_text_mono(mut self, text_mono: bool) -> Self {
self.text_mono = text_mono;
self
}
pub fn into_layout(self) -> Result<impl Component<Msg = FlowMsg> + Swipable + Trace, Error> {
let paragraphs = ConfirmBlob {
description: self.description.unwrap_or("".into()),
extra: self.extra.unwrap_or("".into()),
data: self.data.try_into()?,
description_font: &theme::TEXT_NORMAL,
extra_font: &theme::TEXT_DEMIBOLD,
data_font: if self.chunkify {
let data: TString = self.data.try_into()?;
theme::get_chunkified_text_style(data.len())
} else if self.text_mono {
&theme::TEXT_MONO
} else {
&theme::TEXT_NORMAL
},
}
.into_paragraphs();
let page = SwipePage::vertical(paragraphs);
let mut frame = Frame::left_aligned(self.title, page);
if let Some(subtitle) = self.subtitle {
frame = frame.with_subtitle(subtitle);
}
if self.menu_button {
frame = frame.with_menu_button();
}
if let Some(instruction) = self.footer_instruction {
frame = frame.with_footer(instruction, self.footer_description);
}
Ok(frame.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)))
}
}
pub struct ShowInfoParams {
title: TString<'static>,
subtitle: Option<TString<'static>>,
menu_button: bool,
cancel_button: bool,
footer_instruction: Option<TString<'static>>,
footer_description: Option<TString<'static>>,
chunkify: bool,
items: Vec<(TString<'static>, TString<'static>), 4>,
}
impl ShowInfoParams {
pub const fn new(title: TString<'static>) -> Self {
Self {
title,
subtitle: None,
menu_button: false,
cancel_button: false,
footer_instruction: None,
footer_description: None,
chunkify: false,
items: Vec::new(),
}
}
pub fn add(mut self, key: TString<'static>, value: TString<'static>) -> Option<Self> {
if self.items.push((key, value)).is_ok() {
Some(self)
} else {
None
}
}
pub const fn with_subtitle(mut self, subtitle: Option<TString<'static>>) -> Self {
self.subtitle = subtitle;
self
}
pub const fn with_menu_button(mut self) -> Self {
self.menu_button = true;
self
}
pub const fn with_cancel_button(mut self) -> Self {
self.cancel_button = true;
self
}
pub const fn with_footer(
mut self,
instruction: TString<'static>,
description: Option<TString<'static>>,
) -> Self {
self.footer_instruction = Some(instruction);
self.footer_description = description;
self
}
pub const fn with_chunkify(mut self, chunkify: bool) -> Self {
self.chunkify = chunkify;
self
}
pub fn into_layout(self) -> Result<impl Component<Msg = FlowMsg> + Swipable + Trace, Error> {
let mut paragraphs = ParagraphVecLong::new();
let mut first: bool = true;
for item in self.items {
// FIXME: padding:
if !first {
paragraphs.add(Paragraph::new::<TString<'static>>(
&theme::TEXT_SUB_GREY,
" ".into(),
));
}
first = false;
paragraphs.add(Paragraph::new(&theme::TEXT_SUB_GREY, item.0).no_break());
if self.chunkify {
paragraphs.add(Paragraph::new(
theme::get_chunkified_text_style(item.1.len()),
item.1,
));
} else {
paragraphs.add(Paragraph::new(&theme::TEXT_MONO_GREY_LIGHT, item.1));
}
}
let mut frame = Frame::left_aligned(
self.title,
SwipePage::vertical(paragraphs.into_paragraphs()),
);
if let Some(subtitle) = self.subtitle {
frame = frame.with_subtitle(subtitle);
}
if self.cancel_button {
frame = frame.with_cancel_button();
} else if self.menu_button {
frame = frame.with_menu_button();
}
if let Some(instruction) = self.footer_instruction {
frame = frame.with_footer(instruction, self.footer_description);
}
Ok(frame.map(move |msg| {
matches!(msg, FrameMsg::Button(_)).then_some(if self.cancel_button {
FlowMsg::Cancelled
} else {
FlowMsg::Info
})
}))
}
}

@ -930,6 +930,7 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get_or(Qstr::MP_QSTR_description, "".into())?;
let value: TString = kwargs.get_or(Qstr::MP_QSTR_value, "".into())?;
let action: Option<TString> = kwargs.get(Qstr::MP_QSTR_button)?.try_into_option()?;
let content = SwipeUpScreen::new(Paragraphs::new([
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description),
@ -938,8 +939,7 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
let obj = LayoutObj::new(
Frame::left_aligned(title, content)
.with_warning_button()
.button_styled(theme::button_warning_low())
.with_footer(TR::instructions__swipe_up.into(), None),
.with_footer(TR::instructions__swipe_up.into(), action),
)?;
Ok(obj.into())
};
@ -2019,6 +2019,28 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// ) -> LayoutObj[UiResult]:
/// """Warning modal with multiple steps to confirm."""
Qstr::MP_QSTR_flow_warning_hi_prio => obj_fn_kw!(0, flow::warning_hi_prio::new_warning_hi_prio).as_obj(),
/// def flow_confirm_output(
/// *,
/// title: str | None,
/// address: str,
/// amount: str,
/// chunkify: bool,
/// account: str | None,
/// account_path: str | None,
/// ) -> LayoutObj[UiResult]:
/// """Confirm recipient."""
Qstr::MP_QSTR_flow_confirm_output => obj_fn_kw!(0, flow::new_confirm_output).as_obj(),
/// def flow_confirm_summary(
/// *,
/// title: str,
/// items: Iterable[tuple[str, str]],
/// account_items: Iterable[tuple[str, str]],
/// fee_items: Iterable[tuple[str, str]],
/// ) -> LayoutObj[UiResult]:
/// """Total summary and hold to confirm."""
Qstr::MP_QSTR_flow_confirm_summary => obj_fn_kw!(0, flow::new_confirm_summary).as_obj(),
};
#[cfg(test)]

@ -538,6 +538,30 @@ def flow_warning_hi_prio(
value: str = "",
) -> LayoutObj[UiResult]:
"""Warning modal with multiple steps to confirm."""
# rust/src/ui/model_mercury/layout.rs
def flow_confirm_output(
*,
title: str | None,
address: str,
amount: str,
chunkify: bool,
account: str | None,
account_path: str | None,
) -> LayoutObj[UiResult]:
"""Confirm recipient."""
# rust/src/ui/model_mercury/layout.rs
def flow_confirm_summary(
*,
title: str,
items: Iterable[tuple[str, str]],
account_items: Iterable[tuple[str, str]],
fee_items: Iterable[tuple[str, str]],
) -> LayoutObj[UiResult]:
"""Total summary and hold to confirm."""
CONFIRMED: UiResult
CANCELLED: UiResult
INFO: UiResult

@ -231,7 +231,7 @@ class TR:
coinjoin__waiting_for_others: str = "Waiting for others"
confirm_total__fee_rate: str = "Fee rate:"
confirm_total__sending_from_account: str = "Sending from account:"
confirm_total__title_fee: str = "FEE INFORMATION"
confirm_total__title_fee: str = "Fee info"
confirm_total__title_sending_from: str = "SENDING FROM"
debug__loading_seed: str = "Loading seed"
debug__loading_seed_not_recommended: str = "Loading private seed is not recommended."
@ -358,6 +358,7 @@ class TR:
inputs__space: str = "SPACE"
instructions__continue_in_app: str = "Continue in the app"
instructions__hold_to_confirm: str = "Hold to confirm"
instructions__hold_to_sign: str = "Hold to sign"
instructions__swipe_up: str = "Swipe up"
instructions__tap_to_confirm: str = "Tap to confirm"
joint__title: str = "JOINT TRANSACTION"
@ -665,11 +666,13 @@ class TR:
sd_card__wanna_format: str = "Do you really want to format the SD card?"
sd_card__wrong_sd_card: str = "Wrong SD card."
send__address_path: str = "address path"
send__cancel_sign: str = "Cancel sign"
send__confirm_sending: str = "SENDING AMOUNT"
send__from_multiple_accounts: str = "Sending from multiple accounts."
send__including_fee: str = "Including fee:"
send__maximum_fee: str = "Maximum fee:"
send__receiving_to_multisig: str = "Receiving to a multisig address."
send__send_from: str = "Send from"
send__title_confirm_sending: str = "CONFIRM SENDING"
send__title_joint_transaction: str = "JOINT TRANSACTION"
send__title_receiving_to: str = "RECEIVING TO"
@ -858,7 +861,7 @@ class TR:
words__title_share: str = "SHARE"
words__title_shares: str = "SHARES"
words__title_success: str = "SUCCESS"
words__title_summary: str = "SUMMARY"
words__title_summary: str = "Summary"
words__title_threshold: str = "THRESHOLD"
words__unknown: str = "Unknown"
words__warning: str = "Warning"

@ -506,9 +506,9 @@ async def show_warning(
interact(
RustLayout(
trezorui2.show_warning(
title=content,
description=subheader or "",
button=button.upper(),
title="Important",
value=content,
button=subheader or TR.words__continue_anyway,
)
),
br_type,
@ -547,57 +547,32 @@ async def confirm_output(
output_index: int | None = None,
chunkify: bool = False,
) -> None:
if title is not None:
# TODO: handle translation
if title.upper().startswith("CONFIRM "):
title = title[len("CONFIRM ") :]
amount_title = title
recipient_title = title
if address_label is not None:
title = address_label
elif title is not None:
pass
elif output_index is not None:
amount_title = f"{TR.words__amount} #{output_index + 1}"
recipient_title = f"{TR.words__recipient} #{output_index + 1}"
title = f"{TR.words__recipient} #{output_index + 1}"
else:
amount_title = TR.send__confirm_sending
recipient_title = TR.send__title_sending_to
title = TR.send__title_sending_to
while True:
result = await interact(
# TODO: this should send 2x ButtonRequest
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.confirm_value(
title=recipient_title.upper(),
subtitle=address_label,
description=None,
value=address,
verb=TR.buttons__continue,
hold=False,
info_button=False,
trezorui2.flow_confirm_output(
address=address,
amount=amount,
title=title,
chunkify=chunkify,
account=None,
account_path=None, # TODO: sending from
)
),
"confirm_output",
br_code,
)
if result is not CONFIRMED:
raise ActionCancelled
result = await interact(
RustLayout(
trezorui2.confirm_value(
title=amount_title.upper(),
subtitle=None,
description=None,
value=amount,
verb=None if hold else TR.buttons__confirm,
verb_cancel="^",
hold=hold,
info_button=False,
)
),
"confirm_output",
br_code,
)
if result is CONFIRMED:
return
)
async def should_show_payment_request_details(
@ -889,6 +864,7 @@ async def confirm_total(
fee_label: str | None = None,
account_label: str | None = None,
fee_rate_amount: str | None = None,
path: str | None = None,
br_type: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
@ -900,18 +876,28 @@ async def confirm_total(
(total_label, total_amount),
(fee_label, fee_amount),
]
info_items = []
fee_items = []
account_items = []
if account_label:
info_items.append((TR.confirm_total__sending_from_account, account_label))
account_items.append((TR.confirm_total__sending_from_account, account_label))
if path:
account_items.append((TR.address_details__derivation_path, path))
if fee_rate_amount:
info_items.append((TR.confirm_total__fee_rate, fee_rate_amount))
fee_items.append((TR.confirm_total__fee_rate, fee_rate_amount))
await confirm_summary(
items,
TR.words__title_summary,
info_items=info_items,
br_type=br_type,
br_code=br_code,
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.flow_confirm_summary(
title=title,
items=items,
fee_items=fee_items,
account_items=account_items,
)
),
br_type,
br_code,
)
)

@ -233,7 +233,7 @@
"coinjoin__waiting_for_others": "Waiting for others",
"confirm_total__fee_rate": "Fee rate:",
"confirm_total__sending_from_account": "Sending from account:",
"confirm_total__title_fee": "FEE INFORMATION",
"confirm_total__title_fee": "Fee info",
"confirm_total__title_sending_from": "SENDING FROM",
"debug__loading_seed": "Loading seed",
"debug__loading_seed_not_recommended": "Loading private seed is not recommended.",
@ -362,6 +362,7 @@
"instructions__swipe_up": "Swipe up",
"instructions__tap_to_confirm": "Tap to confirm",
"instructions__hold_to_confirm": "Hold to confirm",
"instructions__hold_to_sign": "Hold to sign",
"joint__title": "JOINT TRANSACTION",
"joint__to_the_total_amount": "To the total amount:",
"joint__you_are_contributing": "You are contributing:",
@ -667,11 +668,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__confirm_sending": "SENDING AMOUNT",
"send__from_multiple_accounts": "Sending from multiple accounts.",
"send__including_fee": "Including fee:",
"send__maximum_fee": "Maximum fee:",
"send__receiving_to_multisig": "Receiving to a multisig address.",
"send__send_from": "Send from",
"send__title_confirm_sending": "CONFIRM SENDING",
"send__title_joint_transaction": "JOINT TRANSACTION",
"send__title_receiving_to": "RECEIVING TO",
@ -860,7 +863,7 @@
"words__title_share": "SHARE",
"words__title_shares": "SHARES",
"words__title_success": "SUCCESS",
"words__title_summary": "SUMMARY",
"words__title_summary": "Summary",
"words__title_threshold": "THRESHOLD",
"words__unknown": "Unknown",
"words__warning": "Warning",

@ -860,5 +860,8 @@
"858": "address_details__derivation_path",
"859": "instructions__continue_in_app",
"860": "words__cancel_and_exit",
"861": "address__confirmed"
"861": "address__confirmed",
"862": "send__cancel_sign",
"863": "send__send_from",
"864": "instructions__hold_to_sign"
}

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "87891f7bef0164d655882be5c86631c413e7d9dce3492725080930c69c82a122",
"datetime": "2024-05-01T21:20:31.907021",
"commit": "4b3e1a08258dbf09fa153db1bf2b8a419b1d84e6"
"merkle_root": "522ec2af48343bcf4528469e821dfc3852d14dae290239556222cbcc4356dc98",
"datetime": "2024-05-11T20:19:06.318093",
"commit": "fa8d6a4a609ae6acd972c596678cad07b99b07d8"
},
"history": [
{

Loading…
Cancel
Save