mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-19 05:58:09 +00:00
fix(core/ui/mercury): FIDO2 layouts
This commit is contained in:
parent
167f567ab0
commit
7c8be6f0ea
1
core/.changelog.d/3797.fixed
Normal file
1
core/.changelog.d/3797.fixed
Normal file
@ -0,0 +1 @@
|
||||
[T3T1] Redesigned FIDO2 UI.
|
@ -301,6 +301,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_instructions__learn_more;
|
||||
MP_QSTR_instructions__shares_continue_with_x_template;
|
||||
MP_QSTR_instructions__shares_start_with_1;
|
||||
MP_QSTR_instructions__swipe_down;
|
||||
MP_QSTR_instructions__swipe_horizontally;
|
||||
MP_QSTR_instructions__swipe_up;
|
||||
MP_QSTR_instructions__tap_to_confirm;
|
||||
@ -1008,15 +1009,20 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_fido__does_not_belong;
|
||||
MP_QSTR_fido__erase_credentials;
|
||||
MP_QSTR_fido__export_credentials;
|
||||
MP_QSTR_fido__more_credentials;
|
||||
MP_QSTR_fido__not_registered;
|
||||
MP_QSTR_fido__not_registered_with_template;
|
||||
MP_QSTR_fido__please_enable_pin_protection;
|
||||
MP_QSTR_fido__select_intro;
|
||||
MP_QSTR_fido__title_authenticate;
|
||||
MP_QSTR_fido__title_credential_details;
|
||||
MP_QSTR_fido__title_for_authentication;
|
||||
MP_QSTR_fido__title_import_credential;
|
||||
MP_QSTR_fido__title_list_credentials;
|
||||
MP_QSTR_fido__title_register;
|
||||
MP_QSTR_fido__title_remove_credential;
|
||||
MP_QSTR_fido__title_reset;
|
||||
MP_QSTR_fido__title_select_credential;
|
||||
MP_QSTR_fido__title_u2f_auth;
|
||||
MP_QSTR_fido__title_u2f_register;
|
||||
MP_QSTR_fido__title_verify_user;
|
||||
|
@ -1358,6 +1358,17 @@ pub enum TranslatedString {
|
||||
reset__slip39_checklist_more_info_threshold = 957, // "The threshold sets the minumum number of shares needed to recover your wallet."
|
||||
reset__slip39_checklist_more_info_threshold_example_template = 958, // "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet."
|
||||
passphrase__continue_with_empty_passphrase = 959, // "Continue with empty passphrase?"
|
||||
#[cfg(feature = "universal_fw")]
|
||||
fido__more_credentials = 960, // "More credentials"
|
||||
#[cfg(feature = "universal_fw")]
|
||||
fido__select_intro = 961, // "Select the credential that you would like to use for authentication."
|
||||
#[cfg(feature = "universal_fw")]
|
||||
fido__title_for_authentication = 962, // "for authentication"
|
||||
#[cfg(feature = "universal_fw")]
|
||||
fido__title_select_credential = 963, // "Select credential"
|
||||
instructions__swipe_down = 964, // "Swipe down"
|
||||
#[cfg(feature = "universal_fw")]
|
||||
fido__title_credential_details = 965, // "Credential details"
|
||||
}
|
||||
|
||||
impl TranslatedString {
|
||||
@ -2710,6 +2721,17 @@ impl TranslatedString {
|
||||
Self::reset__slip39_checklist_more_info_threshold => "The threshold sets the minumum number of shares needed to recover your wallet.",
|
||||
Self::reset__slip39_checklist_more_info_threshold_example_template => "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet.",
|
||||
Self::passphrase__continue_with_empty_passphrase => "Continue with empty passphrase?",
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Self::fido__more_credentials => "More credentials",
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Self::fido__select_intro => "Select the credential that you would like to use for authentication.",
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Self::fido__title_for_authentication => "for authentication",
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Self::fido__title_select_credential => "Select credential",
|
||||
Self::instructions__swipe_down => "Swipe down",
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Self::fido__title_credential_details => "Credential details",
|
||||
}
|
||||
}
|
||||
|
||||
@ -4063,6 +4085,17 @@ impl TranslatedString {
|
||||
Qstr::MP_QSTR_reset__slip39_checklist_more_info_threshold => Some(Self::reset__slip39_checklist_more_info_threshold),
|
||||
Qstr::MP_QSTR_reset__slip39_checklist_more_info_threshold_example_template => Some(Self::reset__slip39_checklist_more_info_threshold_example_template),
|
||||
Qstr::MP_QSTR_passphrase__continue_with_empty_passphrase => Some(Self::passphrase__continue_with_empty_passphrase),
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Qstr::MP_QSTR_fido__more_credentials => Some(Self::fido__more_credentials),
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Qstr::MP_QSTR_fido__select_intro => Some(Self::fido__select_intro),
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Qstr::MP_QSTR_fido__title_for_authentication => Some(Self::fido__title_for_authentication),
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Qstr::MP_QSTR_fido__title_select_credential => Some(Self::fido__title_select_credential),
|
||||
Qstr::MP_QSTR_instructions__swipe_down => Some(Self::instructions__swipe_down),
|
||||
#[cfg(feature = "universal_fw")]
|
||||
Qstr::MP_QSTR_fido__title_credential_details => Some(Self::fido__title_credential_details),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,19 @@ where
|
||||
&mut self.source
|
||||
}
|
||||
|
||||
pub fn area(&self) -> Rect {
|
||||
let mut result: Option<Rect> = None;
|
||||
Self::foreach_visible(
|
||||
&self.source,
|
||||
&self.visible,
|
||||
self.offset,
|
||||
&mut |layout, _content| {
|
||||
result = result.map_or(Some(layout.bounds), |r| Some(r.union(layout.bounds)));
|
||||
},
|
||||
);
|
||||
result.unwrap_or(self.area)
|
||||
}
|
||||
|
||||
/// Update bounding boxes of paragraphs on the current page. First determine
|
||||
/// the number of visible paragraphs and their sizes. These are then
|
||||
/// arranged according to the layout.
|
||||
|
@ -4,9 +4,7 @@ use crate::{
|
||||
strutil::TString,
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{
|
||||
Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken,
|
||||
},
|
||||
component::{Component, Event, EventCtx, TimerToken},
|
||||
display::{self, toif::Icon, Color, Font},
|
||||
event::TouchEvent,
|
||||
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
|
||||
@ -139,10 +137,9 @@ impl Button {
|
||||
matches!(self.state, State::Disabled)
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, ctx: &mut EventCtx, content: ButtonContent) {
|
||||
pub fn set_content(&mut self, content: ButtonContent) {
|
||||
if self.content != content {
|
||||
self.content = content;
|
||||
ctx.request_paint();
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
@ -454,41 +451,6 @@ pub struct ButtonStyle {
|
||||
pub background_color: Color,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn cancel_confirm(
|
||||
left: Button,
|
||||
right: Button,
|
||||
left_is_small: bool,
|
||||
) -> CancelConfirm<
|
||||
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
|
||||
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
|
||||
> {
|
||||
let width = if left_is_small {
|
||||
theme::BUTTON_WIDTH
|
||||
} else {
|
||||
0
|
||||
};
|
||||
theme::button_bar(Split::left(
|
||||
width,
|
||||
theme::BUTTON_SPACING,
|
||||
left.map(|msg| {
|
||||
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
|
||||
}),
|
||||
right.map(|msg| {
|
||||
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum CancelConfirmMsg {
|
||||
Cancelled,
|
||||
Confirmed,
|
||||
}
|
||||
|
||||
type CancelConfirm<F0, F1> = FixedHeightBar<Split<MsgMap<Button, F0>, MsgMap<Button, F1>>>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CancelInfoConfirmMsg {
|
||||
Cancelled,
|
||||
|
@ -1,251 +1,88 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{image::Image, Component, Event, EventCtx, Label, Swipe, SwipeDirection},
|
||||
display,
|
||||
geometry::{Insets, Rect},
|
||||
model_mercury::component::{fido_icons::get_fido_icon_data, theme, ScrollBar},
|
||||
shape,
|
||||
component::{
|
||||
image::Image,
|
||||
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs},
|
||||
Component, Event, EventCtx,
|
||||
},
|
||||
geometry::{Insets, Offset, Rect},
|
||||
model_mercury::component::{fido_icons::get_fido_icon_data, theme},
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::CancelConfirmMsg;
|
||||
use core::cell::Cell;
|
||||
|
||||
const ICON_HEIGHT: i16 = 70;
|
||||
const SCROLLBAR_INSET_TOP: i16 = 5;
|
||||
const SCROLLBAR_HEIGHT: i16 = 10;
|
||||
const APP_NAME_PADDING: i16 = 12;
|
||||
const APP_NAME_HEIGHT: i16 = 30;
|
||||
|
||||
pub enum FidoMsg {
|
||||
Confirmed(usize),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
pub struct FidoConfirm<F: Fn(usize) -> TString<'static>, U> {
|
||||
page_swipe: Swipe,
|
||||
app_name: Label<'static>,
|
||||
account_name: Label<'static>,
|
||||
icon: Image,
|
||||
/// Function/closure that will return appropriate page on demand.
|
||||
pub struct FidoCredential<F: Fn() -> TString<'static>> {
|
||||
app_icon: Option<Image>,
|
||||
text: Paragraphs<ParagraphVecShort<'static>>,
|
||||
get_account: F,
|
||||
scrollbar: ScrollBar,
|
||||
fade: Cell<bool>,
|
||||
controls: U,
|
||||
}
|
||||
|
||||
impl<F, U> FidoConfirm<F, U>
|
||||
where
|
||||
F: Fn(usize) -> TString<'static>,
|
||||
U: Component<Msg = CancelConfirmMsg>,
|
||||
{
|
||||
impl<F: Fn() -> TString<'static>> FidoCredential<F> {
|
||||
const ICON_SIZE: i16 = 32;
|
||||
const SPACING: i16 = 8;
|
||||
|
||||
pub fn new(
|
||||
icon_name: Option<TString<'static>>,
|
||||
app_name: TString<'static>,
|
||||
get_account: F,
|
||||
page_count: usize,
|
||||
icon_name: Option<TString<'static>>,
|
||||
controls: U,
|
||||
) -> Self {
|
||||
let icon_data = get_fido_icon_data(icon_name);
|
||||
|
||||
// Preparing scrollbar and setting its page-count.
|
||||
let mut scrollbar = ScrollBar::horizontal();
|
||||
scrollbar.set_count_and_active_page(page_count, 0);
|
||||
|
||||
// Preparing swipe component and setting possible initial
|
||||
// swipe directions according to number of pages.
|
||||
let mut page_swipe = Swipe::horizontal();
|
||||
page_swipe.allow_right = scrollbar.has_previous_page();
|
||||
page_swipe.allow_left = scrollbar.has_next_page();
|
||||
|
||||
// NOTE: This is an ugly hotfix for the erroneous behavior of
|
||||
// TextLayout used in the account_name Label. In this
|
||||
// particular case, TextLayout calculates the wrong height of
|
||||
// fitted text that's higher than the TextLayout bound itself.
|
||||
//
|
||||
// The following two lines should be swapped when the problem with
|
||||
// TextLayout is fixed.
|
||||
//
|
||||
// See also, continuation of this hotfix in the place() function.
|
||||
|
||||
// let current_account = get_account(scrollbar.active_page);
|
||||
let current_account = "".into();
|
||||
|
||||
let app_icon = get_fido_icon_data(icon_name).map(Image::new);
|
||||
let text = ParagraphVecShort::from_iter([
|
||||
Paragraph::new(&theme::TEXT_SUB_GREY, app_name),
|
||||
Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, (get_account)()),
|
||||
])
|
||||
.into_paragraphs();
|
||||
Self {
|
||||
app_name: Label::centered(app_name, theme::TEXT_DEMIBOLD),
|
||||
account_name: Label::centered(current_account, theme::TEXT_DEMIBOLD),
|
||||
page_swipe,
|
||||
icon: Image::new(icon_data),
|
||||
app_icon,
|
||||
text,
|
||||
get_account,
|
||||
scrollbar,
|
||||
fade: Cell::new(false),
|
||||
controls,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_page_swipe(&mut self, ctx: &mut EventCtx, swipe: SwipeDirection) {
|
||||
// Change the page number.
|
||||
match swipe {
|
||||
SwipeDirection::Left if self.scrollbar.has_next_page() => {
|
||||
self.scrollbar.go_to_next_page();
|
||||
}
|
||||
SwipeDirection::Right if self.scrollbar.has_previous_page() => {
|
||||
self.scrollbar.go_to_previous_page();
|
||||
}
|
||||
_ => {} // page did not change
|
||||
};
|
||||
|
||||
// Disable swipes on the boundaries. Not allowing carousel effect.
|
||||
self.page_swipe.allow_right = self.scrollbar.has_previous_page();
|
||||
self.page_swipe.allow_left = self.scrollbar.has_next_page();
|
||||
|
||||
let current_account = (self.get_account)(self.active_page());
|
||||
self.account_name.set_text(current_account);
|
||||
|
||||
// Redraw the page.
|
||||
ctx.request_paint();
|
||||
|
||||
// Reset backlight to normal level on next paint.
|
||||
self.fade.set(true);
|
||||
}
|
||||
|
||||
fn active_page(&self) -> usize {
|
||||
self.scrollbar.active_page
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, U> Component for FidoConfirm<F, U>
|
||||
where
|
||||
F: Fn(usize) -> TString<'static>,
|
||||
U: Component<Msg = CancelConfirmMsg>,
|
||||
{
|
||||
type Msg = FidoMsg;
|
||||
impl<F: Fn() -> TString<'static>> Component for FidoCredential<F> {
|
||||
type Msg = ();
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.page_swipe.place(bounds);
|
||||
|
||||
// Place the control buttons.
|
||||
let controls_area = self.controls.place(bounds);
|
||||
|
||||
// Get the image and content areas.
|
||||
let content_area = bounds.inset(Insets::bottom(controls_area.height()));
|
||||
let (image_area, content_area) = content_area.split_top(ICON_HEIGHT);
|
||||
|
||||
// In case of showing a scrollbar, getting its area and placing it.
|
||||
let remaining_area = if self.scrollbar.page_count > 1 {
|
||||
let (scrollbar_area, remaining_area) = content_area
|
||||
.inset(Insets::top(SCROLLBAR_INSET_TOP))
|
||||
.split_top(SCROLLBAR_HEIGHT);
|
||||
self.scrollbar.place(scrollbar_area);
|
||||
remaining_area
|
||||
} else {
|
||||
content_area
|
||||
};
|
||||
|
||||
// Place the icon image.
|
||||
self.icon.place(image_area);
|
||||
|
||||
// Place the text labels.
|
||||
let (app_name_area, account_name_area) = remaining_area
|
||||
.inset(Insets::top(APP_NAME_PADDING))
|
||||
.split_top(APP_NAME_HEIGHT);
|
||||
|
||||
self.app_name.place(app_name_area);
|
||||
self.account_name.place(account_name_area);
|
||||
|
||||
// NOTE: This is a hotfix used due to the erroneous behavior of TextLayout.
|
||||
// This line should be removed when the problem with TextLayout is fixed.
|
||||
// See also the code for FidoConfirm::new().
|
||||
self.account_name
|
||||
.set_text((self.get_account)(self.scrollbar.active_page));
|
||||
let icon_size = self.app_icon.map_or(Offset::zero(), |i| i.toif.size());
|
||||
let (icon_area, text_area) = bounds.split_top(icon_size.y);
|
||||
let text_area = text_area.inset(Insets::top(Self::SPACING));
|
||||
self.text.place(text_area);
|
||||
let text_height = self.text.area().height();
|
||||
let vertical_space = bounds.height() - icon_size.y - Self::SPACING - text_height;
|
||||
let off = Offset::y(vertical_space / 2);
|
||||
|
||||
let icon_area = icon_area.with_width(icon_size.x).translate(off);
|
||||
let text_area = text_area.with_height(text_height).translate(off);
|
||||
self.app_icon.place(icon_area);
|
||||
self.text.place(text_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||
// Swipe encountered, update the page.
|
||||
self.on_page_swipe(ctx, swipe);
|
||||
}
|
||||
if let Some(msg) = self.controls.event(ctx, event) {
|
||||
// Some button was clicked, send results.
|
||||
match msg {
|
||||
CancelConfirmMsg::Confirmed => return Some(FidoMsg::Confirmed(self.active_page())),
|
||||
CancelConfirmMsg::Cancelled => return Some(FidoMsg::Cancelled),
|
||||
}
|
||||
if let Event::Attach(_) = event {
|
||||
self.text.inner_mut()[1].update((self.get_account)());
|
||||
ctx.request_paint();
|
||||
}
|
||||
self.app_icon.event(ctx, event);
|
||||
self.text.event(ctx, event);
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.icon.paint();
|
||||
self.controls.paint();
|
||||
self.app_name.paint();
|
||||
|
||||
if self.scrollbar.page_count > 1 {
|
||||
self.scrollbar.paint();
|
||||
}
|
||||
|
||||
// Erasing the old text content before writing the new one.
|
||||
let account_name_area = self.account_name.area();
|
||||
let real_area = account_name_area
|
||||
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
|
||||
display::rect_fill(real_area, theme::BG);
|
||||
|
||||
// Account name is optional.
|
||||
// Showing it only if it differs from app name.
|
||||
// (Dummy requests usually have some text as both app_name and account_name.)
|
||||
let account_name = self.account_name.text();
|
||||
let app_name = self.app_name.text();
|
||||
if !account_name.is_empty() && account_name != app_name {
|
||||
self.account_name.paint();
|
||||
}
|
||||
|
||||
if self.fade.take() {
|
||||
// Note that this is blocking and takes some time.
|
||||
display::fade_backlight(theme::backlight::get_backlight_normal());
|
||||
}
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
self.icon.render(target);
|
||||
self.controls.render(target);
|
||||
self.app_name.render(target);
|
||||
|
||||
if self.scrollbar.page_count > 1 {
|
||||
self.scrollbar.render(target);
|
||||
}
|
||||
|
||||
// Erasing the old text content before writing the new one.
|
||||
let account_name_area = self.account_name.area();
|
||||
let real_area = account_name_area
|
||||
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
|
||||
shape::Bar::new(real_area).with_bg(theme::BG).render(target);
|
||||
|
||||
// Account name is optional.
|
||||
// Showing it only if it differs from app name.
|
||||
// (Dummy requests usually have some text as both app_name and account_name.)
|
||||
let account_name = self.account_name.text();
|
||||
let app_name = self.app_name.text();
|
||||
if !account_name.is_empty() && account_name != app_name {
|
||||
self.account_name.render(target);
|
||||
}
|
||||
|
||||
if self.fade.take() {
|
||||
// Note that this is blocking and takes some time.
|
||||
display::fade_backlight(theme::backlight::get_backlight_normal());
|
||||
}
|
||||
self.app_icon.render(target);
|
||||
self.text.render(target);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, T> crate::trace::Trace for FidoConfirm<F, T>
|
||||
where
|
||||
F: Fn(usize) -> TString<'static>,
|
||||
{
|
||||
impl<F: Fn() -> TString<'static>> crate::trace::Trace for FidoCredential<F> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("FidoConfirm");
|
||||
t.component("FidoCredential");
|
||||
}
|
||||
}
|
||||
|
@ -37,48 +37,44 @@ const ICON_PROTON: &[u8] = include_res!("model_mercury/res/fido/icon_proton.toif
|
||||
const ICON_SLUSHPOOL: &[u8] = include_res!("model_mercury/res/fido/icon_slushpool.toif");
|
||||
const ICON_STRIPE: &[u8] = include_res!("model_mercury/res/fido/icon_stripe.toif");
|
||||
const ICON_TUTANOTA: &[u8] = include_res!("model_mercury/res/fido/icon_tutanota.toif");
|
||||
/// Default icon when app does not have its own
|
||||
const ICON_WEBAUTHN: &[u8] = include_res!("model_mercury/res/fido/icon_webauthn.toif");
|
||||
|
||||
/// Translates icon name into its data.
|
||||
/// Returns default `ICON_WEBAUTHN` when the icon is not found or name not
|
||||
/// supplied.
|
||||
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> &'static [u8] {
|
||||
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> Option< &'static [u8]> {
|
||||
if let Some(icon_name) = icon_name {
|
||||
icon_name.map(|c| match c {
|
||||
"apple" => ICON_APPLE,
|
||||
"aws" => ICON_AWS,
|
||||
"binance" => ICON_BINANCE,
|
||||
"bitbucket" => ICON_BITBUCKET,
|
||||
"bitfinex" => ICON_BITFINEX,
|
||||
"bitwarden" => ICON_BITWARDEN,
|
||||
"cloudflare" => ICON_CLOUDFLARE,
|
||||
"coinbase" => ICON_COINBASE,
|
||||
"dashlane" => ICON_DASHLANE,
|
||||
"dropbox" => ICON_DROPBOX,
|
||||
"duo" => ICON_DUO,
|
||||
"facebook" => ICON_FACEBOOK,
|
||||
"fastmail" => ICON_FASTMAIL,
|
||||
"fedora" => ICON_FEDORA,
|
||||
"gandi" => ICON_GANDI,
|
||||
"gemini" => ICON_GEMINI,
|
||||
"github" => ICON_GITHUB,
|
||||
"gitlab" => ICON_GITLAB,
|
||||
"google" => ICON_GOOGLE,
|
||||
"invity" => ICON_INVITY,
|
||||
"keeper" => ICON_KEEPER,
|
||||
"kraken" => ICON_KRAKEN,
|
||||
"login.gov" => ICON_LOGIN_GOV,
|
||||
"microsoft" => ICON_MICROSOFT,
|
||||
"mojeid" => ICON_MOJEID,
|
||||
"namecheap" => ICON_NAMECHEAP,
|
||||
"proton" => ICON_PROTON,
|
||||
"slushpool" => ICON_SLUSHPOOL,
|
||||
"stripe" => ICON_STRIPE,
|
||||
"tutanota" => ICON_TUTANOTA,
|
||||
_ => ICON_WEBAUTHN,
|
||||
"apple" => Some(ICON_APPLE),
|
||||
"aws" => Some(ICON_AWS),
|
||||
"binance" => Some(ICON_BINANCE),
|
||||
"bitbucket" => Some(ICON_BITBUCKET),
|
||||
"bitfinex" => Some(ICON_BITFINEX),
|
||||
"bitwarden" => Some(ICON_BITWARDEN),
|
||||
"cloudflare" => Some(ICON_CLOUDFLARE),
|
||||
"coinbase" => Some(ICON_COINBASE),
|
||||
"dashlane" => Some(ICON_DASHLANE),
|
||||
"dropbox" => Some(ICON_DROPBOX),
|
||||
"duo" => Some(ICON_DUO),
|
||||
"facebook" => Some(ICON_FACEBOOK),
|
||||
"fastmail" => Some(ICON_FASTMAIL),
|
||||
"fedora" => Some(ICON_FEDORA),
|
||||
"gandi" => Some(ICON_GANDI),
|
||||
"gemini" => Some(ICON_GEMINI),
|
||||
"github" => Some(ICON_GITHUB),
|
||||
"gitlab" => Some(ICON_GITLAB),
|
||||
"google" => Some(ICON_GOOGLE),
|
||||
"invity" => Some(ICON_INVITY),
|
||||
"keeper" => Some(ICON_KEEPER),
|
||||
"kraken" => Some(ICON_KRAKEN),
|
||||
"login.gov" => Some(ICON_LOGIN_GOV),
|
||||
"microsoft" => Some(ICON_MICROSOFT),
|
||||
"mojeid" => Some(ICON_MOJEID),
|
||||
"namecheap" => Some(ICON_NAMECHEAP),
|
||||
"proton" => Some(ICON_PROTON),
|
||||
"slushpool" => Some(ICON_SLUSHPOOL),
|
||||
"stripe" => Some(ICON_STRIPE),
|
||||
"tutanota" => Some(ICON_TUTANOTA),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
ICON_WEBAUTHN
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -19,21 +19,17 @@ for app in fido:
|
||||
% for icon_name, var_name in icons:
|
||||
const ICON_${var_name}: &[u8] = include_res!("model_mercury/res/fido/icon_${icon_name}.toif");
|
||||
% endfor
|
||||
/// Default icon when app does not have its own
|
||||
const ICON_WEBAUTHN: &[u8] = include_res!("model_mercury/res/fido/icon_webauthn.toif");
|
||||
|
||||
/// Translates icon name into its data.
|
||||
/// Returns default `ICON_WEBAUTHN` when the icon is not found or name not
|
||||
/// supplied.
|
||||
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> &'static [u8] {
|
||||
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> Option< &'static [u8]> {
|
||||
if let Some(icon_name) = icon_name {
|
||||
icon_name.map(|c| match c {
|
||||
% for icon_name, var_name in icons:
|
||||
"${icon_name}" => ICON_${var_name},
|
||||
"${icon_name}" => Some(ICON_${var_name}),
|
||||
% endfor
|
||||
_ => ICON_WEBAUTHN,
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
ICON_WEBAUTHN
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -198,14 +198,14 @@ impl PassphraseKeyboard {
|
||||
}
|
||||
|
||||
fn replace_keys_contents(&mut self, ctx: &mut EventCtx) {
|
||||
self.next_btn
|
||||
.set_content(ctx, self.active_layout.next().into());
|
||||
self.next_btn.set_content(self.active_layout.next().into());
|
||||
for (i, btn) in self.keys.iter_mut().enumerate() {
|
||||
let text = KEYBOARD[self.active_layout.to_usize().unwrap()][i];
|
||||
let content = Self::key_content(text);
|
||||
btn.set_content(ctx, content);
|
||||
btn.set_content(content);
|
||||
btn.request_complete_repaint(ctx);
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Possibly changing the buttons' state after change of the input.
|
||||
|
@ -288,12 +288,13 @@ impl Slip39Input {
|
||||
// Confirm button.
|
||||
self.button.enable(ctx);
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_SIMPLE_CHECKMARK24));
|
||||
.set_content(ButtonContent::Icon(theme::ICON_SIMPLE_CHECKMARK24));
|
||||
} else {
|
||||
// Disabled button.
|
||||
self.button.disable(ctx);
|
||||
self.button.set_content(ctx, ButtonContent::Text("".into()));
|
||||
self.button.set_content(ButtonContent::Text("".into()));
|
||||
}
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn input_sequence(&self) -> Option<u16> {
|
||||
|
@ -46,13 +46,12 @@ pub use address_details::AddressDetails;
|
||||
#[cfg(feature = "ui_overlay")]
|
||||
pub use binary_selection::{BinarySelection, BinarySelectionMsg};
|
||||
pub use button::{
|
||||
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
|
||||
CancelInfoConfirmMsg, IconText,
|
||||
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelInfoConfirmMsg, IconText,
|
||||
};
|
||||
#[cfg(feature = "translations")]
|
||||
pub use coinjoin_progress::CoinJoinProgress;
|
||||
pub use error::ErrorScreen;
|
||||
pub use fido::{FidoConfirm, FidoMsg};
|
||||
pub use fido::FidoCredential;
|
||||
pub use footer::Footer;
|
||||
pub use frame::{Frame, FrameMsg};
|
||||
pub use header::Header;
|
||||
@ -88,7 +87,7 @@ pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg};
|
||||
#[cfg(feature = "translations")]
|
||||
pub use tap_to_confirm::TapToConfirm;
|
||||
pub use updatable_more_info::UpdatableMoreInfo;
|
||||
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
|
||||
pub use vertical_menu::{PagedVerticalMenu, VerticalMenu, VerticalMenuChoiceMsg};
|
||||
pub use welcome_screen::WelcomeScreen;
|
||||
|
||||
use super::{constant, theme};
|
||||
|
@ -7,13 +7,13 @@ use crate::{
|
||||
ui::{
|
||||
component::{
|
||||
base::{AttachType, Component},
|
||||
Event, EventCtx, SwipeDirection,
|
||||
Event, EventCtx, Paginate, SwipeDirection,
|
||||
},
|
||||
constant::screen,
|
||||
display::{Color, Icon},
|
||||
geometry::{Offset, Rect},
|
||||
lerp::Lerp,
|
||||
model_mercury::component::button::{Button, ButtonMsg, IconText},
|
||||
model_mercury::component::button::{Button, ButtonContent, ButtonMsg, IconText},
|
||||
shape::{Bar, Renderer},
|
||||
util::animation_disabled,
|
||||
},
|
||||
@ -25,11 +25,7 @@ pub enum VerticalMenuChoiceMsg {
|
||||
|
||||
/// Number of buttons.
|
||||
/// Presently, VerticalMenu holds only fixed number of buttons.
|
||||
/// TODO: for scrollable menu, the implementation must change.
|
||||
const N_ITEMS: usize = 3;
|
||||
|
||||
/// Number of visual separators between buttons.
|
||||
const N_SEPS: usize = N_ITEMS - 1;
|
||||
const MAX_ITEMS: usize = 3;
|
||||
|
||||
/// Fixed height of each menu button.
|
||||
const MENU_BUTTON_HEIGHT: i16 = 64;
|
||||
@ -37,8 +33,7 @@ const MENU_BUTTON_HEIGHT: i16 = 64;
|
||||
/// Fixed height of a separator.
|
||||
const MENU_SEP_HEIGHT: i16 = 2;
|
||||
|
||||
type VerticalMenuButtons = Vec<Button, N_ITEMS>;
|
||||
type AreasForSeparators = Vec<Rect, N_SEPS>;
|
||||
type VerticalMenuButtons = Vec<Button, MAX_ITEMS>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct AttachAnimation {
|
||||
@ -174,11 +169,10 @@ impl AttachAnimation {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VerticalMenu {
|
||||
area: Rect,
|
||||
/// buttons placed vertically from top to bottom
|
||||
buttons: VerticalMenuButtons,
|
||||
/// areas for visual separators between buttons
|
||||
areas_sep: AreasForSeparators,
|
||||
/// length of `buttons` prefix that is currently active, set by `place()`
|
||||
n_items: usize,
|
||||
|
||||
attach_animation: AttachAnimation,
|
||||
}
|
||||
@ -186,9 +180,8 @@ pub struct VerticalMenu {
|
||||
impl VerticalMenu {
|
||||
fn new(buttons: VerticalMenuButtons) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
buttons,
|
||||
areas_sep: AreasForSeparators::new(),
|
||||
n_items: MAX_ITEMS,
|
||||
attach_animation: AttachAnimation::default(),
|
||||
}
|
||||
}
|
||||
@ -226,26 +219,23 @@ impl Component for VerticalMenu {
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// VerticalMenu is supposed to be used in Frame, the remaining space is just
|
||||
// enought to fit 3 buttons separated by thin bars
|
||||
let height_bounds_expected = 3 * MENU_BUTTON_HEIGHT + 2 * MENU_SEP_HEIGHT;
|
||||
assert!(bounds.height() == height_bounds_expected);
|
||||
// enought to fit 3 buttons separated by thin bars. If there's footer only 2
|
||||
// buttons fit.
|
||||
let n_items = (bounds.height() + MENU_SEP_HEIGHT) / (MENU_BUTTON_HEIGHT + MENU_SEP_HEIGHT);
|
||||
self.n_items = n_items as usize;
|
||||
|
||||
self.area = bounds;
|
||||
self.areas_sep.clear();
|
||||
let mut remaining = bounds;
|
||||
let n_seps = self.buttons.len() - 1;
|
||||
for (i, button) in self.buttons.iter_mut().enumerate() {
|
||||
for (i, button) in self.buttons.iter_mut().take(self.n_items).enumerate() {
|
||||
let (area_button, new_remaining) = remaining.split_top(MENU_BUTTON_HEIGHT);
|
||||
button.place(area_button);
|
||||
remaining = new_remaining;
|
||||
if i < n_seps {
|
||||
let (area_sep, new_remaining) = remaining.split_top(MENU_SEP_HEIGHT);
|
||||
unwrap!(self.areas_sep.push(area_sep));
|
||||
let (_area_sep, new_remaining) = remaining.split_top(MENU_SEP_HEIGHT);
|
||||
remaining = new_remaining;
|
||||
}
|
||||
}
|
||||
|
||||
self.area
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
@ -278,7 +268,7 @@ impl Component for VerticalMenu {
|
||||
|
||||
target.with_origin(offset, &|target| {
|
||||
// render buttons separated by thin bars
|
||||
for (i, button) in (&self.buttons).into_iter().enumerate() {
|
||||
for (i, button) in (&self.buttons).into_iter().take(self.n_items).enumerate() {
|
||||
button.render(target);
|
||||
|
||||
Bar::new(button.area())
|
||||
@ -286,18 +276,22 @@ impl Component for VerticalMenu {
|
||||
.with_bg(Color::black())
|
||||
.with_alpha(opacities[i])
|
||||
.render(target);
|
||||
}
|
||||
for (i, area) in self.areas_sep.iter().enumerate() {
|
||||
Bar::new(*area)
|
||||
.with_thickness(MENU_SEP_HEIGHT)
|
||||
.with_fg(theme::GREY_EXTRA_DARK)
|
||||
.render(target);
|
||||
|
||||
Bar::new(*area)
|
||||
.with_fg(Color::black())
|
||||
.with_bg(Color::black())
|
||||
.with_alpha(opacities[i])
|
||||
.render(target);
|
||||
if i + 1 < self.buttons.len().min(self.n_items) {
|
||||
let area = button
|
||||
.area()
|
||||
.translate(Offset::y(MENU_BUTTON_HEIGHT))
|
||||
.with_height(MENU_SEP_HEIGHT);
|
||||
Bar::new(area)
|
||||
.with_thickness(MENU_SEP_HEIGHT)
|
||||
.with_fg(theme::GREY_EXTRA_DARK)
|
||||
.render(target);
|
||||
Bar::new(area)
|
||||
.with_fg(Color::black())
|
||||
.with_bg(Color::black())
|
||||
.with_alpha(opacities[i])
|
||||
.render(target);
|
||||
}
|
||||
}
|
||||
|
||||
// todo screen here is incorrect
|
||||
@ -322,3 +316,90 @@ impl crate::trace::Trace for VerticalMenu {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Polymorphic struct, avoid adding code as it gets duplicated, prefer
|
||||
// extending VerticalMenu instead.
|
||||
pub struct PagedVerticalMenu<F: Fn(usize) -> TString<'static>> {
|
||||
inner: VerticalMenu,
|
||||
page: usize,
|
||||
item_count: usize,
|
||||
label_fn: F,
|
||||
}
|
||||
|
||||
impl<F: Fn(usize) -> TString<'static>> PagedVerticalMenu<F> {
|
||||
pub fn new(item_count: usize, label_fn: F) -> Self {
|
||||
let mut result = Self {
|
||||
inner: VerticalMenu::select_word(["".into(), "".into(), "".into()]),
|
||||
page: 0,
|
||||
item_count,
|
||||
label_fn,
|
||||
};
|
||||
result.change_page(0);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(usize) -> TString<'static>> Paginate for PagedVerticalMenu<F> {
|
||||
fn page_count(&mut self) -> usize {
|
||||
self.num_pages()
|
||||
}
|
||||
|
||||
fn change_page(&mut self, active_page: usize) {
|
||||
for b in 0..self.inner.n_items {
|
||||
let i = active_page * self.inner.n_items + b;
|
||||
let text = if i < self.item_count {
|
||||
(self.label_fn)(i)
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
let mut dummy_ctx = EventCtx::new();
|
||||
self.inner.buttons[b].enable_if(&mut dummy_ctx, !text.is_empty());
|
||||
self.inner.buttons[b].set_content(ButtonContent::Text(text));
|
||||
}
|
||||
|
||||
self.page = active_page
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(usize) -> TString<'static>> Component for PagedVerticalMenu<F> {
|
||||
type Msg = VerticalMenuChoiceMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.inner.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let msg = self.inner.event(ctx, event);
|
||||
if let Some(VerticalMenuChoiceMsg::Selected(i)) = msg {
|
||||
return Some(VerticalMenuChoiceMsg::Selected(
|
||||
self.inner.n_items * self.page + i,
|
||||
));
|
||||
}
|
||||
msg
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// TODO remove when ui-t3t1 done
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
self.inner.render(target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(usize) -> TString<'static>> InternallySwipable for PagedVerticalMenu<F> {
|
||||
fn current_page(&self) -> usize {
|
||||
self.page
|
||||
}
|
||||
|
||||
fn num_pages(&self) -> usize {
|
||||
(self.item_count / self.inner.n_items) + (self.item_count % self.inner.n_items).min(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F: Fn(usize) -> TString<'static>> crate::trace::Trace for PagedVerticalMenu<F> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
self.inner.trace(t)
|
||||
}
|
||||
}
|
||||
|
202
core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs
Normal file
202
core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use crate::{
|
||||
error,
|
||||
micropython::{gc::Gc, list::List, map::Map, obj::Obj, qstr::Qstr, util},
|
||||
strutil::TString,
|
||||
translations::TR,
|
||||
ui::{
|
||||
component::{
|
||||
swipe_detect::SwipeSettings,
|
||||
text::paragraphs::{Paragraph, Paragraphs},
|
||||
ComponentExt, SwipeDirection,
|
||||
},
|
||||
flow::{
|
||||
base::{DecisionBuilder as _, StateChange},
|
||||
FlowMsg, FlowState, SwipeFlow, SwipePage,
|
||||
},
|
||||
layout::obj::LayoutObj,
|
||||
model_mercury::component::{FidoCredential, SwipeContent},
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
component::{
|
||||
Frame, FrameMsg, PagedVerticalMenu, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg,
|
||||
},
|
||||
theme,
|
||||
};
|
||||
|
||||
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ConfirmFido {
|
||||
Intro,
|
||||
ChooseCredential,
|
||||
Details,
|
||||
Tap,
|
||||
Menu,
|
||||
}
|
||||
|
||||
static CRED_SELECTED: AtomicUsize = AtomicUsize::new(0);
|
||||
static SINGLE_CRED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
impl FlowState for ConfirmFido {
|
||||
#[inline]
|
||||
fn index(&'static self) -> usize {
|
||||
*self as usize
|
||||
}
|
||||
|
||||
fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange {
|
||||
match (self, direction) {
|
||||
(Self::Intro, SwipeDirection::Left) => Self::Menu.swipe(direction),
|
||||
(Self::Intro, SwipeDirection::Up) => Self::ChooseCredential.swipe(direction),
|
||||
(Self::Details, SwipeDirection::Up) => Self::Tap.swipe(direction),
|
||||
(Self::Tap, SwipeDirection::Down) => Self::Details.swipe(direction),
|
||||
_ => self.do_nothing(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&'static self, msg: FlowMsg) -> StateChange {
|
||||
match (self, msg) {
|
||||
(_, FlowMsg::Info) => Self::Menu.transit(),
|
||||
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled),
|
||||
(Self::Menu, FlowMsg::Cancelled) => {
|
||||
if Self::single_cred() {
|
||||
Self::Details.swipe_right()
|
||||
} else {
|
||||
Self::Intro.swipe_right()
|
||||
}
|
||||
}
|
||||
(Self::ChooseCredential, FlowMsg::Choice(i)) => {
|
||||
CRED_SELECTED.store(i, Ordering::Relaxed);
|
||||
Self::Details.swipe_left()
|
||||
}
|
||||
(Self::Details, FlowMsg::Cancelled) => Self::ChooseCredential.swipe_right(),
|
||||
(Self::Tap, FlowMsg::Confirmed) => {
|
||||
self.return_msg(FlowMsg::Choice(CRED_SELECTED.load(Ordering::Relaxed)))
|
||||
}
|
||||
_ => self.do_nothing(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
pub extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ConfirmFido::new_obj) }
|
||||
}
|
||||
|
||||
impl ConfirmFido {
|
||||
const EXTRA_PADDING: i16 = 6;
|
||||
|
||||
fn single_cred() -> bool {
|
||||
SINGLE_CRED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
|
||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let app_name: TString = kwargs.get(Qstr::MP_QSTR_app_name)?.try_into()?;
|
||||
let icon_name: Option<TString> = kwargs.get(Qstr::MP_QSTR_icon_name)?.try_into_option()?;
|
||||
let accounts: Gc<List> = kwargs.get(Qstr::MP_QSTR_accounts)?.try_into()?;
|
||||
let num_accounts = accounts.len();
|
||||
SINGLE_CRED.store(num_accounts <= 1, Ordering::Relaxed);
|
||||
CRED_SELECTED.store(0, Ordering::Relaxed);
|
||||
|
||||
let content_intro = Frame::left_aligned(
|
||||
title,
|
||||
SwipeContent::new(Paragraphs::new(Paragraph::new::<TString>(
|
||||
&theme::TEXT_MAIN_GREY_LIGHT,
|
||||
TR::fido__select_intro.into(),
|
||||
))),
|
||||
)
|
||||
.with_menu_button()
|
||||
.with_footer(TR::instructions__swipe_up.into(), None)
|
||||
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
|
||||
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
|
||||
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
|
||||
|
||||
// Closure to lazy-load the information on given page index.
|
||||
// Done like this to allow arbitrarily many pages without
|
||||
// the need of any allocation here in Rust.
|
||||
let label_fn = move |page_index| {
|
||||
let account = unwrap!(accounts.get(page_index));
|
||||
account
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| TString::from_str("-"))
|
||||
};
|
||||
let content_choose_credential = Frame::left_aligned(
|
||||
TR::fido__title_select_credential.into(),
|
||||
SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new(
|
||||
num_accounts,
|
||||
label_fn,
|
||||
))),
|
||||
)
|
||||
.with_subtitle(TR::fido__title_for_authentication.into())
|
||||
.with_menu_button()
|
||||
.with_footer(
|
||||
TR::instructions__swipe_up.into(),
|
||||
(num_accounts > 2).then_some(TR::fido__more_credentials.into()),
|
||||
)
|
||||
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
|
||||
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
|
||||
.with_vertical_pages()
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Button(_) => Some(FlowMsg::Info),
|
||||
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
|
||||
});
|
||||
|
||||
let get_account = move || {
|
||||
let current = CRED_SELECTED.load(Ordering::Relaxed);
|
||||
let account = unwrap!(accounts.get(current));
|
||||
account.try_into().unwrap_or_else(|_| TString::from_str(""))
|
||||
};
|
||||
let content_details = Frame::left_aligned(
|
||||
TR::fido__title_credential_details.into(),
|
||||
SwipeContent::new(FidoCredential::new(icon_name, app_name, get_account)),
|
||||
)
|
||||
.with_footer(TR::instructions__swipe_up.into(), Some(title))
|
||||
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
|
||||
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate());
|
||||
let content_details = if Self::single_cred() {
|
||||
content_details.with_menu_button()
|
||||
} else {
|
||||
content_details.with_cancel_button()
|
||||
}
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Button(bm) => Some(bm),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let content_tap = Frame::left_aligned(title, PromptScreen::new_tap_to_confirm())
|
||||
.with_menu_button()
|
||||
.with_footer(TR::instructions__tap_to_confirm.into(), None)
|
||||
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
|
||||
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
|
||||
FrameMsg::Button(_) => Some(FlowMsg::Info),
|
||||
});
|
||||
|
||||
let content_menu = Frame::left_aligned(
|
||||
"".into(),
|
||||
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::buttons__cancel.into()),
|
||||
)
|
||||
.with_cancel_button()
|
||||
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
|
||||
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
|
||||
});
|
||||
|
||||
let initial_page = if Self::single_cred() {
|
||||
&ConfirmFido::Details
|
||||
} else {
|
||||
&ConfirmFido::Intro
|
||||
};
|
||||
let res = SwipeFlow::new(initial_page)?
|
||||
.with_page(&ConfirmFido::Intro, content_intro)?
|
||||
.with_page(&ConfirmFido::ChooseCredential, content_choose_credential)?
|
||||
.with_page(&ConfirmFido::Details, content_details)?
|
||||
.with_page(&ConfirmFido::Tap, content_tap)?
|
||||
.with_page(&ConfirmFido::Menu, content_menu)?;
|
||||
Ok(LayoutObj::new(res)?.into())
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
pub mod confirm_action;
|
||||
#[cfg(feature = "universal_fw")]
|
||||
pub mod confirm_fido;
|
||||
pub mod confirm_firmware_update;
|
||||
pub mod confirm_output;
|
||||
pub mod confirm_reset;
|
||||
@ -14,9 +16,11 @@ pub mod show_share_words;
|
||||
pub mod show_tutorial;
|
||||
pub mod warning_hi_prio;
|
||||
|
||||
pub use confirm_action::{new_confirm_action, new_confirm_action_simple};
|
||||
mod util;
|
||||
|
||||
pub use confirm_action::{new_confirm_action, new_confirm_action_simple};
|
||||
#[cfg(feature = "universal_fw")]
|
||||
pub use confirm_fido::new_confirm_fido;
|
||||
pub use confirm_firmware_update::new_confirm_firmware_update;
|
||||
pub use confirm_output::new_confirm_output;
|
||||
pub use confirm_reset::new_confirm_reset;
|
||||
|
@ -14,9 +14,7 @@ use crate::{
|
||||
error::{value_error, Error},
|
||||
io::BinaryData,
|
||||
micropython::{
|
||||
gc::Gc,
|
||||
iter::IterBuf,
|
||||
list::List,
|
||||
macros::{obj_fn_1, obj_fn_kw, obj_module},
|
||||
map::Map,
|
||||
module::Module,
|
||||
@ -58,17 +56,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
impl TryFrom<CancelConfirmMsg> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: CancelConfirmMsg) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CancelInfoConfirmMsg> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
@ -101,19 +88,6 @@ impl TryFrom<VerticalMenuChoiceMsg> for Obj {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, U> ComponentMsgObj for FidoConfirm<F, U>
|
||||
where
|
||||
F: Fn(usize) -> TString<'static>,
|
||||
U: Component<Msg = CancelConfirmMsg>,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
FidoMsg::Confirmed(page) => Ok((page as u8).into()),
|
||||
FidoMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentMsgObj for PinKeyboard<'_> {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
@ -716,37 +690,6 @@ extern "C" fn new_show_error(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_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let app_name: TString = kwargs.get(Qstr::MP_QSTR_app_name)?.try_into()?;
|
||||
let icon: Option<TString> = kwargs.get(Qstr::MP_QSTR_icon_name)?.try_into_option()?;
|
||||
let accounts: Gc<List> = kwargs.get(Qstr::MP_QSTR_accounts)?.try_into()?;
|
||||
|
||||
// Cache the page count so that we can move `accounts` into the closure.
|
||||
let page_count = accounts.len();
|
||||
// Closure to lazy-load the information on given page index.
|
||||
// Done like this to allow arbitrarily many pages without
|
||||
// the need of any allocation here in Rust.
|
||||
let get_page = move |page_index| {
|
||||
let account = unwrap!(accounts.get(page_index));
|
||||
account.try_into().unwrap_or_else(|_| "".into())
|
||||
};
|
||||
|
||||
let controls = Button::cancel_confirm(
|
||||
Button::with_icon(theme::ICON_CANCEL),
|
||||
Button::with_text(TR::buttons__confirm.into()).styled(theme::button_confirm()),
|
||||
true,
|
||||
);
|
||||
|
||||
let fido_page = FidoConfirm::new(app_name, get_page, page_count, icon, controls);
|
||||
|
||||
let obj = LayoutObj::new(Frame::centered(title, fido_page))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
@ -1213,6 +1156,13 @@ extern "C" fn new_show_wait_text(message: Obj) -> Obj {
|
||||
unsafe { util::try_or_raise(block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
#[cfg(feature = "universal_fw")]
|
||||
return flow::confirm_fido::new_confirm_fido(n_args, args, kwargs);
|
||||
#[cfg(not(feature = "universal_fw"))]
|
||||
panic!();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// from trezor import utils
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -335,15 +335,20 @@ class TR:
|
||||
fido__does_not_belong: str = "The credential you are trying to import does\nnot belong to this authenticator."
|
||||
fido__erase_credentials: str = "erase all credentials?"
|
||||
fido__export_credentials: str = "Export information about the credentials stored on this device?"
|
||||
fido__more_credentials: str = "More credentials"
|
||||
fido__not_registered: str = "Not registered"
|
||||
fido__not_registered_with_template: str = "This device is not registered with\n{0}."
|
||||
fido__please_enable_pin_protection: str = "Please enable PIN protection."
|
||||
fido__select_intro: str = "Select the credential that you would like to use for authentication."
|
||||
fido__title_authenticate: str = "FIDO2 authenticate"
|
||||
fido__title_credential_details: str = "Credential details"
|
||||
fido__title_for_authentication: str = "for authentication"
|
||||
fido__title_import_credential: str = "Import credential"
|
||||
fido__title_list_credentials: str = "List credentials"
|
||||
fido__title_register: str = "FIDO2 register"
|
||||
fido__title_remove_credential: str = "Remove credential"
|
||||
fido__title_reset: str = "FIDO2 reset"
|
||||
fido__title_select_credential: str = "Select credential"
|
||||
fido__title_u2f_auth: str = "U2F authenticate"
|
||||
fido__title_u2f_register: str = "U2F register"
|
||||
fido__title_verify_user: str = "FIDO2 verify user"
|
||||
@ -386,6 +391,7 @@ class TR:
|
||||
instructions__learn_more: str = "Learn more"
|
||||
instructions__shares_continue_with_x_template: str = "Continue with Share #{0}"
|
||||
instructions__shares_start_with_1: str = "Start with share #1"
|
||||
instructions__swipe_down: str = "Swipe down"
|
||||
instructions__swipe_horizontally: str = "Swipe horizontally"
|
||||
instructions__swipe_up: str = "Swipe up"
|
||||
instructions__tap_to_confirm: str = "Tap to confirm"
|
||||
|
@ -9,18 +9,19 @@ from . import RustLayout
|
||||
if TYPE_CHECKING:
|
||||
from trezor.loop import AwaitableTask
|
||||
|
||||
|
||||
if __debug__:
|
||||
from trezor import io, ui
|
||||
|
||||
from ... import Result
|
||||
|
||||
# needed solely for test_emu_u2f
|
||||
class _RustFidoLayoutImpl(RustLayout):
|
||||
def create_tasks(self) -> tuple[AwaitableTask, ...]:
|
||||
return (
|
||||
self.handle_input_and_rendering(),
|
||||
self.handle_timers(),
|
||||
self.handle_swipe(),
|
||||
self.handle_click_signal(),
|
||||
self.handle_debug_confirm(),
|
||||
)
|
||||
|
||||
@ -32,8 +33,11 @@ if __debug__:
|
||||
raise Result(result)
|
||||
|
||||
for event, x, y in (
|
||||
(io.TOUCH_START, 220, 220),
|
||||
(io.TOUCH_END, 220, 220),
|
||||
(io.TOUCH_START, 120, 160),
|
||||
(io.TOUCH_MOVE, 120, 130),
|
||||
(io.TOUCH_END, 120, 100),
|
||||
(io.TOUCH_START, 120, 120),
|
||||
(io.TOUCH_END, 120, 120),
|
||||
):
|
||||
msg = self.layout.touch_event(event, x, y)
|
||||
if self.layout.paint():
|
||||
@ -66,8 +70,8 @@ async def confirm_fido(
|
||||
|
||||
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
|
||||
# and assume cancellation otherwise.
|
||||
if isinstance(result, int):
|
||||
return result
|
||||
if isinstance(result, tuple):
|
||||
return result[1]
|
||||
|
||||
# Late import won't get executed on the happy path.
|
||||
from trezor.wire import ActionCancelled
|
||||
|
@ -11,10 +11,10 @@ from trezorlib import toif
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
|
||||
ICON_SIZE = (64, 64)
|
||||
DESTINATION = (
|
||||
ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_tt" / "res" / "fido"
|
||||
)
|
||||
DESTINATIONS = {
|
||||
ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_tt" / "res" / "fido": 64,
|
||||
ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_mercury" / "res" / "fido": 32,
|
||||
}
|
||||
EXCLUDE = {"icon_webauthn"}
|
||||
|
||||
# insert ../../common/tools to sys.path, so that we can import coin_info
|
||||
@ -31,9 +31,15 @@ import coin_info
|
||||
@click.command()
|
||||
@click.option("-c", "--check", is_flag=True, help="Do not write, only check.")
|
||||
@click.option("-r", "--remove", is_flag=True, help="Remove unrecognized files.")
|
||||
def build_icons(check, remove):
|
||||
def build_icons(check: bool, remove: bool):
|
||||
"""Build FIDO app icons in the source tree."""
|
||||
|
||||
for path, size in DESTINATIONS.items():
|
||||
build_icons_size(path, size, check, remove)
|
||||
|
||||
|
||||
def build_icons_size(destination: Path, size: int, check: bool, remove: bool):
|
||||
icon_size = (size, size)
|
||||
checks_ok = True
|
||||
apps = coin_info.fido_info()
|
||||
|
||||
@ -47,9 +53,9 @@ def build_icons(check, remove):
|
||||
continue
|
||||
|
||||
im = Image.open(app["icon"])
|
||||
resized = im.resize(ICON_SIZE, Image.BOX)
|
||||
resized = im.resize(icon_size, Image.BOX)
|
||||
toi = toif.from_image(resized)
|
||||
dest_path = DESTINATION / f"icon_{app['key']}.toif"
|
||||
dest_path = destination / f"icon_{app['key']}.toif"
|
||||
|
||||
total_size += len(toi.to_bytes())
|
||||
|
||||
@ -65,11 +71,11 @@ def build_icons(check, remove):
|
||||
print(f"Icon different from source: {dest_path}")
|
||||
checks_ok = False
|
||||
|
||||
print(f"Total icon size: {total_size} bytes")
|
||||
print(f"{destination.parts[-3]} icon size: {total_size} bytes")
|
||||
|
||||
keys = EXCLUDE | {"icon_" + app["key"] for app in apps}
|
||||
unrecognized_files = False
|
||||
for icon_file in DESTINATION.glob("*.toif"):
|
||||
for icon_file in destination.glob("*.toif"):
|
||||
name = icon_file.stem
|
||||
if name not in keys:
|
||||
unrecognized_files = True
|
||||
|
@ -337,15 +337,20 @@
|
||||
"fido__does_not_belong": "The credential you are trying to import does\nnot belong to this authenticator.",
|
||||
"fido__erase_credentials": "erase all credentials?",
|
||||
"fido__export_credentials": "Export information about the credentials stored on this device?",
|
||||
"fido__more_credentials": "More credentials",
|
||||
"fido__not_registered": "Not registered",
|
||||
"fido__not_registered_with_template": "This device is not registered with\n{0}.",
|
||||
"fido__please_enable_pin_protection": "Please enable PIN protection.",
|
||||
"fido__select_intro": "Select the credential that you would like to use for authentication.",
|
||||
"fido__title_authenticate": "FIDO2 authenticate",
|
||||
"fido__title_credential_details": "Credential details",
|
||||
"fido__title_for_authentication": "for authentication",
|
||||
"fido__title_import_credential": "Import credential",
|
||||
"fido__title_list_credentials": "List credentials",
|
||||
"fido__title_register": "FIDO2 register",
|
||||
"fido__title_remove_credential": "Remove credential",
|
||||
"fido__title_reset": "FIDO2 reset",
|
||||
"fido__title_select_credential": "Select credential",
|
||||
"fido__title_u2f_auth": "U2F authenticate",
|
||||
"fido__title_u2f_register": "U2F register",
|
||||
"fido__title_verify_user": "FIDO2 verify user",
|
||||
@ -388,6 +393,7 @@
|
||||
"instructions__learn_more": "Learn more",
|
||||
"instructions__shares_continue_with_x_template": "Continue with Share #{0}",
|
||||
"instructions__shares_start_with_1": "Start with share #1",
|
||||
"instructions__swipe_down": "Swipe down",
|
||||
"instructions__swipe_horizontally": "Swipe horizontally",
|
||||
"instructions__swipe_up": "Swipe up",
|
||||
"instructions__tap_to_confirm": "Tap to confirm",
|
||||
|
@ -958,5 +958,11 @@
|
||||
"956": "words__title_done",
|
||||
"957": "reset__slip39_checklist_more_info_threshold",
|
||||
"958": "reset__slip39_checklist_more_info_threshold_example_template",
|
||||
"959": "passphrase__continue_with_empty_passphrase"
|
||||
"959": "passphrase__continue_with_empty_passphrase",
|
||||
"960": "fido__more_credentials",
|
||||
"961": "fido__select_intro",
|
||||
"962": "fido__title_for_authentication",
|
||||
"963": "fido__title_select_credential",
|
||||
"964": "instructions__swipe_down",
|
||||
"965": "fido__title_credential_details"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"current": {
|
||||
"merkle_root": "6803ea2d1eb1555ae32a5b32689cdf40d7626a8b3717f6ae9418e0948f9da746",
|
||||
"datetime": "2024-08-27T15:18:26.530958",
|
||||
"commit": "6ebe850fe24f5ec894328d82eec97aacccb3d3f5"
|
||||
"merkle_root": "0682f8041f5d002800da51d3c3a36351d326b89ddf8fff6c3e70cd1943f3e064",
|
||||
"datetime": "2024-08-29T14:44:39.968325",
|
||||
"commit": "c5e520fd1e34182fb19044baa190dbc81fcf5cad"
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
|
@ -21,6 +21,7 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
|
||||
from trezorlib.exceptions import Cancelled, TrezorFailure
|
||||
|
||||
from ...common import MNEMONIC12
|
||||
from ...input_flows import InputFlowFidoConfirm
|
||||
from .data_webauthn import CRED1, CRED2, CRED3, CREDS
|
||||
|
||||
RK_CAPACITY = 100
|
||||
@ -30,73 +31,77 @@ RK_CAPACITY = 100
|
||||
@pytest.mark.altcoin
|
||||
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
|
||||
def test_add_remove(client: Client):
|
||||
# Remove index 0 should fail.
|
||||
with pytest.raises(TrezorFailure):
|
||||
fido.remove_credential(client, 0)
|
||||
with client:
|
||||
IF = InputFlowFidoConfirm(client)
|
||||
client.set_input_flow(IF.get())
|
||||
|
||||
# List should be empty.
|
||||
assert fido.list_credentials(client) == []
|
||||
# Remove index 0 should fail.
|
||||
with pytest.raises(TrezorFailure):
|
||||
fido.remove_credential(client, 0)
|
||||
|
||||
# Add valid credential #1.
|
||||
fido.add_credential(client, CRED1)
|
||||
# List should be empty.
|
||||
assert fido.list_credentials(client) == []
|
||||
|
||||
# Check that the credential was added and parameters are correct.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 1
|
||||
assert creds[0].rp_id == "example.com"
|
||||
assert creds[0].rp_name == "Example"
|
||||
assert creds[0].user_id == bytes.fromhex(
|
||||
"3082019330820138A0030201023082019330820138A003020102308201933082"
|
||||
)
|
||||
assert creds[0].user_name == "johnpsmith@example.com"
|
||||
assert creds[0].user_display_name == "John P. Smith"
|
||||
assert creds[0].creation_time == 3
|
||||
assert creds[0].hmac_secret is True
|
||||
# Add valid credential #1.
|
||||
fido.add_credential(client, CRED1)
|
||||
|
||||
# Add valid credential #2, which has same rpId and userId as credential #1.
|
||||
fido.add_credential(client, CRED2)
|
||||
# Check that the credential was added and parameters are correct.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 1
|
||||
assert creds[0].rp_id == "example.com"
|
||||
assert creds[0].rp_name == "Example"
|
||||
assert creds[0].user_id == bytes.fromhex(
|
||||
"3082019330820138A0030201023082019330820138A003020102308201933082"
|
||||
)
|
||||
assert creds[0].user_name == "johnpsmith@example.com"
|
||||
assert creds[0].user_display_name == "John P. Smith"
|
||||
assert creds[0].creation_time == 3
|
||||
assert creds[0].hmac_secret is True
|
||||
|
||||
# Check that the credential #2 replaced credential #1 and parameters are correct.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 1
|
||||
assert creds[0].rp_id == "example.com"
|
||||
assert creds[0].rp_name is None
|
||||
assert creds[0].user_id == bytes.fromhex(
|
||||
"3082019330820138A0030201023082019330820138A003020102308201933082"
|
||||
)
|
||||
assert creds[0].user_name == "johnpsmith@example.com"
|
||||
assert creds[0].user_display_name is None
|
||||
assert creds[0].creation_time == 2
|
||||
assert creds[0].hmac_secret is True
|
||||
# Add valid credential #2, which has same rpId and userId as credential #1.
|
||||
fido.add_credential(client, CRED2)
|
||||
|
||||
# Adding an invalid credential should appear as if user cancelled.
|
||||
with pytest.raises(Cancelled):
|
||||
fido.add_credential(client, CRED1[:-2])
|
||||
# Check that the credential #2 replaced credential #1 and parameters are correct.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 1
|
||||
assert creds[0].rp_id == "example.com"
|
||||
assert creds[0].rp_name is None
|
||||
assert creds[0].user_id == bytes.fromhex(
|
||||
"3082019330820138A0030201023082019330820138A003020102308201933082"
|
||||
)
|
||||
assert creds[0].user_name == "johnpsmith@example.com"
|
||||
assert creds[0].user_display_name is None
|
||||
assert creds[0].creation_time == 2
|
||||
assert creds[0].hmac_secret is True
|
||||
|
||||
# Check that the invalid credential was not added.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 1
|
||||
# Adding an invalid credential should appear as if user cancelled.
|
||||
with pytest.raises(Cancelled):
|
||||
fido.add_credential(client, CRED1[:-2])
|
||||
|
||||
# Add valid credential, which has same userId as #2, but different rpId.
|
||||
fido.add_credential(client, CRED3)
|
||||
# Check that the invalid credential was not added.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 1
|
||||
|
||||
# Check that the credential was added.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 2
|
||||
# Add valid credential, which has same userId as #2, but different rpId.
|
||||
fido.add_credential(client, CRED3)
|
||||
|
||||
# Fill up the credential storage to maximum capacity.
|
||||
for cred in CREDS[: RK_CAPACITY - 2]:
|
||||
fido.add_credential(client, cred)
|
||||
# Check that the credential was added.
|
||||
creds = fido.list_credentials(client)
|
||||
assert len(creds) == 2
|
||||
|
||||
# Adding one more valid credential to full storage should fail.
|
||||
with pytest.raises(TrezorFailure):
|
||||
# Fill up the credential storage to maximum capacity.
|
||||
for cred in CREDS[: RK_CAPACITY - 2]:
|
||||
fido.add_credential(client, cred)
|
||||
|
||||
# Adding one more valid credential to full storage should fail.
|
||||
with pytest.raises(TrezorFailure):
|
||||
fido.add_credential(client, CREDS[-1])
|
||||
|
||||
# Removing the index, which is one past the end, should fail.
|
||||
with pytest.raises(TrezorFailure):
|
||||
fido.remove_credential(client, RK_CAPACITY)
|
||||
|
||||
# Remove index 2.
|
||||
fido.remove_credential(client, 2)
|
||||
# Adding another valid credential should succeed now.
|
||||
fido.add_credential(client, CREDS[-1])
|
||||
|
||||
# Removing the index, which is one past the end, should fail.
|
||||
with pytest.raises(TrezorFailure):
|
||||
fido.remove_credential(client, RK_CAPACITY)
|
||||
|
||||
# Remove index 2.
|
||||
fido.remove_credential(client, 2)
|
||||
# Adding another valid credential should succeed now.
|
||||
fido.add_credential(client, CREDS[-1])
|
||||
|
@ -2335,3 +2335,23 @@ class InputFlowTutorial(InputFlowBase):
|
||||
self.debug.swipe_up(wait=True)
|
||||
self.debug.click(buttons.TAP_TO_CONFIRM, wait=True)
|
||||
self.debug.swipe_up(wait=True)
|
||||
|
||||
|
||||
class InputFlowFidoConfirm(InputFlowBase):
|
||||
def __init__(self, client: Client, cancel: bool = False):
|
||||
super().__init__(client)
|
||||
self.cancel = cancel
|
||||
|
||||
def input_flow_tt(self) -> BRGeneratorType:
|
||||
while True:
|
||||
yield
|
||||
self.debug.press_yes()
|
||||
|
||||
def input_flow_tr(self) -> BRGeneratorType:
|
||||
yield from self.input_flow_tt()
|
||||
|
||||
def input_flow_t3t1(self) -> BRGeneratorType:
|
||||
while True:
|
||||
yield
|
||||
self.debug.swipe_up(wait=True)
|
||||
self.debug.click(buttons.TAP_TO_CONFIRM, wait=True)
|
||||
|
@ -16855,7 +16855,7 @@
|
||||
"T3T1_cs_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "c86f8a5e634d46bb9e213275ca59ebb7dd57624660c1f8381e3f37b44b3687cb",
|
||||
"T3T1_cs_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "a0ec5adc925d8546a9ee52224f874029ca989882daf762e24eb72701722755a2",
|
||||
"T3T1_cs_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "9938e23983b3f11af45521aa0c2488ad21dffd3f75b28f3ef30885da3bfb7123",
|
||||
"T3T1_cs_webauthn-test_msg_webauthn.py::test_add_remove": "e3a00be2a98bbb17b2b2a13742de59f88011ca85efef334b4a475a48a03dad94",
|
||||
"T3T1_cs_webauthn-test_msg_webauthn.py::test_add_remove": "0910f0f00f36470da0475b1f8fdd918d6a550b050a32642639481105664cc043",
|
||||
"T3T1_cs_webauthn-test_u2f_counter.py::test_u2f_counter": "748b990a8eb65c96e9720ac5a339a058390c1546db631dd1cd65aac6ad848251",
|
||||
"T3T1_cs_zcash-test_sign_tx.py::test_external_presigned": "8e8f8d7c9e18312b93627a202e55bb6b4ed48d2a7c30bc89493bc945fa47fb37",
|
||||
"T3T1_cs_zcash-test_sign_tx.py::test_one_two": "e4375b012b97230a765d04857674758533540eda29111ba36ccedeb6008472dc",
|
||||
@ -18197,7 +18197,7 @@
|
||||
"T3T1_de_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "534db7b1cba3a2a0a1cf4d2eba3cf8652001b5c4db1310891871234fcb555e08",
|
||||
"T3T1_de_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "e787f8ef4c14ddcdaae214d30f3499335d9cab6e2066b31a53d47858a407a7e1",
|
||||
"T3T1_de_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "fc619426393e344424721750988489067b17a7a85861bc2d7703053a1b99fa46",
|
||||
"T3T1_de_webauthn-test_msg_webauthn.py::test_add_remove": "e64d049fb19083b94e6333eb1af6e524b24d5c3f1d06da3d286ffcbe806c1072",
|
||||
"T3T1_de_webauthn-test_msg_webauthn.py::test_add_remove": "03f7773f30726020e07dbc7ab3c5a3a6eaf9d63bb954e13e5022d51539718221",
|
||||
"T3T1_de_webauthn-test_u2f_counter.py::test_u2f_counter": "688352aeccbd7fbbee6f3ed7e4dbcc1362b78d965a0c854e27e756b9871d43da",
|
||||
"T3T1_de_zcash-test_sign_tx.py::test_external_presigned": "85f2067de3f218e718ae164e97dc030c4aef065c29755f6138afe10d664a2d56",
|
||||
"T3T1_de_zcash-test_sign_tx.py::test_one_two": "10258e39046685e302d403a9df4f82cc4b851cfa07d8103f2a52f4afd81fa99d",
|
||||
@ -19539,7 +19539,7 @@
|
||||
"T3T1_en_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "2488848ff080a1de419fdc6bc7064bf4a0504b306f70df555863e9ddcac66a93",
|
||||
"T3T1_en_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "23ccb252c1ff5847c1db298a36dae6cb48fa2ae0962c10acc05bcf666cbe9875",
|
||||
"T3T1_en_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "ba87e90e6d810d1a8e39febbc867d89a69007b1ce7bdcc63224627e14914ab66",
|
||||
"T3T1_en_webauthn-test_msg_webauthn.py::test_add_remove": "3d20b34ed2f65f424ca962dbd5470d7078b6b01af415aba19ba7c3fd11e5202b",
|
||||
"T3T1_en_webauthn-test_msg_webauthn.py::test_add_remove": "49a2ce71a462f645ababd1dea2fc88fcf3d1b291391a12209342bf4e36e19b5c",
|
||||
"T3T1_en_webauthn-test_u2f_counter.py::test_u2f_counter": "eb0670c196dacdf4d35fe8d03ba368ace9b3e90471a0f892ccec5e1ea7b42c10",
|
||||
"T3T1_en_zcash-test_sign_tx.py::test_external_presigned": "ea19880720b16fe8a730e908b2abfb34cf507940c156e882853a7632b842b329",
|
||||
"T3T1_en_zcash-test_sign_tx.py::test_one_two": "7346debbcbd3b808e607c9f6a75c638307c976d4c3249fb3ec1dc6c416826a38",
|
||||
@ -20881,7 +20881,7 @@
|
||||
"T3T1_es_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "290251b9cc8901f682b5238649d59148f2e38f8bf600c9a7371c8704d1ebf531",
|
||||
"T3T1_es_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "8c3138d7b797bcaea06d66998cc049d19448030b5c0f3c93112e5fd046422e67",
|
||||
"T3T1_es_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "f91fca0f393aaaa079d872dd72286065f0df707dc66b7cf70b030e63ac4fec45",
|
||||
"T3T1_es_webauthn-test_msg_webauthn.py::test_add_remove": "6a7c3882c247b42f236385b8b7e913d98d6959b033b16b87692e411873894c0e",
|
||||
"T3T1_es_webauthn-test_msg_webauthn.py::test_add_remove": "1beb0994c99deb80b0a5a95752ac4453df19abad5e960a4e20d6db2a9bdbae56",
|
||||
"T3T1_es_webauthn-test_u2f_counter.py::test_u2f_counter": "2e7ce8f4c09f1c65cb451f9784b4f9973bb53ca2ccb9ad4282addc9d0b2beb65",
|
||||
"T3T1_es_zcash-test_sign_tx.py::test_external_presigned": "43b0985d5076d4b3a9962384e688d6b5bac71f2b493547cfa6cf41a45bb405d2",
|
||||
"T3T1_es_zcash-test_sign_tx.py::test_one_two": "156d2d776997b10e5b8a85c231e734a95508a6df77ce9c5241333d9adcd71a97",
|
||||
@ -22223,7 +22223,7 @@
|
||||
"T3T1_fr_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "ddaa5b732f252125d32b0f5a37e7921bcc1ff294d77b09c112e4e3f563b8855c",
|
||||
"T3T1_fr_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "e05975469765465d77085adce8e3adc259e5c8e653a2be6fcdaf428277805f22",
|
||||
"T3T1_fr_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "01bdb5b8ceac205728755f3f9e555c30ab7a6caceb03c6156542f2955bf08ea8",
|
||||
"T3T1_fr_webauthn-test_msg_webauthn.py::test_add_remove": "45717859ce16c0e10b6aad7fe4dadedba449fbad3578c263d33324f936c1bca2",
|
||||
"T3T1_fr_webauthn-test_msg_webauthn.py::test_add_remove": "e7292add524a8fb87e18786704b7e809e17fa00bf828370e572a61932c90fbee",
|
||||
"T3T1_fr_webauthn-test_u2f_counter.py::test_u2f_counter": "dc90f97d311358870f5ff032a1af0cc677a8966ea236c82bcd7f7d3f58990c93",
|
||||
"T3T1_fr_zcash-test_sign_tx.py::test_external_presigned": "ed968fc6f87e77bf1062997e2ac7b1881aef2361331bd4551f823a7f9eb6e639",
|
||||
"T3T1_fr_zcash-test_sign_tx.py::test_one_two": "24180452ff7c2caec1587e3f0c9cdde8f760fdb060bf0a7a41bcaf80841d0aa6",
|
||||
|
Loading…
Reference in New Issue
Block a user