feat(eckhart): external menu on `confirm_summary`
What changed, and why it matters
This commit updates the user interface code for the Trezor hardware wallet's newer 'Eckhart' layout. It adds support for an optional 'external menu' on the transaction summary confirmation screen, replacing the older internal menu when requested. The change is a UI refactor and feature flag wiring; it does not appear to fix or introduce a security vulnerability.
No security action required. Treat as normal feature/refactor commit. If reviewing for release readiness, verify that the new external menu path is covered by device tests and that the `NotImplementedError` fallback is acceptable for callers that still supply account/extra info.
Security signals we found
No security-relevant keywords in commit title or message
No changelog entry provided
Change is purely UI layout/flow plumbing
Adds a debug-only assertion to catch inconsistent menu flags
No evidence of vulnerability disclosure or CVE association
Evidence from the diff
The patch modifies the Eckhart UI layout in the Trezor firmware. It changes with_flow_menu() to accept a boolean, adds an external_menu parameter to new_confirm_summary(), and returns a single-page flow when external_menu is true. It also adds a guard that returns NotImplementedError if external menu is requested together with account/extra info paragraphs. A debug_assert! ensures external_menu and has_flow_menu are not both true. The change is architectural UI plumbing, not a security fix.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rscore/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rscore/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rsInspect captured patch +35 / −15
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs
index 42872eff..ca9559ea 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs
@@ -131,13 +131,13 @@ where
// Once we have eventually replaced all these with new style "external menu",
// we should get rid of this flag and the related debuglink code.
#[cfg(feature = "ui_debug")]
- pub fn with_flow_menu(mut self) -> Self {
+ pub fn with_flow_menu(mut self, has_flow_menu: bool) -> Self {
// Allow visiting this menu automatically by tests
- self.has_flow_menu = true;
+ self.has_flow_menu = has_flow_menu;
self
}
#[cfg(not(feature = "ui_debug"))]
- pub fn with_flow_menu(self) -> Self {
+ pub fn with_flow_menu(self, _has_flow_menu: bool) -> Self {
self
}
@@ -403,6 +403,8 @@ where
t.int("page_limit", page_limit as i64);
}
t.int("page_count", self.content.pager().total() as i64);
+
+ debug_assert!(!(self.external_menu && self.has_flow_menu));
t.bool("has_menu", self.external_menu);
t.bool("has_flow_menu", self.has_flow_menu);
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
index 2546f1ac..f9f06b59 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
@@ -230,7 +230,7 @@ pub fn new_confirm_output(
TextScreen::new(main_paragraphs.into_paragraphs().with_placement(
LinearPlacement::vertical().with_spacing(theme::TEXT_VERTICAL_SPACING),
))
- .with_flow_menu()
+ .with_flow_menu(true)
.with_header(Header::new(title.unwrap_or(TString::empty())).with_menu_button())
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.with_hint(Hint::new_page_counter())
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs
index 30680a3a..1f2a70c2 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_summary.rs
@@ -25,6 +25,7 @@ use super::super::{
ActionBar, Header, Hint, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu,
VerticalMenuScreen, VerticalMenuScreenMsg,
},
+ flow,
flow::util::content_menu_info,
theme::{self, gradient::Gradient},
};
@@ -35,8 +36,10 @@ const MENU_ITEM_ACCOUNT_INFO: usize = 2;
const TIMEOUT: Duration = Duration::from_secs(2);
+// TODO: this should eventually disappear as we will use external menu
+// everywhere
#[derive(Copy, Clone, PartialEq, Eq)]
-pub enum ConfirmSummary {
+pub enum ConfirmSummaryWithMenu {
Summary,
Menu,
ExtraInfo,
@@ -45,7 +48,7 @@ pub enum ConfirmSummary {
Cancelled,
}
-impl FlowController for ConfirmSummary {
+impl FlowController for ConfirmSummaryWithMenu {
#[inline]
fn index(&'static self) -> usize {
*self as usize
@@ -85,7 +88,16 @@ pub fn new_confirm_summary(
extra_paragraphs: Option<PropsList>,
verb_cancel: Option<TString<'static>>,
back_button: bool,
+ external_menu: bool,
) -> Result<SwipeFlow, error::Error> {
+ if external_menu
+ && (account_title.is_some()
+ || account_paragraphs.is_some()
+ || extra_title.is_some()
+ || extra_paragraphs.is_some())
+ {
+ return Err(error::Error::NotImplementedError);
+ }
// Summary
let mut summary_paragraphs = ParagraphVecShort::new();
if let Some(amount_label) = amount_label {
@@ -123,7 +135,8 @@ pub fn new_confirm_summary(
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(title).with_menu_button())
- .with_flow_menu()
+ .with_flow_menu(!external_menu)
+ .with_external_menu(external_menu)
.with_action_bar(if back_button {
ActionBar::new_double(Button::with_icon(theme::ICON_CHEVRON_UP), confirm_button)
} else {
@@ -143,6 +156,10 @@ pub fn new_confirm_summary(
TextScreenMsg::Menu => Some(FlowMsg::Info),
});
+ if external_menu {
+ return flow::util::single_page(content_summary);
+ }
+
// Menu
let mut menu = VerticalMenu::<ShortMenuVec>::empty();
let mut menu_items = Vec::<usize, 3>::new();
@@ -224,13 +241,13 @@ pub fn new_confirm_summary(
.with_page_limit(1)
.map(|_| Some(FlowMsg::Confirmed));
- let mut res = SwipeFlow::new(&ConfirmSummary::Summary)?;
- res.add_page(&ConfirmSummary::Summary, content_summary)?
- .add_page(&ConfirmSummary::Menu, content_menu)?
- .add_page(&ConfirmSummary::ExtraInfo, content_extra)?
- .add_page(&ConfirmSummary::AccountInfo, content_account)?
- .add_page(&ConfirmSummary::Cancel, content_cancel)?
- .add_page(&ConfirmSummary::Cancelled, content_cancelled)?;
+ let mut res = SwipeFlow::new(&ConfirmSummaryWithMenu::Summary)?;
+ res.add_page(&ConfirmSummaryWithMenu::Summary, content_summary)?
+ .add_page(&ConfirmSummaryWithMenu::Menu, content_menu)?
+ .add_page(&ConfirmSummaryWithMenu::ExtraInfo, content_extra)?
+ .add_page(&ConfirmSummaryWithMenu::AccountInfo, content_account)?
+ .add_page(&ConfirmSummaryWithMenu::Cancel, content_cancel)?
+ .add_page(&ConfirmSummaryWithMenu::Cancelled, content_cancelled)?;
Ok(res)
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index d76104bd..413a84f5 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -346,7 +346,7 @@ impl FirmwareUI for UIEckhart {
extra_title: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
back_button: bool,
- _external_menu: bool, // TODO: will eventually replace the internal menu
+ external_menu: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
// collect available info
let account_paragraphs = if let Some(items) = account_items {
@@ -372,6 +372,7 @@ impl FirmwareUI for UIEckhart {
extra_paragraphs,
verb_cancel,
back_button,
+ external_menu,
)?;
Ok(flow)
}
Why this scored 17/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.