chore(core/eckhart): redesign backup needed menu
What changed, and why it matters
This commit is a user-interface redesign for the Trezor hardware wallet's 'backup needed' menu on the Eckhart model. It removes the ability to start a backup directly from the device menu and instead shows an informational screen telling the user to open Trezor Suite to create a backup. There is no security vulnerability here; it is a routine product change.
No security action required. Treat as a normal UI/UX change during review and testing.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors the Eckhart layout’s device menu: it removes the BackupDevice result/message from the Rust UI API and device menu result enum, deletes the Python handler that invoked apps.management.backup_device, and adds a new BackupInfoScreen subscreen that displays a static informational paragraph (homescreen__backup_needed_info). Translation files and their signatures are updated accordingly. No cryptographic, authentication, or memory-safety code is modified.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/src/apps/homescreen/device_menu.pycore/embed/rust/src/ui/layout/device_menu_result.rscore/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_eckhart/component_msg_obj.rscore/translations/en.jsoncore/translations/signatures.jsonInspect captured patch +49 / −29
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 8ae7281b..4f65f0c8 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -21,7 +21,6 @@ static void _librust_qstrs(void) {
MP_QSTR_BACK;
MP_QSTR_BLEIF;
MP_QSTR_BacklightLevels;
- MP_QSTR_BackupDevice;
MP_QSTR_CANCELLED;
MP_QSTR_CONFIRMED;
MP_QSTR_CheckBackup;
@@ -356,6 +355,7 @@ static void _librust_qstrs(void) {
MP_QSTR_hint;
MP_QSTR_hold;
MP_QSTR_hold_danger;
+ MP_QSTR_homescreen__backup_needed_info;
MP_QSTR_homescreen__click_to_connect;
MP_QSTR_homescreen__click_to_unlock;
MP_QSTR_homescreen__firmware_type;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index e4f141f0..f4287518 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1549,6 +1549,7 @@ pub enum TranslatedString {
pin__wipe_code_exists_title = 1157, // "Wipe code set"
wipe_code__pin_not_set_description = 1158, // "PIN must be set before enabling wipe code."
wipe_code__cancel_setup = 1159, // {"Bolt": "Cancel wipe code setup", "Caesar": "Cancel wipe code setup", "Delizia": "Cancel wipe code setup", "Eckhart": "Cancel wipe code setup?"}
+ homescreen__backup_needed_info = 1160, // "Open Trezor Suite and create a wallet backup. This is the only way to recover access to your assets."
}
impl TranslatedString {
@@ -3542,6 +3543,7 @@ impl TranslatedString {
(Self::wipe_code__cancel_setup, "Cancel wipe code setup"),
#[cfg(feature = "layout_eckhart")]
(Self::wipe_code__cancel_setup, "Cancel wipe code setup?"),
+ (Self::homescreen__backup_needed_info, "Open Trezor Suite and create a wallet backup. This is the only way to recover access to your assets."),
];
#[cfg(feature = "micropython")]
@@ -4185,6 +4187,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_haptic_feedback__enable, Self::haptic_feedback__enable),
(Qstr::MP_QSTR_haptic_feedback__subtitle, Self::haptic_feedback__subtitle),
(Qstr::MP_QSTR_haptic_feedback__title, Self::haptic_feedback__title),
+ (Qstr::MP_QSTR_homescreen__backup_needed_info, Self::homescreen__backup_needed_info),
(Qstr::MP_QSTR_homescreen__click_to_connect, Self::homescreen__click_to_connect),
(Qstr::MP_QSTR_homescreen__click_to_unlock, Self::homescreen__click_to_unlock),
(Qstr::MP_QSTR_homescreen__firmware_type, Self::homescreen__firmware_type),
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 78eea09b..86679415 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -2143,7 +2143,6 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// class DeviceMenuResult:
/// """Result of a device menu operation."""
/// ReviewFailedBackup: ClassVar[DeviceMenuResult]
- /// BackupDevice: ClassVar[DeviceMenuResult]
/// DisconnectDevice: ClassVar[DeviceMenuResult]
/// PairDevice: ClassVar[DeviceMenuResult]
/// UnpairDevice: ClassVar[DeviceMenuResult]
diff --git a/core/embed/rust/src/ui/layout/device_menu_result.rs b/core/embed/rust/src/ui/layout/device_menu_result.rs
index 94b53d89..29f2df94 100644
--- a/core/embed/rust/src/ui/layout/device_menu_result.rs
+++ b/core/embed/rust/src/ui/layout/device_menu_result.rs
@@ -42,7 +42,6 @@ static DEVICE_MENU_RESULT_TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_DeviceMenuResult,
locals: &obj_dict! { obj_map! {
Qstr::MP_QSTR_ReviewFailedBackup => REVIEW_FAILED_BACKUP.as_obj(),
- Qstr::MP_QSTR_BackupDevice => BACKUP_DEVICE.as_obj(),
Qstr::MP_QSTR_PairDevice => PAIR_DEVICE.as_obj(),
Qstr::MP_QSTR_DisconnectDevice => DISCONNECT_DEVICE.as_obj(),
Qstr::MP_QSTR_UnpairDevice => UNPAIR_DEVICE.as_obj(),
diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
index 6b33a52a..7a38257c 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
@@ -159,7 +159,6 @@ impl ComponentMsgObj for DeviceMenuScreen {
match msg {
// Root menu
DeviceMenuMsg::ReviewFailedBackup => Ok(REVIEW_FAILED_BACKUP.as_obj()),
- DeviceMenuMsg::BackupDevice => Ok(BACKUP_DEVICE.as_obj()),
// "Pair & Connect"
DeviceMenuMsg::PairDevice => Ok(PAIR_DEVICE.as_obj()),
DeviceMenuMsg::DisconnectDevice => Ok(DISCONNECT_DEVICE.as_obj()),
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
index 53ffacc6..6e7876dd 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
@@ -11,7 +11,7 @@ use crate::{
ui::{
component::{
text::{
- paragraphs::{ParagraphSource, Paragraphs},
+ paragraphs::{Paragraph, ParagraphSource, Paragraphs},
TextStyle,
},
Component, Event, EventCtx,
@@ -108,7 +108,6 @@ enum Action {
pub enum DeviceMenuMsg {
// Root menu
ReviewFailedBackup,
- BackupDevice,
// "Pair & Connect"
PairDevice, // pair a new device
@@ -269,6 +268,8 @@ enum Subscreen {
AboutScreen,
// A screen showing the regulatory information
RegulatoryScreen,
+ // A screen showing information about the backup
+ BackupInfoScreen,
}
// Used to preallocate memory for the largest enum variant
@@ -277,6 +278,7 @@ enum ActiveScreen {
Menu(VerticalMenuScreen<MediumMenuVec>, DeviceMenuId),
Device(VerticalMenuScreen<ShortMenuVec>),
About(TextScreen<Paragraphs<PropsList>>),
+ BackupInfo(TextScreen<Paragraphs<Paragraph<'static>>>),
Regulatory(RegulatoryScreen),
// used only during `DeviceMenuScreen::new`
@@ -659,12 +661,11 @@ impl DeviceMenuScreen {
}
if backup_needed {
- let item = MenuItem::return_msg(
- TR::homescreen__title_backup_needed.into(),
- DeviceMenuMsg::BackupDevice,
- )
- .with_subtext(Some((TR::words__review.into(), None)))
- .light_warn();
+ let backup_idx = self.add_subscreen(Subscreen::BackupInfoScreen);
+ let item =
+ MenuItem::go_to_subscreen(TR::homescreen__title_backup_needed.into(), backup_idx)
+ .with_subtext(Some((TR::words__review.into(), None)))
+ .light_warn();
items.add(item);
}
@@ -820,6 +821,21 @@ impl DeviceMenuScreen {
Subscreen::RegulatoryScreen => {
*self.active_screen.deref_mut() = ActiveScreen::Regulatory(RegulatoryScreen::new());
}
+ Subscreen::BackupInfoScreen => {
+ *self.active_screen.deref_mut() = ActiveScreen::BackupInfo(
+ TextScreen::new(
+ Paragraph::new(&theme::TEXT_REGULAR, TR::homescreen__backup_needed_info)
+ .into_paragraphs()
+ .with_placement(LinearPlacement::vertical()),
+ )
+ .with_header(
+ Header::new(TR::homescreen__title_backup_needed.into())
+ .with_icon(theme::ICON_INFO, theme::YELLOW)
+ .with_text_style(theme::label_title_warning())
+ .with_close_button(),
+ ),
+ );
+ }
}
}
@@ -864,6 +880,7 @@ impl DeviceMenuScreen {
},
Subscreen::DeviceScreen(..) => DeviceMenuId::PairAndConnect,
Subscreen::AboutScreen | Subscreen::RegulatoryScreen => DeviceMenuId::Device,
+ Subscreen::BackupInfoScreen => DeviceMenuId::Root,
};
self.activate_subscreen(unwrap!(self.try_resolve_submenu(parent)), ctx);
@@ -894,6 +911,9 @@ impl Component for DeviceMenuScreen {
ActiveScreen::Regulatory(regulatory) => {
regulatory.place(bounds);
}
+ ActiveScreen::BackupInfo(backup_info) => {
+ backup_info.place(bounds);
+ }
ActiveScreen::Empty => {}
};
@@ -910,7 +930,7 @@ impl Component for DeviceMenuScreen {
ActiveScreen::Menu(_, id) => *id,
ActiveScreen::Device(_) => DeviceMenuId::PairAndConnect,
ActiveScreen::Regulatory(_) | ActiveScreen::About(_) => DeviceMenuId::Device,
- ActiveScreen::Empty => DeviceMenuId::Root,
+ ActiveScreen::Empty | ActiveScreen::BackupInfo(_) => DeviceMenuId::Root,
};
return Some(DeviceMenuMsg::RefreshMenu(submenu_idx));
@@ -962,6 +982,11 @@ impl Component for DeviceMenuScreen {
return self.go_back(ctx);
}
}
+ (Subscreen::BackupInfoScreen, ActiveScreen::BackupInfo(backup_info)) => {
+ if let Some(TextScreenMsg::Cancelled) = backup_info.event(ctx, event) {
+ return self.go_back(ctx);
+ }
+ }
_ => {}
}
@@ -974,6 +999,7 @@ impl Component for DeviceMenuScreen {
ActiveScreen::Device(device) => device.render(target),
ActiveScreen::About(about) => about.render(target),
ActiveScreen::Regulatory(regulatory) => regulatory.render(target),
+ ActiveScreen::BackupInfo(backup_info) => backup_info.render(target),
ActiveScreen::Empty => {}
};
}
@@ -997,6 +1023,9 @@ impl crate::trace::Trace for DeviceMenuScreen {
ActiveScreen::Regulatory(ref screen) => {
t.child("Regulatory", screen);
}
+ ActiveScreen::BackupInfo(ref screen) => {
+ t.child("BackupInfo", screen);
+ }
ActiveScreen::Empty => {
t.null("ActiveScreen");
}
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index d54155fc..0e415243 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -866,7 +866,6 @@ class LayoutState:
class DeviceMenuResult:
"""Result of a device menu operation."""
ReviewFailedBackup: ClassVar[DeviceMenuResult]
- BackupDevice: ClassVar[DeviceMenuResult]
DisconnectDevice: ClassVar[DeviceMenuResult]
PairDevice: ClassVar[DeviceMenuResult]
UnpairDevice: ClassVar[DeviceMenuResult]
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index 9d0f8297..26df5d22 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -405,6 +405,7 @@ class TR:
haptic_feedback__enable: str = "Enable haptic feedback?"
haptic_feedback__subtitle: str = "Setting"
haptic_feedback__title: str = "Haptic feedback"
+ homescreen__backup_needed_info: str = "Open Trezor Suite and create a wallet backup. This is the only way to recover access to your assets."
homescreen__click_to_connect: str = "Click to Connect"
homescreen__click_to_unlock: str = "Click to Unlock"
homescreen__firmware_type: str = "Firmware type"
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index b1131d10..af7c50da 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -168,17 +168,6 @@ async def handle_device_menu() -> None:
init_submenu_idx = SubmenuId.ROOT
else:
break
- elif menu_result is DeviceMenuResult.BackupDevice and backup_needed:
- from trezor.messages import BackupDevice
-
- from apps.management.backup_device import backup_device
-
- try:
- await backup_device(BackupDevice())
- except ActionCancelled:
- init_submenu_idx = SubmenuId.ROOT
- else:
- break
# Pair & Connect
elif menu_result is DeviceMenuResult.DisconnectDevice and ble.is_connected():
init_submenu_idx = SubmenuId.PAIR_AND_CONNECT
diff --git a/core/translations/en.json b/core/translations/en.json
index a3ebe97c..11720d07 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -467,6 +467,7 @@
"haptic_feedback__enable": "Enable haptic feedback?",
"haptic_feedback__subtitle": "Setting",
"haptic_feedback__title": "Haptic feedback",
+ "homescreen__backup_needed_info": "Open Trezor Suite and create a wallet backup. This is the only way to recover access to your assets.",
"homescreen__click_to_connect": "Click to Connect",
"homescreen__click_to_unlock": "Click to Unlock",
"homescreen__firmware_type": "Firmware type",
diff --git a/core/translations/order.json b/core/translations/order.json
index 37551665..d7730659 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1158,5 +1158,6 @@
"1156": "pin__wipe_code_exists_description",
"1157": "pin__wipe_code_exists_title",
"1158": "wipe_code__pin_not_set_description",
- "1159": "wipe_code__cancel_setup"
+ "1159": "wipe_code__cancel_setup",
+ "1160": "homescreen__backup_needed_info"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index f82c2c4a..b160b9e9 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "edf1dbd19709f1784a4e6208b3d31ce23edb55393ce0f4e822a4ece391704012",
- "datetime": "2025-09-17T13:53:25.542717+00:00",
- "commit": "bb54085ae22deb63f5443898e20910dbb1bc0dfa"
+ "merkle_root": "deef70a9cdf095544939b05919db364ed47321e65cba0411320459d606a9ccca",
+ "datetime": "2025-09-18T17:55:50.239847+00:00",
+ "commit": "b8ef2aa324a1f148f796157a05549dc24c6666e5"
},
"history": [
{
Why this scored 15/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.