feat(core/ui): T3T1 set new PIN flow

[no changelog]
obrusvit 3 weeks ago committed by Vít Obrusník
parent 308d4debd6
commit 78f95129a0

@ -213,6 +213,7 @@ static void _librust_qstrs(void) {
MP_QSTR_firmware_update__title_fingerprint;
MP_QSTR_flow_confirm_reset_create;
MP_QSTR_flow_confirm_reset_recover;
MP_QSTR_flow_confirm_set_new_pin;
MP_QSTR_flow_get_address;
MP_QSTR_flow_prompt_backup;
MP_QSTR_flow_show_share_words;
@ -306,6 +307,9 @@ static void _librust_qstrs(void) {
MP_QSTR_passphrase__turn_off;
MP_QSTR_passphrase__turn_on;
MP_QSTR_path;
MP_QSTR_pin__cancel_description;
MP_QSTR_pin__cancel_info;
MP_QSTR_pin__cancel_setup;
MP_QSTR_pin__change;
MP_QSTR_pin__changed;
MP_QSTR_pin__cursor_will_change;

@ -1255,6 +1255,9 @@ pub enum TranslatedString {
words__cancel_and_exit = 860, // "Cancel and exit"
address__confirmed = 861, // "Receive address confirmed"
reset__title_shamir_backup = 862, // "Shamir backup"
pin__cancel_description = 863, // "Continue without PIN"
pin__cancel_info = 864, // "Without a PIN, anyone can access this device."
pin__cancel_setup = 865, // "Cancel PIN setup"
}
impl TranslatedString {
@ -2505,6 +2508,9 @@ impl TranslatedString {
Self::words__cancel_and_exit => "Cancel and exit",
Self::address__confirmed => "Receive address confirmed",
Self::reset__title_shamir_backup => "Shamir backup",
Self::pin__cancel_description => "Continue without PIN",
Self::pin__cancel_info => "Without a PIN, anyone can access this device.",
Self::pin__cancel_setup => "Cancel PIN setup",
}
}
@ -3756,6 +3762,9 @@ impl TranslatedString {
Qstr::MP_QSTR_words__cancel_and_exit => Some(Self::words__cancel_and_exit),
Qstr::MP_QSTR_address__confirmed => Some(Self::address__confirmed),
Qstr::MP_QSTR_reset__title_shamir_backup => Some(Self::reset__title_shamir_backup),
Qstr::MP_QSTR_pin__cancel_description => Some(Self::pin__cancel_description),
Qstr::MP_QSTR_pin__cancel_info => Some(Self::pin__cancel_info),
Qstr::MP_QSTR_pin__cancel_setup => Some(Self::pin__cancel_setup),
_ => None,
}
}

@ -32,11 +32,12 @@ const MAX_VISIBLE_DOTS: usize = 18;
const MAX_VISIBLE_DIGITS: usize = 18;
const DIGIT_COUNT: usize = 10; // 0..10
const HEADER_PADDING_TOP: i16 = 4;
const HEADER_PADDING_SIDE: i16 = 2;
const HEADER_PADDING_BOTTOM: i16 = 2;
const HEADER_PADDING_BOTTOM: i16 = 4;
const HEADER_PADDING: Insets = Insets::new(
0,
HEADER_PADDING_TOP,
HEADER_PADDING_SIDE,
HEADER_PADDING_BOTTOM,
HEADER_PADDING_SIDE,
@ -232,23 +233,7 @@ impl Component for PinKeyboard<'_> {
}
fn paint(&mut self) {
self.erase_btn.paint();
self.textbox_pad.paint();
if self.textbox.inner().is_empty() {
if let Some(ref mut w) = self.major_warning {
w.paint();
} else {
self.major_prompt.paint();
}
self.minor_prompt.paint();
self.cancel_btn.paint();
} else {
self.textbox.paint();
}
self.confirm_btn.paint();
for btn in &mut self.digit_btns {
btn.paint();
}
todo!("remove when ui-t3t1 done");
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {

@ -0,0 +1,145 @@
use crate::{
error,
micropython::qstr::Qstr,
strutil::TString,
translations::TR,
ui::{
component::{
text::paragraphs::{Paragraph, Paragraphs},
ComponentExt, SwipeDirection,
},
flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage},
},
};
use super::super::{
component::{
CancelInfoConfirmMsg, Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg,
},
theme,
};
#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)]
pub enum SetNewPin {
Intro,
Menu,
CancelPinIntro,
CancelPinConfirm,
}
impl FlowState for SetNewPin {
fn handle_swipe(&self, direction: SwipeDirection) -> Decision<Self> {
match (self, direction) {
(SetNewPin::Intro, SwipeDirection::Left) => Decision::Goto(SetNewPin::Menu, direction),
(SetNewPin::CancelPinIntro, SwipeDirection::Up) => {
Decision::Goto(SetNewPin::CancelPinConfirm, direction)
}
(SetNewPin::CancelPinConfirm, SwipeDirection::Down) => {
Decision::Goto(SetNewPin::CancelPinIntro, direction)
}
(SetNewPin::Intro, SwipeDirection::Up) => Decision::Return(FlowMsg::Confirmed),
_ => Decision::Nothing,
}
}
fn handle_event(&self, msg: FlowMsg) -> Decision<Self> {
match (self, msg) {
(SetNewPin::Intro, FlowMsg::Info) => {
Decision::Goto(SetNewPin::Menu, SwipeDirection::Left)
}
(SetNewPin::Menu, FlowMsg::Choice(0)) => {
Decision::Goto(SetNewPin::CancelPinIntro, SwipeDirection::Left)
}
(SetNewPin::Menu, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::Intro, SwipeDirection::Right)
}
(SetNewPin::CancelPinIntro, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::Menu, SwipeDirection::Right)
}
(SetNewPin::CancelPinConfirm, FlowMsg::Cancelled) => {
Decision::Goto(SetNewPin::CancelPinIntro, SwipeDirection::Right)
}
(SetNewPin::CancelPinConfirm, FlowMsg::Confirmed) => {
Decision::Return(FlowMsg::Cancelled)
}
_ => Decision::Nothing,
}
}
}
use crate::{
micropython::{map::Map, obj::Obj, util},
ui::layout::obj::LayoutObj,
};
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_set_new_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, SetNewPin::new_obj) }
}
impl SetNewPin {
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
// TODO: supply more arguments for Wipe code setting when figma done
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let par_array: [Paragraph<'static>; 1] =
[Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)];
let paragraphs = Paragraphs::new(par_array);
let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs))
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.map(|msg| {
matches!(msg, FrameMsg::Button(CancelInfoConfirmMsg::Info)).then_some(FlowMsg::Info)
});
let content_menu = Frame::left_aligned(
"".into(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::pin__cancel_setup.into()),
)
.with_cancel_button()
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
FrameMsg::Button(_) => None,
});
let par_array_cancel_intro: [Paragraph<'static>; 2] = [
Paragraph::new(&theme::TEXT_WARNING, TR::words__not_recommended),
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, TR::pin__cancel_info),
];
let paragraphs_cancel_intro = Paragraphs::new(par_array_cancel_intro);
let content_cancel_intro = Frame::left_aligned(
TR::pin__cancel_setup.into(),
SwipePage::vertical(paragraphs_cancel_intro),
)
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::pin__cancel_description.into()),
)
.map(|msg| match msg {
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let content_cancel_confirm = Frame::left_aligned(
TR::pin__cancel_setup.into(),
PromptScreen::new_tap_to_cancel(),
)
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(CancelInfoConfirmMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let store = flow_store()
.add(content_intro)?
.add(content_menu)?
.add(content_cancel_intro)?
.add(content_cancel_confirm)?;
let res = SwipeFlow::new(SetNewPin::Intro, store)?;
Ok(LayoutObj::new(res)?.into())
}
}

@ -1,5 +1,6 @@
pub mod confirm_reset_create;
pub mod confirm_reset_recover;
pub mod confirm_set_new_pin;
pub mod get_address;
pub mod prompt_backup;
pub mod show_share_words;
@ -7,6 +8,7 @@ pub mod warning_hi_prio;
pub use confirm_reset_create::ConfirmResetCreate;
pub use confirm_reset_recover::ConfirmResetRecover;
pub use confirm_set_new_pin::SetNewPin;
pub use get_address::GetAddress;
pub use prompt_backup::PromptBackup;
pub use show_share_words::ShowShareWords;

@ -882,14 +882,26 @@ fn new_show_modal(
extern "C" fn new_show_error(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let icon = BlendedImage::new(
theme::IMAGE_BG_CIRCLE,
theme::IMAGE_FG_ERROR,
theme::ERROR_COLOR,
theme::FG,
theme::BG,
);
new_show_modal(kwargs, icon, theme::button_default())
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let allow_cancel: bool = kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into()?;
let content = SwipeUpScreen::new(Paragraphs::new([Paragraph::new(
&theme::TEXT_MAIN_GREY_LIGHT,
description,
)]));
let frame = if allow_cancel {
Frame::left_aligned(title, content)
.with_cancel_button()
.with_danger()
.with_footer(TR::instructions__swipe_up.into(), None)
} else {
Frame::left_aligned(title, content)
.with_danger()
.with_footer(TR::instructions__swipe_up.into(), None)
};
let obj = LayoutObj::new(frame)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -1670,6 +1682,15 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm TOS before creating a wallet and have a user hold to confirm creation."""
Qstr::MP_QSTR_flow_confirm_reset_create => obj_fn_kw!(0, flow::confirm_reset_create::new_confirm_reset_create).as_obj(),
// TODO: supply more arguments for Wipe code setting when figma done
/// def flow_confirm_set_new_pin(
/// *,
/// title: str,
/// description: str,
/// ) -> LayoutObj[UiResult]:
/// """Confirm new PIN setup with an option to cancel action."""
Qstr::MP_QSTR_flow_confirm_set_new_pin => obj_fn_kw!(0, flow::confirm_set_new_pin::new_set_new_pin).as_obj(),
/// def show_info_with_cancel(
/// *,
/// title: str,

@ -155,6 +155,15 @@ def flow_confirm_reset_create() -> LayoutObj[UiResult]:
"""Confirm TOS before creating a wallet and have a user hold to confirm creation."""
# rust/src/ui/model_mercury/layout.rs
def flow_confirm_set_new_pin(
*,
title: str,
description: str,
) -> LayoutObj[UiResult]:
"""Confirm new PIN setup with an option to cancel action."""
# rust/src/ui/model_mercury/layout.rs
def show_info_with_cancel(
*,

@ -469,6 +469,9 @@ class TR:
passphrase__title_source: str = "Passphrase source"
passphrase__turn_off: str = "Turn off passphrase protection?"
passphrase__turn_on: str = "Turn on passphrase protection?"
pin__cancel_description: str = "Continue without PIN"
pin__cancel_info: str = "Without a PIN, anyone can access this device."
pin__cancel_setup: str = "Cancel PIN setup"
pin__change: str = "Change PIN?"
pin__changed: str = "PIN changed."
pin__cursor_will_change: str = "Position of the cursor will change between entries for enhanced security."

@ -1404,8 +1404,9 @@ async def pin_mismatch_popup(
is_wipe_code: bool = False,
) -> None:
await button_request("pin_mismatch", code=BR_TYPE_OTHER)
title = TR.wipe_code__wipe_code_mismatch if is_wipe_code else TR.pin__pin_mismatch
description = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch
title = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch
description = TR.wipe_code__enter_new if is_wipe_code else TR.pin__reenter_new
return await show_error_popup(
title,
description,
@ -1432,14 +1433,7 @@ async def confirm_set_new_pin(
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.confirm_emphasized(
title=title,
items=(
(True, description + "\n\n"),
information,
),
verb=TR.buttons__turn_on,
)
trezorui2.flow_confirm_set_new_pin(title=title, description=description)
),
br_type,
br_code,

@ -471,8 +471,11 @@
"passphrase__title_source": "Passphrase source",
"passphrase__turn_off": "Turn off passphrase protection?",
"passphrase__turn_on": "Turn on passphrase protection?",
"pin__cancel_info": "Without a PIN, anyone can access this device.",
"pin__cancel_setup": "Cancel PIN setup",
"pin__change": "Change PIN?",
"pin__changed": "PIN changed.",
"pin__cancel_description": "Continue without PIN",
"pin__cursor_will_change": "Position of the cursor will change between entries for enhanced security.",
"pin__diff_from_wipe_code": "The new PIN must be different from your wipe code.",
"pin__disabled": "PIN protection\nturned off.",

@ -861,5 +861,8 @@
"859": "instructions__continue_in_app",
"860": "words__cancel_and_exit",
"861": "address__confirmed",
"862": "reset__title_shamir_backup"
"862": "reset__title_shamir_backup",
"863": "pin__cancel_description",
"864": "pin__cancel_info",
"865": "pin__cancel_setup"
}

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "98827e51058bba4be29aca85980cddc9c666af131ae371626edaefb4865bae7e",
"datetime": "2024-05-13T21:55:07.519142",
"commit": "ee5010523f707babfdfa85b3f11f8e9dcba2846f"
"merkle_root": "ae04527a2d780136398e2b4aa3fb1eac107323914d24c0486525b75566fd71ed",
"datetime": "2024-05-13T22:27:12.526875",
"commit": "0395ec5c9bc01020f82b92eba3197c6661f1490e"
},
"history": [
{

@ -17,8 +17,8 @@ LEFT = grid(DISPLAY_WIDTH, 3, 0)
MID = grid(DISPLAY_WIDTH, 3, 1)
RIGHT = grid(DISPLAY_WIDTH, 3, 2)
TOP = grid(DISPLAY_HEIGHT, 4, 0)
BOTTOM = grid(DISPLAY_HEIGHT, 4, 3)
TOP = grid(DISPLAY_HEIGHT, 6, 0)
BOTTOM = grid(DISPLAY_HEIGHT, 6, 5)
OK = (RIGHT, BOTTOM)
CANCEL = (LEFT, BOTTOM)

@ -46,10 +46,12 @@ def get_char_category(char: str) -> PassphraseCategory:
def go_next(debug: "DebugLink", wait: bool = False) -> "LayoutContent" | None:
if debug.model in (models.T2T1, models.T3T1):
if debug.model in (models.T2T1,):
return debug.click(buttons.OK, wait=wait) # type: ignore
elif debug.model in (models.T2B1,):
return debug.press_right(wait=wait) # type: ignore
elif debug.model in (models.T3T1,):
return debug.swipe_up(wait=wait)
else:
raise RuntimeError("Unknown model")
@ -111,6 +113,17 @@ def navigate_to_action_and_press(
debug.press_middle(wait=True)
def unlock_gesture(debug: "DebugLink", wait: bool = False) -> "LayoutContent" | None:
if debug.model in (models.T2T1,):
return debug.click(buttons.OK, wait=wait) # type: ignore
elif debug.model in (models.T2B1,):
return debug.press_right(wait=wait) # type: ignore
elif debug.model in (models.T3T1,):
return debug.click(buttons.TAP_TO_CONFIRM, wait=wait) # type: ignore
else:
raise RuntimeError("Unknown model")
def _get_action_index(wanted_action: str, all_actions: list[str]) -> int:
"""Get index of the action in the list of all actions"""
if wanted_action in all_actions:

@ -62,7 +62,7 @@ def select_number_of_words(
if wait:
debug.wait_layout()
TR.assert_equals(debug.read_layout().text_content(), "recovery__num_of_words")
if debug.model in (models.T2T1, models.T3T1):
if debug.model in (models.T2T1,):
# click the number
word_option_offset = 6
word_options = (12, 18, 20, 24, 33)
@ -81,6 +81,20 @@ def select_number_of_words(
for _ in range(index):
debug.press_right(wait=True)
layout = debug.press_middle(wait=True)
elif debug.model in (models.T3T1,):
if num_of_words == 12:
coords = buttons.grid34(0, 1)
elif num_of_words == 18:
coords = buttons.grid34(2, 1)
elif num_of_words == 20:
coords = buttons.grid34(0, 2)
elif num_of_words == 24:
coords = buttons.grid34(2, 2)
elif num_of_words == 33:
coords = buttons.grid34(1, 3)
else:
raise ValueError("Invalid num_of_words")
layout = debug.click(coords, wait=True)
else:
raise ValueError("Unknown model")

@ -29,7 +29,7 @@ from .. import translations as TR
from ..device_tests.bitcoin.payment_req import make_coinjoin_request
from ..tx_cache import TxCache
from . import recovery
from .common import go_next
from .common import go_next, unlock_gesture
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink, LayoutContent
@ -279,7 +279,8 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle
# unlock
# lockscreen triggered automatically
debug.wait_layout(wait_for_external_change=True)
layout = go_next(debug, wait=True)
layout = unlock_gesture(debug, wait=True)
assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4, wait=True)
assert layout is not None
@ -301,7 +302,7 @@ def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"):
recovery.select_number_of_words(debug, 20)
if debug.model in (models.T2T1, models.T3T1):
layout = debug.click(buttons.OK, wait=True)
layout = go_next(debug, wait=True)
assert layout.main_component() == "MnemonicKeyboard"
elif debug.model in (models.T2B1,):
layout = debug.press_right(wait=True)

@ -95,7 +95,9 @@ def prepare(
elif situation == Situation.PIN_SETUP:
# Set new PIN
device_handler.run(device.change_pin) # type: ignore
TR.assert_in(debug.wait_layout().text_content(), "pin__turn_on")
TR.assert_in_multiple(
debug.wait_layout().text_content(), ["pin__turn_on", "pin__info"]
)
if debug.model in (models.T2T1, models.T3T1):
go_next(debug)
elif debug.model in (models.T2B1,):
@ -306,12 +308,15 @@ def test_pin_setup(device_handler: "BackgroundDeviceHandler"):
def test_pin_setup_mismatch(device_handler: "BackgroundDeviceHandler"):
with PIN_CANCELLED, prepare(device_handler, Situation.PIN_SETUP) as debug:
_enter_two_times(debug, "1", "2")
if debug.model in (models.T2T1, models.T3T1):
if debug.model in (models.T2T1,):
go_next(debug)
_cancel_pin(debug)
elif debug.model in (models.T2B1,):
debug.press_middle()
debug.press_no()
elif debug.model in (models.T3T1,):
go_next(debug, wait=True)
_cancel_pin(debug)
@pytest.mark.setup_client(pin="1")

@ -421,12 +421,11 @@ def pytest_configure(config: "Config") -> None:
def pytest_runtest_setup(item: pytest.Item) -> None:
"""Called for each test item (class, individual tests).
Ensures that altcoin tests are skipped, and that no test is skipped on
both T1 and TT.
Ensures that altcoin tests are skipped, and that no test is skipped for all models.
"""
if all(
item.get_closest_marker(marker)
for marker in ("skip_t1b1", "skip_t2t1", "skip_t2b1")
for marker in ("skip_t1b1", "skip_t2t1", "skip_t2b1", "skip_t3t1")
):
raise RuntimeError("Don't skip tests for all trezor models!")

@ -21,6 +21,7 @@ from trezorlib.client import MAX_PIN_LENGTH, PASSPHRASE_TEST_PATH
from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import Cancelled, TrezorFailure
from .. import buttons
from ..input_flows import (
InputFlowCodeChangeFail,
InputFlowNewCodeMismatch,
@ -180,3 +181,27 @@ def test_change_invalid_current(client: Client):
client.init_device()
assert client.features.pin_protection is True
_check_pin(client, PIN4)
@pytest.mark.skip_t2b1()
@pytest.mark.skip_t2t1()
@pytest.mark.setup_client(pin=None)
def test_pin_menu_cancel_setup(client: Client):
def cancel_pin_setup_input_flow():
yield
# enter context menu
client.debug.click(buttons.CORNER_BUTTON)
client.debug.synchronize_at("VerticalMenu")
# click "Cancel PIN setup"
client.debug.click(buttons.VERTICAL_MENU[0])
client.debug.synchronize_at("Paragraphs")
# swipe through info screen
client.debug.swipe_up()
client.debug.synchronize_at("PromptScreen")
# tap to confirm
client.debug.click(buttons.TAP_TO_CONFIRM)
with client, pytest.raises(Cancelled):
client.set_input_flow(cancel_pin_setup_input_flow)
client.call(messages.ChangePin())
_check_no_pin(client)

Loading…
Cancel
Save