feat(delizia): swipe to go back on confirm_value
What changed, and why it matters
This commit adds a 'swipe down to go back' feature to the Delizia user interface on Trezor hardware wallets. When reviewing a cryptocurrency transaction, a user can now swipe down on the amount screen to return to the address screen and double-check it. It is a usability improvement, not a fix for a vulnerability.
No security action required; treat as a normal feature/UX commit.
Security signals we found
No memory-safety changes
No cryptographic changes
No privilege or authorization changes
No secret handling changes
UI-only navigation/flow change
Evidence from the diff
The change extends the Delizia layout’s confirm_value/confirm_action flows to recognize a downward swipe as FlowMsg::Back, and wires the Python confirm_output() helper to loop back to the address screen when the user swipes down on the amount screen. It also adds a matching device test (test_one_one_fee_back_from_amount) for the t3t1 model. No security boundary is crossed and no sensitive data is exposed.
Changed components
core/embed/rust/src/ui/layout_delizia/flow/confirm_action.rscore/embed/rust/src/ui/layout_delizia/flow/util.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/src/trezor/ui/layouts/delizia/__init__.pytests/device_tests/bitcoin/test_signtx.pytests/input_flows.pyInspect captured patch +97 / −35
diff --git a/core/embed/rust/src/ui/layout_delizia/flow/confirm_action.rs b/core/embed/rust/src/ui/layout_delizia/flow/confirm_action.rs
index 1ada3437..7f32b1bf 100644
--- a/core/embed/rust/src/ui/layout_delizia/flow/confirm_action.rs
+++ b/core/embed/rust/src/ui/layout_delizia/flow/confirm_action.rs
@@ -109,6 +109,7 @@ impl FlowController for ConfirmAction {
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Up) => self.return_msg(FlowMsg::Confirmed),
+ (Self::Action, Direction::Down) => self.return_msg(FlowMsg::Back),
_ => self.do_nothing(),
}
}
@@ -138,6 +139,7 @@ impl FlowController for ConfirmActionWithConfirmation {
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Up) => Self::Confirmation.swipe(direction),
+ (Self::Action, Direction::Down) => self.return_msg(FlowMsg::Back),
(Self::Confirmation, Direction::Down) => Self::Action.swipe(direction),
_ => self.do_nothing(),
}
@@ -170,6 +172,7 @@ impl FlowController for ConfirmActionWithMenu {
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Up) => self.return_msg(FlowMsg::Confirmed),
+ (Self::Action, Direction::Down) => self.return_msg(FlowMsg::Back),
_ => self.do_nothing(),
}
}
@@ -202,6 +205,7 @@ impl FlowController for ConfirmActionWithMenuAndConfirmation {
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Up) => Self::Confirmation.swipe(direction),
+ (Self::Action, Direction::Down) => self.return_msg(FlowMsg::Back),
(Self::Confirmation, Direction::Down) => Self::Action.swipe(direction),
_ => self.do_nothing(),
}
@@ -264,6 +268,7 @@ pub fn new_confirm_action(
},
ConfirmActionStrings::new(title, subtitle, None, prompt_screen.then_some(prompt_title)),
hold,
+ false,
None,
0,
false,
@@ -276,6 +281,7 @@ fn new_confirm_action_uni<T: Component + Paginate + MaybeTrace + 'static>(
extra: ConfirmActionExtra,
strings: ConfirmActionStrings,
hold: bool,
+ swipe_down: bool,
frame_margin: usize,
page_counter: bool,
) -> Result<SwipeFlow, error::Error> {
@@ -286,6 +292,9 @@ fn new_confirm_action_uni<T: Component + Paginate + MaybeTrace + 'static>(
.with_margin(frame_margin)
.with_swipeup_footer(strings.footer_description)
.with_vertical_pages();
+ if swipe_down {
+ content = content.with_swipe(Direction::Down, SwipeSettings::Default);
+ }
content = match extra {
ConfirmActionExtra::ExternalMenu => content.with_menu_button().with_external_menu(),
@@ -449,11 +458,13 @@ fn create_confirm(
}
#[inline(never)]
+#[allow(clippy::too_many_arguments)]
pub fn new_confirm_action_simple<T: Component + Paginate + MaybeTrace + 'static>(
content: T,
extra: ConfirmActionExtra,
strings: ConfirmActionStrings,
hold: bool,
+ swipe_down: bool,
page_limit: Option<u16>,
frame_margin: usize,
page_counter: bool,
@@ -463,6 +474,7 @@ pub fn new_confirm_action_simple<T: Component + Paginate + MaybeTrace + 'static>
extra,
strings,
hold,
+ swipe_down,
frame_margin,
page_counter,
)
diff --git a/core/embed/rust/src/ui/layout_delizia/flow/util.rs b/core/embed/rust/src/ui/layout_delizia/flow/util.rs
index d8695d95..053cde96 100644
--- a/core/embed/rust/src/ui/layout_delizia/flow/util.rs
+++ b/core/embed/rust/src/ui/layout_delizia/flow/util.rs
@@ -154,8 +154,8 @@ impl ConfirmValue {
self
}
- pub const fn with_swipe_down(mut self) -> Self {
- self.swipe_down = true;
+ pub const fn with_swipe_down(mut self, swipe_down: bool) -> Self {
+ self.swipe_down = swipe_down;
self
}
@@ -337,6 +337,7 @@ impl ConfirmValue {
)
.with_footer_description(self.footer_description),
self.hold,
+ self.swipe_down,
self.page_limit,
self.frame_margin,
self.page_counter,
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index fea246d8..1da86a3e 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -102,6 +102,7 @@ impl FirmwareUI for UIDelizia {
ConfirmActionExtra::ExternalMenu,
ConfirmActionStrings::new(title, Some(subtitle), None, None),
false,
+ false,
None,
0,
false,
@@ -123,7 +124,7 @@ impl FirmwareUI for UIDelizia {
page_counter: bool,
prompt_screen: bool,
cancel: bool,
- _back_button: bool,
+ back_button: bool,
_warning_footer: Option<TString<'static>>,
external_menu: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
@@ -146,6 +147,7 @@ impl FirmwareUI for UIDelizia {
.with_chunkify(chunkify)
.with_page_counter(page_counter)
.with_cancel(cancel)
+ .with_swipe_down(back_button)
.with_prompt(prompt_screen)
.with_external_menu(external_menu)
.with_hold(hold)
@@ -205,6 +207,7 @@ impl FirmwareUI for UIDelizia {
Some(TR::homescreen__settings_title.into()),
),
false,
+ false,
None,
0,
false,
@@ -241,6 +244,7 @@ impl FirmwareUI for UIDelizia {
Some(TR::coinjoin__title.into()),
),
true,
+ false,
None,
0,
false,
@@ -270,6 +274,7 @@ impl FirmwareUI for UIDelizia {
ConfirmActionExtra::Menu(ConfirmActionMenuStrings::new()),
ConfirmActionStrings::new(title, None, None, Some(title)),
false,
+ false,
None,
0,
false,
@@ -338,6 +343,7 @@ impl FirmwareUI for UIDelizia {
),
ConfirmActionStrings::new(title, None, None, Some(title)),
true,
+ false,
None,
0,
false,
@@ -460,6 +466,7 @@ impl FirmwareUI for UIDelizia {
ConfirmActionExtra::Menu(ConfirmActionMenuStrings::new()),
ConfirmActionStrings::new(title, subtitle, None, hold.then_some(title)),
hold,
+ false,
None,
0,
false,
@@ -500,6 +507,7 @@ impl FirmwareUI for UIDelizia {
),
ConfirmActionStrings::new(title, None, None, None).with_footer_description(Some(verb)),
false,
+ false,
None,
0,
false,
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 214caf2f..b934ef28 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -25,6 +25,7 @@ BR_CODE_OTHER = ButtonRequestType.Other # global_import_cache
CONFIRMED = trezorui_api.CONFIRMED
CANCELLED = trezorui_api.CANCELLED
INFO = trezorui_api.INFO
+BACK = trezorui_api.BACK
def confirm_action(
@@ -614,28 +615,33 @@ async def confirm_output(
]
else:
info_items = []
-
- await confirm_value(
- TR.words__address,
- address,
- description or "",
- "confirm_output",
- br_code,
- subtitle=title,
- chunkify=chunkify,
- cancel_text=TR.send__cancel_sign,
- info_items=info_items,
- )
- await confirm_value(
- TR.words__amount,
- amount,
- "",
- "confirm_output",
- br_code,
- subtitle=title,
- cancel_text=TR.send__cancel_sign,
- info_items=info_items,
- )
+ while True:
+ await confirm_value(
+ TR.words__address,
+ address,
+ description or "",
+ "confirm_output",
+ br_code,
+ subtitle=title,
+ chunkify=chunkify,
+ cancel_text=TR.send__cancel_sign,
+ info_items=info_items,
+ )
+ amount_response = await confirm_value(
+ TR.words__amount,
+ amount,
+ description="",
+ br_name="confirm_output",
+ br_code=br_code,
+ subtitle=title,
+ cancel_text=TR.send__cancel_sign,
+ info_items=info_items,
+ can_go_back=True,
+ )
+ if amount_response is BACK:
+ continue
+ else:
+ break
else:
await raise_if_not_confirmed(
trezorui_api.flow_confirm_output(
@@ -834,6 +840,7 @@ def confirm_value(
verb: str | None = None,
subtitle: str | None = None,
hold: bool = False,
+ can_go_back: bool = False,
is_data: bool = True,
chunkify: bool = False,
info_items: (
@@ -856,6 +863,7 @@ def confirm_value(
hold=hold,
chunkify=chunkify,
cancel=cancel,
+ back_button=can_go_back,
external_menu=True,
)
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index 1b665d1d..5d3cd53d 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -636,10 +636,10 @@ async def confirm_output(
external_menu=True,
back_button=True,
)
- if (
- await interact_with_menu(amount_layout, menu, "confirm_output", br_code)
- is BACK
- ):
+ amount_response = await interact_with_menu(
+ amount_layout, menu, "confirm_output", br_code
+ )
+ if amount_response is BACK:
continue
else:
break
diff --git a/tests/device_tests/bitcoin/test_signtx.py b/tests/device_tests/bitcoin/test_signtx.py
index 0d7a5e97..ec3c4570 100644
--- a/tests/device_tests/bitcoin/test_signtx.py
+++ b/tests/device_tests/bitcoin/test_signtx.py
@@ -158,7 +158,7 @@ def test_one_one_fee(session: Session):
)
-@pytest.mark.models("t3w1")
+@pytest.mark.models("t3w1", "t3t1")
def test_one_one_fee_back_from_amount(session: Session):
# input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5
diff --git a/tests/input_flows.py b/tests/input_flows.py
index 43fc94d0..3f2bb880 100644
--- a/tests/input_flows.py
+++ b/tests/input_flows.py
@@ -1208,17 +1208,50 @@ class InputFlowSignTxBackFromAmount(InputFlowBase):
def __init__(self, client: Client):
super().__init__(client)
- def input_flow_eckhart(self) -> BRGeneratorType:
- yield # confirm address
+ def input_flow_delizia(self) -> BRGeneratorType:
+ yield
+ layout = self.debug.read_layout()
+ assert TR.words__address in layout.title()
+ assert TR.words__recipient + " #1" in layout.title()
+ self.debug.swipe_up()
+
+ yield
+ layout = self.debug.read_layout()
+ assert TR.words__amount in layout.title()
+ assert TR.words__recipient + " #1" in layout.title()
+ self.debug.swipe_down()
+
+ yield
+ layout = self.debug.read_layout()
+ assert TR.words__address in layout.title()
+ assert TR.words__recipient + " #1" in layout.title()
+ self.debug.swipe_up()
+
+ yield
self.debug.read_layout()
- self.debug.click(self.debug.screen_buttons.ok())
+ self.debug.swipe_up()
- yield # confirm amount
+ yield
self.debug.read_layout()
+ self.debug.press_yes()
+
+ def input_flow_eckhart(self) -> BRGeneratorType:
+ yield
+ layout = self.debug.read_layout()
+ assert TR.words__send in layout.title()
+ assert TR.words__recipient + " #1" in layout.title()
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ yield
+ layout = self.debug.read_layout()
+ assert TR.words__amount in layout.text_content()
+ assert TR.words__recipient + " #1" in layout.title()
self.debug.click(self.debug.screen_buttons.cancel())
yield
- self.debug.read_layout()
+ layout = self.debug.read_layout()
+ assert TR.words__send in layout.title()
+ assert TR.words__recipient + " #1" in layout.title()
self.debug.click(self.debug.screen_buttons.ok())
yield
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.