feat(core/ui): redesigned receive flow

[no changelog]
mmilata/touch-test
Martin Milata 1 year ago
parent 4a92ae00d4
commit 16cd2bf820

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

@ -40,6 +40,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;
@ -55,6 +56,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;
@ -106,6 +108,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;
@ -113,4 +116,5 @@ static void _librust_qstrs(void) {
MP_QSTR_bootscreen;
MP_QSTR_skip_first_paint;
MP_QSTR_wrong_pin;
MP_QSTR_xpubs;
}

@ -26,7 +26,7 @@ pub use maybe::Maybe;
pub use pad::Pad;
pub use paginated::{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,

@ -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();
}
}

@ -460,7 +460,7 @@ pub enum Alignment {
pub type Alignment2D = (Alignment, Alignment);
pub const TOP_LEFT: Alignment2D = (Alignment::Start, Alignment::Start);
pub const TOP_RIGHT: Alignment2D = (Alignment::Start, Alignment::End);
pub const TOP_RIGHT: Alignment2D = (Alignment::End, Alignment::Start);
pub const CENTER: Alignment2D = (Alignment::Center, Alignment::Center);
pub const BOTTOM_LEFT: Alignment2D = (Alignment::Start, Alignment::End);
pub const BOTTOM_RIGHT: Alignment2D = (Alignment::End, Alignment::End);

@ -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,
@ -33,7 +34,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(),
@ -44,15 +45,15 @@ 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_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))
}
@ -403,6 +404,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,
@ -520,6 +544,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,
@ -529,3 +556,83 @@ pub enum CancelInfoConfirmMsg {
pub enum SelectWordMsg {
Selected(usize),
}
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 new(icon: Icon, side: i16, border: i16, inner: T) -> Self {
Self {
inner,
button: Floating::top_right(side, border, 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 {

@ -0,0 +1,271 @@
use crate::{
error::Error,
ui::{
component::{base::ComponentExt, Component, Event, EventCtx, Never, Pad, Paginate, Qr},
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,
fade: Option<i32>,
}
impl<T> HorizontalPage<T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, background: Color) -> Self {
Self {
content,
scrollbar: ScrollBar::horizontal(),
swipe: Swipe::new(),
pad: Pad::with_background(background),
fade: None,
}
}
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();
}
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 = T::Msg;
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 => {
// Scroll down, if possible.
self.scrollbar.go_to_next_page();
self.on_page_change(ctx);
return None;
}
SwipeDirection::Right => {
// Scroll up, if possible.
self.scrollbar.go_to_previous_page();
self.on_page_change(ctx);
return None;
}
_ => {
// Ignore other directions.
}
}
}
self.content.event(ctx, event)
}
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();
}
}
use super::Frame;
use crate::ui::component::text::paragraphs::{
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
};
use heapless::Vec;
pub struct AddressDetails<T> {
qr_code: Frame<Qr, T>,
details: Frame<Paragraphs<ParagraphVecShort<T>>, T>,
xpubs: Vec<Frame<Paragraphs<Paragraph<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,
"\nDerivation 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(),
"RECEIVE ADDRESS".into(),
para.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(
Frame::left_aligned(
theme::label_title(),
title,
Paragraph::new(&theme::TEXT_XPUB, xpub).into_paragraphs(),
)
.with_border(theme::borders_horizontal_scroll()),
)
.map_err(|_| Error::OutOfRange)
}
}
impl<T> Paginate for AddressDetails<T> {
fn page_count(&mut self) -> usize {
2 + self.xpubs.len()
}
fn change_page(&mut self, to_page: usize) {
self.current_page = to_page
}
}
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);
for xpub in &mut self.xpubs {
xpub.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),
n => self.xpubs[n - 2].event(ctx, event),
}
}
fn paint(&mut self) {
match self.current_page {
0 => self.qr_code.paint(),
1 => self.details.paint(),
n => self.xpubs[n - 2].paint(),
}
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
match self.current_page {
0 => self.qr_code.bounds(sink),
1 => self.details.bounds(sink),
n => self.xpubs[n - 2].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),
n => self.xpubs[n - 2].trace(t),
}
t.close();
}
}

@ -6,6 +6,7 @@ mod fido_icons;
mod frame;
mod hold_to_confirm;
mod homescreen;
mod horizontal_page;
mod keyboard;
mod loader;
mod number_input;
@ -16,13 +17,14 @@ mod swipe;
pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg, SelectWordMsg,
CancelInfoConfirmMsg, FloatingButton, FloatingButtonMsg, SelectWordMsg,
};
pub use dialog::{Dialog, DialogMsg, IconDialog};
pub use fido::{FidoConfirm, FidoMsg};
pub use frame::{Frame, NotificationFrame};
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
pub use horizontal_page::{AddressDetails, HorizontalPage};
pub use keyboard::{
bip39::Bip39Input,
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},

