mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-27 08:38:07 +00:00
feat(core/ui): redesigned receive flow
[no changelog]
This commit is contained in:
parent
1b94a7cb7b
commit
4af5939a0b
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
core/assets/info-circle.png
Normal file
BIN
core/assets/info-circle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 266 B |
BIN
core/assets/octa-bang.png
Normal file
BIN
core/assets/octa-bang.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 468 B |
@ -21,6 +21,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_jpeg_test;
|
||||
MP_QSTR_confirm_action;
|
||||
MP_QSTR_confirm_homescreen;
|
||||
MP_QSTR_confirm_address;
|
||||
MP_QSTR_confirm_blob;
|
||||
MP_QSTR_confirm_properties;
|
||||
MP_QSTR_confirm_coinjoin;
|
||||
@ -40,6 +41,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_show_success;
|
||||
MP_QSTR_show_warning;
|
||||
MP_QSTR_show_info;
|
||||
MP_QSTR_show_mismatch;
|
||||
MP_QSTR_show_simple;
|
||||
MP_QSTR_request_number;
|
||||
MP_QSTR_request_pin;
|
||||
@ -56,6 +58,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_show_remaining_shares;
|
||||
MP_QSTR_show_share_words;
|
||||
MP_QSTR_show_progress;
|
||||
MP_QSTR_show_address_details;
|
||||
|
||||
MP_QSTR_attach_timer_fn;
|
||||
MP_QSTR_touch_event;
|
||||
@ -107,6 +110,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_time_ms;
|
||||
MP_QSTR_app_name;
|
||||
MP_QSTR_icon_name;
|
||||
MP_QSTR_account;
|
||||
MP_QSTR_accounts;
|
||||
MP_QSTR_indeterminate;
|
||||
MP_QSTR_notification;
|
||||
@ -114,4 +118,5 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_bootscreen;
|
||||
MP_QSTR_skip_first_paint;
|
||||
MP_QSTR_wrong_pin;
|
||||
MP_QSTR_xpubs;
|
||||
}
|
||||
|
@ -87,6 +87,12 @@ impl BlendedImage {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: currently this function is used too rarely to justify writing special
|
||||
// case for unblended image.
|
||||
pub fn single(icon: Icon, color: Color, area_color: Color) -> Self {
|
||||
Self::new(icon, icon, color, color, area_color)
|
||||
}
|
||||
|
||||
fn paint_image(&self) {
|
||||
display::icon_over_icon(
|
||||
None,
|
||||
|
@ -24,9 +24,9 @@ pub use map::Map;
|
||||
pub use marquee::Marquee;
|
||||
pub use maybe::Maybe;
|
||||
pub use pad::Pad;
|
||||
pub use paginated::{PageMsg, Paginate};
|
||||
pub use paginated::{AuxPageMsg, PageMsg, Paginate};
|
||||
pub use painter::Painter;
|
||||
pub use placed::{FixedHeightBar, GridPlaced};
|
||||
pub use placed::{FixedHeightBar, Floating, GridPlaced, VSplit};
|
||||
pub use qr_code::Qr;
|
||||
pub use text::{
|
||||
formatted::FormattedText,
|
||||
|
@ -3,6 +3,15 @@ use crate::ui::component::{
|
||||
FormattedText,
|
||||
};
|
||||
|
||||
pub enum AuxPageMsg {
|
||||
/// Page component was instantiated with BACK button on every page and it
|
||||
/// was pressed.
|
||||
GoBack,
|
||||
|
||||
/// Page component was configured to react to swipes and user swiped left.
|
||||
SwipeLeft,
|
||||
}
|
||||
|
||||
/// Common message type for pagination components.
|
||||
pub enum PageMsg<T, U> {
|
||||
/// Pass-through from paged component.
|
||||
@ -12,9 +21,8 @@ pub enum PageMsg<T, U> {
|
||||
/// "OK" and "Cancel" buttons.
|
||||
Controls(U),
|
||||
|
||||
/// Page component was instantiated with BACK button on every page and it
|
||||
/// was pressed.
|
||||
GoBack,
|
||||
/// Auxilliary events used by exotic pages on touchscreens.
|
||||
Aux(AuxPageMsg),
|
||||
}
|
||||
|
||||
pub trait Paginate {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Grid, GridCellSpan, Rect},
|
||||
geometry::{Alignment, Alignment2D, Grid, GridCellSpan, Insets, Offset, Rect, TOP_RIGHT},
|
||||
};
|
||||
|
||||
pub struct GridPlaced<T> {
|
||||
@ -121,3 +121,135 @@ where
|
||||
d.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Floating<T> {
|
||||
inner: T,
|
||||
size: Offset,
|
||||
border: Offset,
|
||||
align: Alignment2D,
|
||||
}
|
||||
|
||||
impl<T> Floating<T> {
|
||||
pub const fn new(size: Offset, border: Offset, align: Alignment2D, inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
size,
|
||||
border,
|
||||
align,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn top_right(side: i16, border: i16, inner: T) -> Self {
|
||||
let size = Offset::uniform(side);
|
||||
let border = Offset::uniform(border);
|
||||
Self::new(size, border, TOP_RIGHT, inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Floating<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let mut border = self.border;
|
||||
let area = match self.align.0 {
|
||||
Alignment::Start => bounds.split_left(self.size.x).0,
|
||||
Alignment::Center => panic!("alignment not supported"),
|
||||
Alignment::End => {
|
||||
border.x = -border.x;
|
||||
bounds.split_right(self.size.x).1
|
||||
}
|
||||
};
|
||||
let area = match self.align.1 {
|
||||
Alignment::Start => area.split_top(self.size.y).0,
|
||||
Alignment::Center => panic!("alignment not supported"),
|
||||
Alignment::End => {
|
||||
border.y = -border.y;
|
||||
area.split_bottom(self.size.y).1
|
||||
}
|
||||
};
|
||||
self.inner.place(area.translate(border))
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.inner.event(ctx, event)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.inner.paint()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Floating<T>
|
||||
where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Floating");
|
||||
d.field("inner", &self.inner);
|
||||
d.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VSplit<T, U> {
|
||||
first: T,
|
||||
second: U,
|
||||
width: i16,
|
||||
spacing: i16,
|
||||
}
|
||||
|
||||
impl<T, U> VSplit<T, U> {
|
||||
pub const fn new(width: i16, spacing: i16, first: T, second: U) -> Self {
|
||||
Self {
|
||||
first,
|
||||
second,
|
||||
width,
|
||||
spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, T, U> Component for VSplit<T, U>
|
||||
where
|
||||
T: Component<Msg = M>,
|
||||
U: Component<Msg = M>,
|
||||
{
|
||||
type Msg = M;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (left, right) = bounds.split_left(self.width);
|
||||
let right = right.inset(Insets::left(self.spacing));
|
||||
self.first.place(left);
|
||||
self.second.place(right);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.first
|
||||
.event(ctx, event)
|
||||
.or_else(|| self.second.event(ctx, event))
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.first.paint();
|
||||
self.second.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for VSplit<T, U>
|
||||
where
|
||||
T: Component + crate::trace::Trace,
|
||||
U: Component + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("VSplit");
|
||||
d.field("first", &self.first);
|
||||
d.field("second", &self.second);
|
||||
d.close();
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ where
|
||||
PageMsg::Content(_) => Err(Error::TypeError),
|
||||
PageMsg::Controls(true) => Ok(CONFIRMED.as_obj()),
|
||||
PageMsg::Controls(false) => Ok(CANCELLED.as_obj()),
|
||||
PageMsg::GoBack => unreachable!(),
|
||||
PageMsg::Aux(_) => Err(Error::TypeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
158
core/embed/rust/src/ui/model_tt/component/address_details.rs
Normal file
158
core/embed/rust/src/ui/model_tt/component/address_details.rs
Normal file
@ -0,0 +1,158 @@
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
ui::{
|
||||
component::{
|
||||
text::paragraphs::{
|
||||
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
|
||||
},
|
||||
Component, Event, EventCtx, Never, Paginate, Qr,
|
||||
},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{theme, Frame};
|
||||
|
||||
pub struct AddressDetails<T> {
|
||||
qr_code: Frame<Qr, T>,
|
||||
details: Frame<Paragraphs<ParagraphVecShort<T>>, T>,
|
||||
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
|
||||
xpubs: Vec<(T, T), 16>,
|
||||
current_page: usize,
|
||||
}
|
||||
|
||||
impl<T> AddressDetails<T>
|
||||
where
|
||||
T: ParagraphStrType + From<&'static str>,
|
||||
{
|
||||
pub fn new(
|
||||
qr_address: T,
|
||||
case_sensitive: bool,
|
||||
account: Option<T>,
|
||||
path: Option<T>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut para = ParagraphVecShort::new();
|
||||
if let Some(a) = account {
|
||||
para.add(Paragraph::new(&theme::TEXT_NORMAL, "Account:".into()));
|
||||
para.add(Paragraph::new(&theme::TEXT_MONO, a));
|
||||
}
|
||||
if let Some(p) = path {
|
||||
para.add(Paragraph::new(
|
||||
&theme::TEXT_NORMAL,
|
||||
"Derivation path:".into(),
|
||||
));
|
||||
para.add(Paragraph::new(&theme::TEXT_MONO, p));
|
||||
}
|
||||
let result = Self {
|
||||
qr_code: Frame::left_aligned(
|
||||
theme::label_title(),
|
||||
"RECEIVE ADDRESS".into(),
|
||||
Qr::new(qr_address, case_sensitive)?.with_border(7),
|
||||
)
|
||||
.with_border(theme::borders_horizontal_scroll()),
|
||||
details: Frame::left_aligned(
|
||||
theme::label_title(),
|
||||
"RECEIVING TO".into(),
|
||||
para.into_paragraphs(),
|
||||
)
|
||||
.with_border(theme::borders_horizontal_scroll()),
|
||||
xpub_view: Frame::left_aligned(
|
||||
theme::label_title(),
|
||||
" \n ".into(),
|
||||
Paragraph::new(&theme::TEXT_XPUB, "".into()).into_paragraphs(),
|
||||
)
|
||||
.with_border(theme::borders_horizontal_scroll()),
|
||||
xpubs: Vec::new(),
|
||||
current_page: 0,
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> {
|
||||
self.xpubs
|
||||
.push((title, xpub))
|
||||
.map_err(|_| Error::OutOfRange)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Paginate for AddressDetails<T>
|
||||
where
|
||||
T: ParagraphStrType + Clone,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
2 + self.xpubs.len()
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
self.current_page = to_page;
|
||||
if to_page > 1 {
|
||||
let i = to_page - 2;
|
||||
// Context is needed for updating child so that it can request repaint. In this
|
||||
// case the parent component that handles paging always requests complete
|
||||
// repaint after page change so we can use a dummy context here.
|
||||
let mut dummy_ctx = EventCtx::new();
|
||||
self.xpub_view
|
||||
.update_title(&mut dummy_ctx, self.xpubs[i].0.clone());
|
||||
self.xpub_view.update_content(&mut dummy_ctx, |p| {
|
||||
p.inner_mut().update(self.xpubs[i].1.clone());
|
||||
p.change_page(0)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for AddressDetails<T>
|
||||
where
|
||||
T: ParagraphStrType,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.qr_code.place(bounds);
|
||||
self.details.place(bounds);
|
||||
self.xpub_view.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match self.current_page {
|
||||
0 => self.qr_code.event(ctx, event),
|
||||
1 => self.details.event(ctx, event),
|
||||
_ => self.xpub_view.event(ctx, event),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
match self.current_page {
|
||||
0 => self.qr_code.paint(),
|
||||
1 => self.details.paint(),
|
||||
_ => self.xpub_view.paint(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
match self.current_page {
|
||||
0 => self.qr_code.bounds(sink),
|
||||
1 => self.details.bounds(sink),
|
||||
_ => self.xpub_view.bounds(sink),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for AddressDetails<T>
|
||||
where
|
||||
T: ParagraphStrType + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("AddressDetails");
|
||||
match self.current_page {
|
||||
0 => self.qr_code.trace(t),
|
||||
1 => self.details.trace(t),
|
||||
_ => self.xpub_view.trace(t),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -2,7 +2,8 @@ use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{
|
||||
Component, ComponentExt, Event, EventCtx, FixedHeightBar, GridPlaced, Map, TimerToken,
|
||||
Component, ComponentExt, Event, EventCtx, FixedHeightBar, Floating, GridPlaced, Map,
|
||||
Paginate, TimerToken, VSplit,
|
||||
},
|
||||
display::{self, toif::Icon, Color, Font},
|
||||
event::TouchEvent,
|
||||
@ -34,7 +35,7 @@ impl<T> Button<T> {
|
||||
/// (positive).
|
||||
pub const BASELINE_OFFSET: i16 = -3;
|
||||
|
||||
pub fn new(content: ButtonContent<T>) -> Self {
|
||||
pub const fn new(content: ButtonContent<T>) -> Self {
|
||||
Self {
|
||||
content,
|
||||
area: Rect::zero(),
|
||||
@ -46,19 +47,19 @@ impl<T> Button<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_text(text: T) -> Self {
|
||||
pub const fn with_text(text: T) -> Self {
|
||||
Self::new(ButtonContent::Text(text))
|
||||
}
|
||||
|
||||
pub fn with_icon(icon: Icon) -> Self {
|
||||
pub const fn with_icon(icon: Icon) -> Self {
|
||||
Self::new(ButtonContent::Icon(icon))
|
||||
}
|
||||
|
||||
pub fn with_icon_and_text(content: IconText) -> Self {
|
||||
pub const fn with_icon_and_text(content: IconText) -> Self {
|
||||
Self::new(ButtonContent::IconAndText(content))
|
||||
}
|
||||
|
||||
pub fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self {
|
||||
pub const fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self {
|
||||
Self::new(ButtonContent::IconBlend(bg, fg, fg_offset))
|
||||
}
|
||||
|
||||
@ -425,6 +426,29 @@ impl<T> Button<T> {
|
||||
Self::cancel_confirm(left, right, right_size_factor)
|
||||
}
|
||||
|
||||
pub fn cancel_confirm_square(
|
||||
left: Button<T>,
|
||||
right: Button<T>,
|
||||
) -> CancelConfirmSquare<
|
||||
T,
|
||||
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
|
||||
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
|
||||
>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
theme::button_bar(VSplit::new(
|
||||
theme::BUTTON_HEIGHT,
|
||||
theme::BUTTON_SPACING,
|
||||
left.map(|msg| {
|
||||
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
|
||||
}),
|
||||
right.map(|msg| {
|
||||
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn cancel_info_confirm(
|
||||
confirm: T,
|
||||
info: T,
|
||||
@ -542,6 +566,9 @@ type CancelInfoConfirm<T, F0, F1, F2> = FixedHeightBar<(
|
||||
Map<GridPlaced<Button<T>>, F2>,
|
||||
)>;
|
||||
|
||||
type CancelConfirmSquare<T, F0, F1> =
|
||||
FixedHeightBar<VSplit<Map<Button<T>, F0>, Map<Button<T>, F1>>>;
|
||||
|
||||
pub enum CancelInfoConfirmMsg {
|
||||
Cancelled,
|
||||
Info,
|
||||
@ -610,3 +637,87 @@ impl IconText {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FloatingButton<T> {
|
||||
inner: T,
|
||||
button: Floating<Button<&'static str>>,
|
||||
}
|
||||
|
||||
pub enum FloatingButtonMsg<T> {
|
||||
ButtonClicked,
|
||||
Content(T),
|
||||
}
|
||||
|
||||
impl<T> FloatingButton<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
pub const fn top_right_corner(icon: Icon, inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
button: Floating::top_right(
|
||||
theme::CORNER_BUTTON_SIDE,
|
||||
theme::CORNER_BUTTON_SPACING,
|
||||
Button::with_icon(icon),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for FloatingButton<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = FloatingButtonMsg<T::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.button.place(bounds);
|
||||
self.inner.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
|
||||
return Some(FloatingButtonMsg::ButtonClicked);
|
||||
}
|
||||
self.inner.event(ctx, event).map(FloatingButtonMsg::Content)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.inner.paint();
|
||||
self.button.paint();
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
self.inner.bounds(sink);
|
||||
self.button.bounds(sink);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Paginate for FloatingButton<T>
|
||||
where
|
||||
T: Paginate,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
self.inner.page_count()
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
self.inner.change_page(to_page)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for FloatingButton<T>
|
||||
where
|
||||
T: Component + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("FloatingButton");
|
||||
t.field("inner", self.inner());
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
use crate::ui::{
|
||||
component::{
|
||||
image::BlendedImage,
|
||||
text::paragraphs::{
|
||||
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
|
||||
text::{
|
||||
paragraphs::{
|
||||
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
|
||||
},
|
||||
TextStyle,
|
||||
},
|
||||
Child, Component, Event, EventCtx, Never,
|
||||
},
|
||||
@ -116,15 +119,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: T) -> Self {
|
||||
if !description.as_ref().is_empty() {
|
||||
pub fn with_text(mut self, style: &'static TextStyle, text: T) -> Self {
|
||||
if !text.as_ref().is_empty() {
|
||||
self.paragraphs
|
||||
.inner_mut()
|
||||
.add(Paragraph::new(&theme::TEXT_NORMAL_OFF_WHITE, description).centered());
|
||||
.add(Paragraph::new(style, text).centered());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(self, description: T) -> Self {
|
||||
self.with_text(&theme::TEXT_NORMAL_OFF_WHITE, description)
|
||||
}
|
||||
|
||||
pub fn new_shares(lines: [T; 4], controls: U) -> Self {
|
||||
let [l0, l1, l2, l3] = lines;
|
||||
Self {
|
||||
|
@ -1,6 +1,8 @@
|
||||
use super::theme;
|
||||
use crate::ui::{
|
||||
component::{label::Label, text::TextStyle, Child, Component, Event, EventCtx},
|
||||
component::{
|
||||
base::ComponentExt, label::Label, text::TextStyle, Child, Component, Event, EventCtx,
|
||||
},
|
||||
display::{self, toif::Icon, Color},
|
||||
geometry::{Alignment, Insets, Offset, Rect},
|
||||
util::icon_text_center,
|
||||
@ -45,6 +47,23 @@ where
|
||||
pub fn inner(&self) -> &T {
|
||||
self.content.inner()
|
||||
}
|
||||
|
||||
pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: U) {
|
||||
self.title.mutate(ctx, |ctx, t| {
|
||||
t.set_text(new_title);
|
||||
t.request_complete_repaint(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_content<F>(&mut self, ctx: &mut EventCtx, update_fn: F)
|
||||
where
|
||||
F: Fn(&mut T),
|
||||
{
|
||||
self.content.mutate(ctx, |ctx, c| {
|
||||
update_fn(c);
|
||||
c.request_complete_repaint(ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Component for Frame<T, U>
|
||||
|
144
core/embed/rust/src/ui/model_tt/component/horizontal_page.rs
Normal file
144
core/embed/rust/src/ui/model_tt/component/horizontal_page.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use crate::ui::{
|
||||
component::{
|
||||
base::ComponentExt, AuxPageMsg, Component, Event, EventCtx, Never, Pad, PageMsg, Paginate,
|
||||
},
|
||||
display::{self, Color},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
use super::{theme, ScrollBar, Swipe, SwipeDirection};
|
||||
|
||||
const SCROLLBAR_HEIGHT: i16 = 32;
|
||||
|
||||
pub struct HorizontalPage<T> {
|
||||
content: T,
|
||||
pad: Pad,
|
||||
swipe: Swipe,
|
||||
scrollbar: ScrollBar,
|
||||
swipe_right_to_go_back: bool,
|
||||
fade: Option<u16>,
|
||||
}
|
||||
|
||||
impl<T> HorizontalPage<T>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
{
|
||||
pub fn new(content: T, background: Color) -> Self {
|
||||
Self {
|
||||
content,
|
||||
swipe: Swipe::new(),
|
||||
pad: Pad::with_background(background),
|
||||
scrollbar: ScrollBar::horizontal(),
|
||||
swipe_right_to_go_back: false,
|
||||
fade: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_swipe_right_to_go_back(mut self) -> Self {
|
||||
self.swipe_right_to_go_back = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
&self.content
|
||||
}
|
||||
|
||||
fn setup_swipe(&mut self) {
|
||||
self.swipe.allow_left = self.scrollbar.has_next_page();
|
||||
self.swipe.allow_right = self.scrollbar.has_previous_page() || self.swipe_right_to_go_back;
|
||||
}
|
||||
|
||||
fn on_page_change(&mut self, ctx: &mut EventCtx) {
|
||||
// Adjust the swipe parameters according to the scrollbar.
|
||||
self.setup_swipe();
|
||||
|
||||
// Change the page in the content, make sure it gets completely repainted and
|
||||
// clear the background under it.
|
||||
self.content.change_page(self.scrollbar.active_page);
|
||||
self.content.request_complete_repaint(ctx);
|
||||
self.pad.clear();
|
||||
|
||||
// Swipe has dimmed the screen, so fade back to normal backlight after the next
|
||||
// paint.
|
||||
self.fade = Some(theme::BACKLIGHT_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for HorizontalPage<T>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
{
|
||||
type Msg = PageMsg<T::Msg, Never>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.swipe.place(bounds);
|
||||
|
||||
let (content, scrollbar) = bounds.split_bottom(SCROLLBAR_HEIGHT);
|
||||
self.pad.place(content);
|
||||
self.content.place(content);
|
||||
self.scrollbar.place(scrollbar);
|
||||
|
||||
self.scrollbar
|
||||
.set_count_and_active_page(self.content.page_count(), 0);
|
||||
self.setup_swipe();
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
ctx.set_page_count(self.scrollbar.page_count);
|
||||
if let Some(swipe) = self.swipe.event(ctx, event) {
|
||||
match swipe {
|
||||
SwipeDirection::Left => {
|
||||
self.scrollbar.go_to_next_page();
|
||||
self.on_page_change(ctx);
|
||||
return None;
|
||||
}
|
||||
SwipeDirection::Right => {
|
||||
if self.swipe_right_to_go_back && self.scrollbar.active_page == 0 {
|
||||
return Some(PageMsg::Aux(AuxPageMsg::GoBack));
|
||||
}
|
||||
self.scrollbar.go_to_previous_page();
|
||||
self.on_page_change(ctx);
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
// Ignore other directions.
|
||||
}
|
||||
}
|
||||
}
|
||||
self.content.event(ctx, event).map(PageMsg::Content)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.content.paint();
|
||||
self.scrollbar.paint();
|
||||
if let Some(val) = self.fade.take() {
|
||||
// Note that this is blocking and takes some time.
|
||||
display::fade_backlight(val);
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
sink(self.pad.area);
|
||||
self.scrollbar.bounds(sink);
|
||||
self.content.bounds(sink);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for HorizontalPage<T>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("HorizontalPage");
|
||||
t.field("active_page", &self.scrollbar.active_page);
|
||||
t.field("page_count", &self.scrollbar.page_count);
|
||||
t.field("content", &self.content);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod address_details;
|
||||
mod button;
|
||||
mod dialog;
|
||||
mod fido;
|
||||
@ -7,6 +8,7 @@ mod frame;
|
||||
mod hold_to_confirm;
|
||||
#[cfg(feature = "dma2d")]
|
||||
mod homescreen;
|
||||
mod horizontal_page;
|
||||
mod keyboard;
|
||||
mod loader;
|
||||
mod number_input;
|
||||
@ -17,9 +19,10 @@ mod scroll;
|
||||
mod swipe;
|
||||
mod welcome_screen;
|
||||
|
||||
pub use address_details::AddressDetails;
|
||||
pub use button::{
|
||||
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
|
||||
CancelInfoConfirmMsg, IconText, SelectWordMsg,
|
||||
CancelInfoConfirmMsg, FloatingButton, FloatingButtonMsg, IconText, SelectWordMsg,
|
||||
};
|
||||
pub use dialog::{Dialog, DialogMsg, IconDialog};
|
||||
pub use fido::{FidoConfirm, FidoMsg};
|
||||
@ -27,6 +30,7 @@ pub use frame::{Frame, NotificationFrame};
|
||||
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
|
||||
#[cfg(feature = "dma2d")]
|
||||
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
|
||||
pub use horizontal_page::HorizontalPage;
|
||||
pub use keyboard::{
|
||||
bip39::Bip39Input,
|
||||
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::ui::{
|
||||
component::{
|
||||
base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, FixedHeightBar, Label,
|
||||
Pad, Paginate,
|
||||
base::ComponentExt,
|
||||
paginated::{AuxPageMsg, PageMsg},
|
||||
Component, Event, EventCtx, FixedHeightBar, Label, Pad, Paginate,
|
||||
},
|
||||
display::{self, toif::Icon, Color},
|
||||
geometry::{Insets, Rect},
|
||||
@ -21,6 +22,7 @@ pub struct SwipePage<T, U> {
|
||||
scrollbar: ScrollBar,
|
||||
hint: Label<&'static str>,
|
||||
button_back: Option<Button<&'static str>>,
|
||||
swipe_left: bool,
|
||||
fade: Option<u16>,
|
||||
}
|
||||
|
||||
@ -39,6 +41,7 @@ where
|
||||
pad: Pad::with_background(background),
|
||||
hint: Label::centered("SWIPE TO CONTINUE", theme::label_page_hint()),
|
||||
button_back: None,
|
||||
swipe_left: false,
|
||||
fade: None,
|
||||
}
|
||||
}
|
||||
@ -48,9 +51,15 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_swipe_left(mut self) -> Self {
|
||||
self.swipe_left = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn setup_swipe(&mut self) {
|
||||
self.swipe.allow_up = self.scrollbar.has_next_page();
|
||||
self.swipe.allow_down = self.scrollbar.has_previous_page();
|
||||
self.swipe.allow_left = self.swipe_left;
|
||||
}
|
||||
|
||||
fn on_page_change(&mut self, ctx: &mut EventCtx) {
|
||||
@ -138,6 +147,9 @@ where
|
||||
self.on_page_change(ctx);
|
||||
return None;
|
||||
}
|
||||
SwipeDirection::Left if self.swipe_left => {
|
||||
return Some(PageMsg::Aux(AuxPageMsg::SwipeLeft));
|
||||
}
|
||||
_ => {
|
||||
// Ignore other directions.
|
||||
}
|
||||
@ -152,7 +164,7 @@ where
|
||||
}
|
||||
} else {
|
||||
if let Some(ButtonMsg::Clicked) = self.button_back.event(ctx, event) {
|
||||
return Some(PageMsg::GoBack);
|
||||
return Some(PageMsg::Aux(AuxPageMsg::GoBack));
|
||||
}
|
||||
self.hint.event(ctx, event);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use crate::{
|
||||
component::{
|
||||
base::ComponentExt,
|
||||
image::BlendedImage,
|
||||
paginated::{PageMsg, Paginate},
|
||||
paginated::{AuxPageMsg, PageMsg, Paginate},
|
||||
painter,
|
||||
placed::GridPlaced,
|
||||
text::{
|
||||
@ -45,9 +45,10 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
component::{
|
||||
Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg,
|
||||
Dialog, DialogMsg, FidoConfirm, FidoMsg, Frame, HoldToConfirm, HoldToConfirmMsg,
|
||||
Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard,
|
||||
AddressDetails, Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg,
|
||||
CancelInfoConfirmMsg, Dialog, DialogMsg, FidoConfirm, FidoMsg, FloatingButton,
|
||||
FloatingButtonMsg, Frame, HoldToConfirm, HoldToConfirmMsg, Homescreen, HomescreenMsg,
|
||||
HorizontalPage, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard,
|
||||
MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog, NumberInputDialogMsg,
|
||||
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress,
|
||||
SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input, SwipeHoldPage, SwipePage,
|
||||
@ -223,7 +224,8 @@ where
|
||||
match msg {
|
||||
PageMsg::Content(_) => Err(Error::TypeError),
|
||||
PageMsg::Controls(msg) => msg.try_into(),
|
||||
PageMsg::GoBack => Ok(CANCELLED.as_obj()),
|
||||
PageMsg::Aux(AuxPageMsg::GoBack) => Ok(CANCELLED.as_obj()),
|
||||
PageMsg::Aux(AuxPageMsg::SwipeLeft) => Ok(INFO.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -236,7 +238,7 @@ where
|
||||
match msg {
|
||||
PageMsg::Content(_) => Err(Error::TypeError),
|
||||
PageMsg::Controls(msg) => msg.try_into(),
|
||||
PageMsg::GoBack => unreachable!(),
|
||||
PageMsg::Aux(_) => Err(Error::TypeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -338,6 +340,41 @@ impl ComponentMsgObj for Qr {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ComponentMsgObj for FloatingButton<T>
|
||||
where
|
||||
T: ComponentMsgObj,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
FloatingButtonMsg::ButtonClicked => Ok(INFO.as_obj()),
|
||||
FloatingButtonMsg::Content(c) => self.inner().msg_try_into_obj(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ComponentMsgObj for HorizontalPage<T>
|
||||
where
|
||||
T: ComponentMsgObj + Paginate,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
PageMsg::Content(inner_msg) => Ok(self.inner().msg_try_into_obj(inner_msg)?),
|
||||
PageMsg::Controls(_) => Err(Error::TypeError),
|
||||
PageMsg::Aux(AuxPageMsg::GoBack) => Ok(CANCELLED.as_obj()),
|
||||
PageMsg::Aux(_) => Err(Error::TypeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ComponentMsgObj for AddressDetails<T>
|
||||
where
|
||||
T: ParagraphStrType,
|
||||
{
|
||||
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
@ -434,23 +471,54 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map
|
||||
let description: Option<StrBuffer> =
|
||||
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
|
||||
let extra: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
|
||||
let verb: Option<StrBuffer> = kwargs
|
||||
.get(Qstr::MP_QSTR_verb)
|
||||
.unwrap_or_else(|_| Obj::const_none())
|
||||
.try_into_option()?;
|
||||
let verb_cancel: Option<StrBuffer> = kwargs
|
||||
.get(Qstr::MP_QSTR_verb_cancel)
|
||||
.unwrap_or_else(|_| Obj::const_none())
|
||||
.try_into_option()?;
|
||||
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
|
||||
|
||||
let verb: StrBuffer = "CONFIRM".into();
|
||||
confirm_blob(title, data, description, extra, verb, verb_cancel, hold)
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
confirm_blob(
|
||||
title,
|
||||
data,
|
||||
description,
|
||||
extra,
|
||||
Some(verb),
|
||||
verb_cancel,
|
||||
hold,
|
||||
)
|
||||
extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let description: Option<StrBuffer> =
|
||||
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
|
||||
let extra: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
|
||||
let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?;
|
||||
|
||||
let paragraphs = ConfirmBlob {
|
||||
description: description.unwrap_or_else(StrBuffer::empty),
|
||||
extra: extra.unwrap_or_else(StrBuffer::empty),
|
||||
data: data.try_into()?,
|
||||
description_font: &theme::TEXT_NORMAL,
|
||||
extra_font: &theme::TEXT_BOLD,
|
||||
data_font: &theme::TEXT_MONO,
|
||||
}
|
||||
.into_paragraphs();
|
||||
|
||||
let buttons = Button::cancel_confirm(
|
||||
Button::<&'static str>::with_icon(Icon::new(theme::ICON_CANCEL)),
|
||||
Button::<&'static str>::with_icon(Icon::new(theme::ICON_CONFIRM))
|
||||
.styled(theme::button_confirm()),
|
||||
1,
|
||||
);
|
||||
let obj = LayoutObj::new(FloatingButton::top_right_corner(
|
||||
Icon::new(theme::ICON_INFO_CIRCLE),
|
||||
Frame::left_aligned(
|
||||
theme::label_title(),
|
||||
title,
|
||||
SwipePage::new(paragraphs, buttons, theme::BG).with_swipe_left(),
|
||||
),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
@ -583,6 +651,36 @@ extern "C" fn new_show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) ->
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
|
||||
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
|
||||
let account: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?;
|
||||
let path: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_path)?.try_into_option()?;
|
||||
|
||||
let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?;
|
||||
let mut iter_buf = IterBuf::new();
|
||||
let iter = Iter::try_from_obj_with_buf(xpubs, &mut iter_buf)?;
|
||||
|
||||
let mut ad = AddressDetails::new(address, case_sensitive, account, path)?;
|
||||
|
||||
for i in iter {
|
||||
let [xtitle, text]: [StrBuffer; 2] = iter_into_array(i)?;
|
||||
ad.add_xpub(xtitle, text)?;
|
||||
}
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
HorizontalPage::new(
|
||||
FloatingButton::top_right_corner(Icon::new(theme::ICON_CANCEL_LARGER), ad),
|
||||
theme::BG,
|
||||
)
|
||||
.with_swipe_right_to_go_back(),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
@ -847,6 +945,33 @@ extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_show_mismatch() -> Obj {
|
||||
let block = move || {
|
||||
let title: StrBuffer = "Address mismatch?".into();
|
||||
let description: StrBuffer = "Please contact Trezor support at".into();
|
||||
let url: StrBuffer = "trezor.io/support".into();
|
||||
let button = "QUIT";
|
||||
|
||||
let icon = BlendedImage::single(Icon::new(theme::ICON_OCTA), theme::WARN_COLOR, theme::BG);
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
IconDialog::new(
|
||||
icon,
|
||||
title,
|
||||
Button::cancel_confirm_square(
|
||||
Button::with_icon(Icon::new(theme::ICON_BACK)),
|
||||
Button::with_text(button).styled(theme::button_reset()),
|
||||
),
|
||||
)
|
||||
.with_description(description)
|
||||
.with_text(&theme::TEXT_DEMIBOLD, url),
|
||||
)?;
|
||||
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?;
|
||||
@ -1422,12 +1547,24 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// data: str | bytes,
|
||||
/// description: str | None,
|
||||
/// extra: str | None,
|
||||
/// verb: str | None = None,
|
||||
/// verb_cancel: str | None = None,
|
||||
/// hold: bool = False,
|
||||
/// ) -> object:
|
||||
/// """Confirm byte sequence data."""
|
||||
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),
|
||||
|
||||
/// def confirm_address(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// data: str | bytes,
|
||||
/// description: str | None,
|
||||
/// extra: str | None,
|
||||
/// ) -> object:
|
||||
/// """Confirm address. Similar to `confirm_blob` but has corner info button
|
||||
/// and allows left swipe which does the same thing as the button."""
|
||||
Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(),
|
||||
|
||||
/// def confirm_properties(
|
||||
/// *,
|
||||
/// title: str,
|
||||
@ -1456,6 +1593,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// """Show QR code."""
|
||||
Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, new_show_qr).as_obj(),
|
||||
|
||||
/// def show_address_details(
|
||||
/// *,
|
||||
/// address: str,
|
||||
/// case_sensitive: bool,
|
||||
/// account: str | None,
|
||||
/// path: str | None,
|
||||
/// xpubs: list[tuple[str, str]],
|
||||
/// ) -> object:
|
||||
/// """Show address details - QR code, account, path, cosigner xpubs."""
|
||||
Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(),
|
||||
|
||||
/// def confirm_value(
|
||||
/// *,
|
||||
/// title: str,
|
||||
@ -1551,6 +1699,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// """Info modal. No buttons shown when `button` is empty string."""
|
||||
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
|
||||
|
||||
/// def show_mismatch() -> object:
|
||||
/// """Warning modal, receiving address mismatch."""
|
||||
Qstr::MP_QSTR_show_mismatch => obj_fn_0!(new_show_mismatch).as_obj(),
|
||||
|
||||
/// def show_simple(
|
||||
/// *,
|
||||
/// title: str | None,
|
||||
|
BIN
core/embed/rust/src/ui/model_tt/res/cancel-larger.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/cancel-larger.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tt/res/info-circle.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/info-circle.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tt/res/octa-bang.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/octa-bang.toif
Normal file
Binary file not shown.
@ -55,6 +55,7 @@ pub const ICON_SIZE: i16 = 16;
|
||||
|
||||
// UI icons (greyscale).
|
||||
pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif");
|
||||
pub const ICON_CANCEL_LARGER: &[u8] = include_res!("model_tt/res/cancel-larger.toif");
|
||||
pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.toif");
|
||||
pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif");
|
||||
pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif");
|
||||
@ -70,6 +71,8 @@ pub const ICON_SUCCESS_SMALL: &[u8] = include_res!("model_tt/res/success_bld.toi
|
||||
pub const ICON_WARN_SMALL: &[u8] = include_res!("model_tt/res/warn_bld.toif");
|
||||
pub const ICON_PAGE_NEXT: &[u8] = include_res!("model_tt/res/page-next.toif");
|
||||
pub const ICON_PAGE_PREV: &[u8] = include_res!("model_tt/res/page-prev.toif");
|
||||
pub const ICON_OCTA: &[u8] = include_res!("model_tt/res/octa-bang.toif");
|
||||
pub const ICON_INFO_CIRCLE: &[u8] = include_res!("model_tt/res/info-circle.toif");
|
||||
|
||||
// Large, three-color icons.
|
||||
pub const WARN_COLOR: Color = YELLOW;
|
||||
@ -139,7 +142,7 @@ pub const fn label_title() -> TextStyle {
|
||||
TextStyle::new(Font::BOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||
}
|
||||
|
||||
pub fn button_default() -> ButtonStyleSheet {
|
||||
pub const fn button_default() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::BOLD,
|
||||
@ -171,7 +174,7 @@ pub fn button_default() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_confirm() -> ButtonStyleSheet {
|
||||
pub const fn button_confirm() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::BOLD,
|
||||
@ -203,7 +206,7 @@ pub fn button_confirm() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_cancel() -> ButtonStyleSheet {
|
||||
pub const fn button_cancel() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::BOLD,
|
||||
@ -235,11 +238,11 @@ pub fn button_cancel() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_danger() -> ButtonStyleSheet {
|
||||
pub const fn button_danger() -> ButtonStyleSheet {
|
||||
button_cancel()
|
||||
}
|
||||
|
||||
pub fn button_reset() -> ButtonStyleSheet {
|
||||
pub const fn button_reset() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::BOLD,
|
||||
@ -271,7 +274,7 @@ pub fn button_reset() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_info() -> ButtonStyleSheet {
|
||||
pub const fn button_info() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::BOLD,
|
||||
@ -303,7 +306,7 @@ pub fn button_info() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_pin() -> ButtonStyleSheet {
|
||||
pub const fn button_pin() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
@ -335,7 +338,7 @@ pub fn button_pin() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_counter() -> ButtonStyleSheet {
|
||||
pub const fn button_counter() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::DEMIBOLD,
|
||||
@ -367,11 +370,11 @@ pub fn button_counter() -> ButtonStyleSheet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_clear() -> ButtonStyleSheet {
|
||||
pub const fn button_clear() -> ButtonStyleSheet {
|
||||
button_default()
|
||||
}
|
||||
|
||||
pub fn loader_default() -> LoaderStyleSheet {
|
||||
pub const fn loader_default() -> LoaderStyleSheet {
|
||||
LoaderStyleSheet {
|
||||
normal: &LoaderStyle {
|
||||
icon: None,
|
||||
@ -418,6 +421,7 @@ pub const TEXT_CHECKLIST_SELECTED: TextStyle =
|
||||
TextStyle::new(Font::NORMAL, FG, BG, GREY_LIGHT, GREY_LIGHT);
|
||||
pub const TEXT_CHECKLIST_DONE: TextStyle =
|
||||
TextStyle::new(Font::NORMAL, GREEN_DARK, BG, GREY_LIGHT, GREY_LIGHT);
|
||||
pub const TEXT_XPUB: TextStyle = TEXT_NORMAL.with_line_breaking(LineBreaking::BreakWordsNoHyphen);
|
||||
|
||||
pub const FORMATTED: FormattedFonts = FormattedFonts {
|
||||
normal: Font::NORMAL,
|
||||
@ -432,6 +436,8 @@ pub const BUTTON_HEIGHT: i16 = 38;
|
||||
pub const BUTTON_SPACING: i16 = 6;
|
||||
pub const CHECKLIST_SPACING: i16 = 10;
|
||||
pub const RECOVERY_SPACING: i16 = 18;
|
||||
pub const CORNER_BUTTON_SIDE: i16 = 32;
|
||||
pub const CORNER_BUTTON_SPACING: i16 = 8;
|
||||
|
||||
/// Standard button height in pixels.
|
||||
pub const fn button_rows(count: usize) -> i16 {
|
||||
@ -462,6 +468,10 @@ pub const fn borders_scroll() -> Insets {
|
||||
Insets::new(13, 5, 14, 10)
|
||||
}
|
||||
|
||||
pub const fn borders_horizontal_scroll() -> Insets {
|
||||
Insets::new(13, 10, 0, 10)
|
||||
}
|
||||
|
||||
pub const fn borders_notification() -> Insets {
|
||||
Insets::new(48, 10, 14, 10)
|
||||
}
|
||||
|
@ -82,12 +82,25 @@ def confirm_blob(
|
||||
data: str | bytes,
|
||||
description: str | None,
|
||||
extra: str | None,
|
||||
verb: str | None = None,
|
||||
verb_cancel: str | None = None,
|
||||
hold: bool = False,
|
||||
) -> object:
|
||||
"""Confirm byte sequence data."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
def confirm_address(
|
||||
*,
|
||||
title: str,
|
||||
data: str | bytes,
|
||||
description: str | None,
|
||||
extra: str | None,
|
||||
) -> object:
|
||||
"""Confirm address. Similar to `confirm_blob` but has corner info button
|
||||
and allows left swipe which does the same thing as the button."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
def confirm_properties(
|
||||
*,
|
||||
@ -119,6 +132,18 @@ def show_qr(
|
||||
"""Show QR code."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
def show_address_details(
|
||||
*,
|
||||
address: str,
|
||||
case_sensitive: bool,
|
||||
account: str | None,
|
||||
path: str | None,
|
||||
xpubs: list[tuple[str, str]],
|
||||
) -> object:
|
||||
"""Show address details - QR code, account, path, cosigner xpubs."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
def confirm_value(
|
||||
*,
|
||||
@ -222,6 +247,11 @@ def show_info(
|
||||
"""Info modal. No buttons shown when `button` is empty string."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
def show_mismatch() -> object:
|
||||
"""Warning modal, receiving address mismatch."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
def show_simple(
|
||||
*,
|
||||
|
@ -28,7 +28,6 @@ async def get_address(
|
||||
pubkey = node.public_key()
|
||||
address = address_from_public_key(pubkey, HRP)
|
||||
if msg.show_display:
|
||||
title = paths.address_n_to_str(address_n)
|
||||
await show_address(ctx, address, title=title)
|
||||
await show_address(ctx, address, path=paths.address_n_to_str(address_n))
|
||||
|
||||
return BinanceAddress(address=address)
|
||||
|
@ -41,7 +41,7 @@ async def get_address(
|
||||
from apps.common.paths import address_n_to_str, validate_path
|
||||
|
||||
from . import addresses
|
||||
from .keychain import validate_path_against_script_type
|
||||
from .keychain import address_n_to_name, validate_path_against_script_type
|
||||
from .multisig import multisig_pubkey_index
|
||||
|
||||
multisig = msg.multisig # local_cache_attribute
|
||||
@ -95,6 +95,7 @@ async def get_address(
|
||||
mac = get_address_mac(address, coin.slip44, keychain)
|
||||
|
||||
if msg.show_display:
|
||||
path = address_n_to_str(address_n)
|
||||
if multisig:
|
||||
if multisig.nodes:
|
||||
pubnodes = multisig.nodes
|
||||
@ -102,23 +103,30 @@ async def get_address(
|
||||
pubnodes = [hd.node for hd in multisig.pubkeys]
|
||||
multisig_index = multisig_pubkey_index(multisig, node.public_key())
|
||||
|
||||
title = f"Multisig {multisig.m} of {len(pubnodes)}"
|
||||
await show_address(
|
||||
ctx,
|
||||
address_short,
|
||||
case_sensitive=address_case_sensitive,
|
||||
title=title,
|
||||
path=path,
|
||||
multisig_index=multisig_index,
|
||||
xpubs=_get_xpubs(coin, multisig_xpub_magic, pubnodes),
|
||||
account=f"Multisig {multisig.m} of {len(pubnodes)}",
|
||||
)
|
||||
else:
|
||||
title = address_n_to_str(address_n)
|
||||
account_name = address_n_to_name(coin, address_n, script_type)
|
||||
if account_name is None:
|
||||
account = "Unknown path"
|
||||
elif account_name == "":
|
||||
account = coin.coin_shortcut
|
||||
else:
|
||||
account = f"{coin.coin_shortcut} {account_name}"
|
||||
await show_address(
|
||||
ctx,
|
||||
address_short,
|
||||
address_qr=address,
|
||||
case_sensitive=address_case_sensitive,
|
||||
title=title,
|
||||
path=path,
|
||||
account=account,
|
||||
)
|
||||
|
||||
return Address(address=address, mac=mac)
|
||||
|
@ -384,7 +384,7 @@ def address_n_to_name(
|
||||
) -> str | None:
|
||||
ACCOUNT_TYPES = (
|
||||
AccountType(
|
||||
"Legacy account",
|
||||
"Legacy",
|
||||
PATTERN_BIP44,
|
||||
InputScriptType.SPENDADDRESS,
|
||||
require_segwit=True,
|
||||
@ -392,7 +392,7 @@ def address_n_to_name(
|
||||
require_taproot=False,
|
||||
),
|
||||
AccountType(
|
||||
"Account",
|
||||
"",
|
||||
PATTERN_BIP44,
|
||||
InputScriptType.SPENDADDRESS,
|
||||
require_segwit=False,
|
||||
@ -400,7 +400,7 @@ def address_n_to_name(
|
||||
require_taproot=False,
|
||||
),
|
||||
AccountType(
|
||||
"Legacy SegWit account",
|
||||
"L. SegWit",
|
||||
PATTERN_BIP49,
|
||||
InputScriptType.SPENDP2SHWITNESS,
|
||||
require_segwit=True,
|
||||
@ -408,7 +408,7 @@ def address_n_to_name(
|
||||
require_taproot=False,
|
||||
),
|
||||
AccountType(
|
||||
"SegWit account",
|
||||
"SegWit",
|
||||
PATTERN_BIP84,
|
||||
InputScriptType.SPENDWITNESS,
|
||||
require_segwit=True,
|
||||
@ -416,7 +416,7 @@ def address_n_to_name(
|
||||
require_taproot=False,
|
||||
),
|
||||
AccountType(
|
||||
"Taproot account",
|
||||
"Taproot",
|
||||
PATTERN_BIP86,
|
||||
InputScriptType.SPENDTAPROOT,
|
||||
require_segwit=False,
|
||||
@ -424,7 +424,7 @@ def address_n_to_name(
|
||||
require_taproot=True,
|
||||
),
|
||||
AccountType(
|
||||
"Coinjoin account",
|
||||
"Coinjoin",
|
||||
PATTERN_SLIP25_TAPROOT,
|
||||
InputScriptType.SPENDTAPROOT,
|
||||
require_segwit=False,
|
||||
|
@ -975,9 +975,8 @@ async def show_cardano_address(
|
||||
if not protocol_magics.is_mainnet(protocol_magic):
|
||||
network_name = protocol_magics.to_ui_string(protocol_magic)
|
||||
|
||||
title = f"{ADDRESS_TYPE_NAMES[address_parameters.address_type]} address"
|
||||
address_extra = None
|
||||
title_qr = title
|
||||
path = None
|
||||
account = ADDRESS_TYPE_NAMES[address_parameters.address_type]
|
||||
if address_parameters.address_type in (
|
||||
CAT.BYRON,
|
||||
CAT.BASE,
|
||||
@ -987,17 +986,14 @@ async def show_cardano_address(
|
||||
CAT.REWARD,
|
||||
):
|
||||
if address_parameters.address_n:
|
||||
address_extra = address_n_to_str(address_parameters.address_n)
|
||||
title_qr = address_n_to_str(address_parameters.address_n)
|
||||
path = address_n_to_str(address_parameters.address_n)
|
||||
elif address_parameters.address_n_staking:
|
||||
address_extra = address_n_to_str(address_parameters.address_n_staking)
|
||||
title_qr = address_n_to_str(address_parameters.address_n_staking)
|
||||
path = address_n_to_str(address_parameters.address_n_staking)
|
||||
|
||||
await layouts.show_address(
|
||||
ctx,
|
||||
address,
|
||||
title=title,
|
||||
path=path,
|
||||
account=account,
|
||||
network=network_name,
|
||||
address_extra=address_extra,
|
||||
title_qr=title_qr,
|
||||
)
|
||||
|
@ -32,7 +32,6 @@ async def get_address(
|
||||
address = address_from_bytes(node.ethereum_pubkeyhash(), network)
|
||||
|
||||
if msg.show_display:
|
||||
title = paths.address_n_to_str(address_n)
|
||||
await show_address(ctx, address, title=title)
|
||||
await show_address(ctx, address, path=paths.address_n_to_str(address_n))
|
||||
|
||||
return EthereumAddress(address=address)
|
||||
|
@ -68,12 +68,11 @@ async def get_address(
|
||||
)
|
||||
|
||||
if msg.show_display:
|
||||
title = paths.address_n_to_str(msg.address_n)
|
||||
await show_address(
|
||||
ctx,
|
||||
addr,
|
||||
address_qr="monero:" + addr,
|
||||
title=title,
|
||||
path=paths.address_n_to_str(msg.address_n),
|
||||
)
|
||||
|
||||
return MoneroAddress(address=addr.encode())
|
||||
|
@ -30,12 +30,11 @@ async def get_address(
|
||||
address = node.nem_address(network)
|
||||
|
||||
if msg.show_display:
|
||||
title = address_n_to_str(address_n)
|
||||
await show_address(
|
||||
ctx,
|
||||
address,
|
||||
case_sensitive=False,
|
||||
title=title,
|
||||
path=address_n_to_str(address_n),
|
||||
network=get_network_str(network),
|
||||
)
|
||||
|
||||
|
@ -25,7 +25,6 @@ async def get_address(
|
||||
address = address_from_public_key(pubkey)
|
||||
|
||||
if msg.show_display:
|
||||
title = paths.address_n_to_str(msg.address_n)
|
||||
await show_address(ctx, address, title=title)
|
||||
await show_address(ctx, address, path=paths.address_n_to_str(msg.address_n))
|
||||
|
||||
return RippleAddress(address=address)
|
||||
|
@ -24,7 +24,7 @@ async def get_address(
|
||||
address = helpers.address_from_public_key(pubkey)
|
||||
|
||||
if msg.show_display:
|
||||
title = paths.address_n_to_str(msg.address_n)
|
||||
await show_address(ctx, address, case_sensitive=False, title=title)
|
||||
path = paths.address_n_to_str(msg.address_n)
|
||||
await show_address(ctx, address, case_sensitive=False, path=path)
|
||||
|
||||
return StellarAddress(address=address)
|
||||
|
@ -29,7 +29,6 @@ async def get_address(
|
||||
address = helpers.base58_encode_check(pkh, helpers.TEZOS_ED25519_ADDRESS_PREFIX)
|
||||
|
||||
if msg.show_display:
|
||||
title = paths.address_n_to_str(msg.address_n)
|
||||
await show_address(ctx, address, title=title)
|
||||
await show_address(ctx, address, path=paths.address_n_to_str(msg.address_n))
|
||||
|
||||
return TezosAddress(address=address)
|
||||
|
@ -164,8 +164,6 @@ async def show_address(
|
||||
network: str | None = None,
|
||||
multisig_index: int | None = None,
|
||||
xpubs: Sequence[str] = (),
|
||||
address_extra: str | None = None,
|
||||
title_qr: str | None = None,
|
||||
) -> None:
|
||||
result = await interact(
|
||||
ctx,
|
||||
|
@ -353,24 +353,20 @@ async def confirm_homescreen(
|
||||
)
|
||||
|
||||
|
||||
def _show_xpub(xpub: str, title: str, cancel: str | None) -> ui.Layout:
|
||||
content = RustLayout(
|
||||
trezorui2.confirm_blob(
|
||||
title=title,
|
||||
data=xpub,
|
||||
verb_cancel=cancel,
|
||||
extra=None,
|
||||
description=None,
|
||||
)
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
async def show_xpub(ctx: GenericContext, xpub: str, title: str) -> None:
|
||||
await raise_if_not_confirmed(
|
||||
interact(
|
||||
ctx,
|
||||
_show_xpub(xpub, title, None),
|
||||
RustLayout(
|
||||
trezorui2.confirm_blob(
|
||||
title=title,
|
||||
data=xpub,
|
||||
verb="CONFIRM",
|
||||
verb_cancel=None,
|
||||
extra=None,
|
||||
description=None,
|
||||
)
|
||||
),
|
||||
"show_xpub",
|
||||
ButtonRequestType.PublicKey,
|
||||
)
|
||||
@ -383,61 +379,72 @@ async def show_address(
|
||||
*,
|
||||
address_qr: str | None = None,
|
||||
case_sensitive: bool = True,
|
||||
title: str = "Confirm address",
|
||||
path: str | None = None,
|
||||
account: str | None = None,
|
||||
network: str | None = None,
|
||||
multisig_index: int | None = None,
|
||||
xpubs: Sequence[str] = (),
|
||||
address_extra: str | None = None,
|
||||
title_qr: str | None = None,
|
||||
) -> None:
|
||||
is_multisig = len(xpubs) > 0
|
||||
while True:
|
||||
title = (
|
||||
"RECEIVE ADDRESS\n(MULTISIG)"
|
||||
if multisig_index is not None
|
||||
else "RECEIVE ADDRESS"
|
||||
)
|
||||
result = await interact(
|
||||
ctx,
|
||||
RustLayout(
|
||||
trezorui2.confirm_blob(
|
||||
title=title.upper(),
|
||||
trezorui2.confirm_address(
|
||||
title=title,
|
||||
data=address,
|
||||
description=network or "",
|
||||
extra=address_extra or "",
|
||||
verb_cancel="QR",
|
||||
extra=None,
|
||||
)
|
||||
),
|
||||
"show_address",
|
||||
ButtonRequestType.Address,
|
||||
)
|
||||
|
||||
# User pressed right button.
|
||||
if result is CONFIRMED:
|
||||
break
|
||||
|
||||
result = await interact(
|
||||
ctx,
|
||||
RustLayout(
|
||||
trezorui2.show_qr(
|
||||
address=address if address_qr is None else address_qr,
|
||||
case_sensitive=case_sensitive,
|
||||
title=title.upper() if title_qr is None else title_qr.upper(),
|
||||
verb_cancel="XPUBs" if is_multisig else "ADDRESS",
|
||||
)
|
||||
),
|
||||
"show_qr",
|
||||
ButtonRequestType.Address,
|
||||
)
|
||||
if result is CONFIRMED:
|
||||
break
|
||||
# User pressed corner button or swiped left, go to address details.
|
||||
elif result is INFO:
|
||||
|
||||
if is_multisig:
|
||||
for i, xpub in enumerate(xpubs):
|
||||
cancel = "NEXT" if i < len(xpubs) - 1 else "ADDRESS"
|
||||
title_xpub = f"XPUB #{i + 1}"
|
||||
title_xpub += " (yours)" if i == multisig_index else " (cosigner)"
|
||||
result = await interact(
|
||||
ctx,
|
||||
_show_xpub(xpub, title=title_xpub, cancel=cancel),
|
||||
"show_xpub",
|
||||
ButtonRequestType.PublicKey,
|
||||
)
|
||||
if result is CONFIRMED:
|
||||
return
|
||||
def xpub_title(i: int):
|
||||
result = f"MULTISIG XPUB #{i + 1}\n"
|
||||
result += " (YOURS)" if i == multisig_index else " (COSIGNER)"
|
||||
return result
|
||||
|
||||
result = await interact(
|
||||
ctx,
|
||||
RustLayout(
|
||||
trezorui2.show_address_details(
|
||||
address=address if address_qr is None else address_qr,
|
||||
case_sensitive=case_sensitive,
|
||||
account=account,
|
||||
path=path,
|
||||
xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)],
|
||||
)
|
||||
),
|
||||
"show_address_details",
|
||||
ButtonRequestType.Address,
|
||||
)
|
||||
# Can only go back from the address details but corner button returns INFO.
|
||||
assert result in (INFO, CANCELLED)
|
||||
|
||||
else:
|
||||
result = await interact(
|
||||
ctx,
|
||||
RustLayout(trezorui2.show_mismatch()),
|
||||
"warning_address_mismatch",
|
||||
ButtonRequestType.Warning,
|
||||
)
|
||||
assert result in (CONFIRMED, CANCELLED)
|
||||
# Right button aborts action, left goes back to showing address.
|
||||
if result is CONFIRMED:
|
||||
raise ActionCancelled
|
||||
|
||||
|
||||
def show_pubkey(
|
||||
@ -693,6 +700,7 @@ async def confirm_blob(
|
||||
data=data,
|
||||
extra=None,
|
||||
hold=hold,
|
||||
verb="CONFIRM",
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -129,9 +129,11 @@ class LayoutContent:
|
||||
|
||||
# First line should have content after the tag, last line does not store content
|
||||
tag = f"< {tag_name}"
|
||||
if tag in self.lines[0]:
|
||||
first_line = self.lines[0].split(tag)[1]
|
||||
all_lines = [first_line] + self.lines[1:-1]
|
||||
for i in range(len(self.lines)):
|
||||
if tag in self.lines[i]:
|
||||
first_line = self.lines[i].split(tag)[1]
|
||||
all_lines = [first_line] + self.lines[i + 1 : -1]
|
||||
break
|
||||
else:
|
||||
all_lines = self.lines[1:-1]
|
||||
|
||||
|
@ -18,7 +18,7 @@ import pytest
|
||||
|
||||
from trezorlib import btc, messages, tools
|
||||
from trezorlib.debuglink import TrezorClientDebugLink as Client
|
||||
from trezorlib.exceptions import TrezorFailure
|
||||
from trezorlib.exceptions import Cancelled, TrezorFailure
|
||||
|
||||
VECTORS = ( # path, script_type, address
|
||||
(
|
||||
@ -43,9 +43,12 @@ VECTORS = ( # path, script_type, address
|
||||
),
|
||||
)
|
||||
|
||||
CORNER_BUTTON = (215, 25)
|
||||
|
||||
|
||||
@pytest.mark.skip_t2
|
||||
@pytest.mark.parametrize("path, script_type, address", VECTORS)
|
||||
def test_show(
|
||||
def test_show_t1(
|
||||
client: Client, path: str, script_type: messages.InputScriptType, address: str
|
||||
):
|
||||
def input_flow():
|
||||
@ -68,6 +71,67 @@ def test_show(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_t1
|
||||
@pytest.mark.parametrize("path, script_type, address", VECTORS)
|
||||
def test_show_tt(
|
||||
client: Client, path: str, script_type: messages.InputScriptType, address: str
|
||||
):
|
||||
def input_flow():
|
||||
yield
|
||||
client.debug.click(CORNER_BUTTON, wait=True)
|
||||
yield
|
||||
client.debug.swipe_left(wait=True)
|
||||
client.debug.swipe_right(wait=True)
|
||||
client.debug.swipe_left(wait=True)
|
||||
client.debug.click(CORNER_BUTTON, wait=True)
|
||||
yield
|
||||
client.debug.press_no()
|
||||
yield
|
||||
client.debug.press_no()
|
||||
yield
|
||||
client.debug.press_yes()
|
||||
|
||||
with client:
|
||||
client.set_input_flow(input_flow)
|
||||
assert (
|
||||
btc.get_address(
|
||||
client,
|
||||
"Bitcoin",
|
||||
tools.parse_path(path),
|
||||
script_type=script_type,
|
||||
show_display=True,
|
||||
)
|
||||
== address
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_t1
|
||||
@pytest.mark.parametrize("path, script_type, address", VECTORS)
|
||||
def test_show_cancel(
|
||||
client: Client, path: str, script_type: messages.InputScriptType, address: str
|
||||
):
|
||||
def input_flow():
|
||||
yield
|
||||
client.debug.click(CORNER_BUTTON, wait=True)
|
||||
yield
|
||||
client.debug.swipe_left(wait=True)
|
||||
client.debug.click(CORNER_BUTTON, wait=True)
|
||||
yield
|
||||
client.debug.press_no()
|
||||
yield
|
||||
client.debug.press_yes()
|
||||
|
||||
with client, pytest.raises(Cancelled):
|
||||
client.set_input_flow(input_flow)
|
||||
btc.get_address(
|
||||
client,
|
||||
"Bitcoin",
|
||||
tools.parse_path(path),
|
||||
script_type=script_type,
|
||||
show_display=True,
|
||||
)
|
||||
|
||||
|
||||
def test_show_unrecognized_path(client: Client):
|
||||
with pytest.raises(TrezorFailure):
|
||||
btc.get_address(
|
||||
@ -213,32 +277,36 @@ def test_show_multisig_xpubs(
|
||||
def input_flow():
|
||||
yield # show address
|
||||
layout = client.debug.wait_layout() # TODO: do not need to *wait* now?
|
||||
assert layout.get_title() == "MULTISIG 2 OF 3"
|
||||
assert layout.get_title() == "RECEIVE ADDRESS (MULTISIG)"
|
||||
assert layout.get_content().replace(" ", "") == address
|
||||
|
||||
client.debug.press_no()
|
||||
client.debug.click(CORNER_BUTTON)
|
||||
yield # show QR code
|
||||
assert "Painter" in client.debug.wait_layout().text
|
||||
assert "Qr" in client.debug.wait_layout().text
|
||||
|
||||
client.debug.swipe_left()
|
||||
# address details
|
||||
layout = client.debug.wait_layout()
|
||||
assert "Multisig 2 of 3" in layout.text
|
||||
|
||||
# Three xpub pages with the same testing logic
|
||||
for xpub_num in range(3):
|
||||
expected_title = f"XPUB #{xpub_num + 1} " + (
|
||||
"(yours)" if i == xpub_num else "(cosigner)"
|
||||
expected_title = f"MULTISIG XPUB #{xpub_num + 1} " + (
|
||||
"(YOURS)" if i == xpub_num else "(COSIGNER)"
|
||||
)
|
||||
|
||||
client.debug.press_no()
|
||||
yield # show XPUB#{xpub_num}
|
||||
layout1 = client.debug.wait_layout()
|
||||
assert layout1.get_title() == expected_title
|
||||
client.debug.swipe_up()
|
||||
|
||||
layout2 = client.debug.wait_layout()
|
||||
assert layout2.get_title() == expected_title
|
||||
content = (layout1.get_content() + layout2.get_content()).replace(
|
||||
" ", ""
|
||||
)
|
||||
assert content == xpubs[xpub_num]
|
||||
client.debug.swipe_left()
|
||||
layout = client.debug.wait_layout()
|
||||
assert layout.get_title() == expected_title
|
||||
content = layout.get_content().replace(" ", "")
|
||||
assert xpubs[xpub_num] in content
|
||||
|
||||
client.debug.click(CORNER_BUTTON)
|
||||
yield # show address
|
||||
client.debug.press_no()
|
||||
yield # address mismatch
|
||||
client.debug.press_no()
|
||||
yield # show address
|
||||
client.debug.press_yes()
|
||||
|
||||
with client:
|
||||
|
@ -340,7 +340,7 @@ def test_signmessage_pagination(client: Client, message: str):
|
||||
expected_message = (
|
||||
("Confirm message: " + message).replace("\n", "").replace(" ", "")
|
||||
)
|
||||
message_read = message_read.replace(" ", "")
|
||||
message_read = message_read.replace(" ", "").replace("...", "")
|
||||
assert expected_message == message_read
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user