@ -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,
@ -337,6 +338,36 @@ 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> {
self.inner().msg_try_into_obj(msg)
}
}
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()?;
@ -421,7 +452,22 @@ fn confirm_blob(
SwipePage::new(paragraphs, buttons, theme::BG),
))?
} else {
panic!("Either `hold=true` or `verb=Some(StrBuffer)` must be specified");
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,
);
LayoutObj::new(FloatingButton::new(
Icon::new(theme::ICON_INFO_CIRCLE),
32,
8,
Frame::left_aligned(
theme::label_title(),
title,
SwipePage::new(paragraphs, buttons, theme::BG),
),
))?
};
Ok(obj.into())
}
@ -433,23 +479,17 @@ 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,
Some(verb),
verb_cancel,
hold,
)
confirm_blob(title, data, description, extra, verb, verb_cancel, hold)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -582,6 +622,33 @@ 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::new(Icon::new(theme::ICON_CANCEL_LARGER), 32, 8, ad),
theme::BG,
))?;
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()?;
@ -846,6 +913,39 @@ 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(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], _kwargs: &Map| {
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::new(
Icon::new(theme::ICON_OCTA),
Icon::new(theme::ICON_OCTA),
theme::WARN_COLOR,
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_with_args_and_kwargs(n_args, args, kwargs, 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()?;
@ -1400,6 +1500,7 @@ 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:
@ -1434,6 +1535,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,
@ -1529,6 +1641,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_kw!(0, new_show_mismatch).as_obj(),
/// def show_simple(
/// *,
/// title: str | None,

@ -53,6 +53,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");
@ -65,6 +66,8 @@ pub const ICON_LIST_CHECK: &[u8] = include_res!("model_tt/res/check.toif");
pub const ICON_LOCK: &[u8] = include_res!("model_tt/res/lock.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;
@ -134,7 +137,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,
@ -166,7 +169,7 @@ pub fn button_default() -> ButtonStyleSheet {
}
}
pub fn button_confirm() -> ButtonStyleSheet {
pub const fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -198,7 +201,7 @@ pub fn button_confirm() -> ButtonStyleSheet {
}
}
pub fn button_cancel() -> ButtonStyleSheet {
pub const fn button_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -230,11 +233,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,
@ -266,7 +269,7 @@ pub fn button_reset() -> ButtonStyleSheet {
}
}
pub fn button_info() -> ButtonStyleSheet {
pub const fn button_info() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -298,7 +301,7 @@ pub fn button_info() -> ButtonStyleSheet {
}
}
pub fn button_pin() -> ButtonStyleSheet {
pub const fn button_pin() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
@ -330,7 +333,7 @@ pub fn button_pin() -> ButtonStyleSheet {
}
}
pub fn button_counter() -> ButtonStyleSheet {
pub const fn button_counter() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::DEMIBOLD,
@ -362,11 +365,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,
@ -409,6 +412,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,
@ -453,6 +457,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,6 +82,7 @@ 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:
@ -119,6 +120,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 +235,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,27 @@ 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)
account = (
f"{coin.coin_name} {account_name}" if account_name else "Unknown path"
)
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)

@ -923,9 +923,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 = f"{ADDRESS_TYPE_NAMES[address_parameters.address_type]}"
if address_parameters.address_type in (
CAT.BYRON,
CAT.BASE,
@ -935,17 +934,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)

@ -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,24 +379,24 @@ 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:
result = await interact(
ctx,
RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
# title=title.upper(),
title="RECEIVE ADDRESS",
data=address,
description=network or "",
extra=address_extra or "",
verb_cancel="QR",
verb=None,
extra=None,
)
),
"show_address",
@ -409,35 +405,41 @@ async def show_address(
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
if 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 title_fn(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=True,
account=account,
path=path,
xpubs=[(title_fn(i), xpub) for i, xpub in enumerate(xpubs)],
)
),
"show_address_details",
ButtonRequestType.Address,
)
assert result is INFO
else:
result = await interact(
ctx,
RustLayout(trezorui2.show_mismatch()),
"warning_address_mismatch",
ButtonRequestType.Warning,
)
if result is CONFIRMED:
raise ActionCancelled
else:
assert result is CANCELLED
continue
def show_pubkey(
@ -693,6 +695,7 @@ async def confirm_blob(
data=data,
extra=None,
hold=hold,
verb="CONFIRM",
)
)

@ -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)
client.debug.swipe_left()
client.debug.swipe_right()
client.debug.swipe_left()
client.debug.click(CORNER_BUTTON)
yield
client.debug.press_no()
yield
client.debug.press_no()
yield
client.debug.press_yes()
yield
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)
client.debug.swipe_left()
client.debug.click(CORNER_BUTTON)
yield
client.debug.press_no()
yield
client.debug.press_yes()
yield
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"
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…
Cancel
Save