Skip to content

Commit 5e96d1e

Browse files
committed
wizard: add Trezor Safe 7 support
The Trezor Safe 7 speaks the Trezor Host Protocol v2 rather than the legacy v1 wire protocol. With THP implemented in the wallet, the GUI needs three things to drive it. A pairing dialog for the THP CodeEntry step: on the first pairing the device displays a six-digit code and the host has to echo it back. The dialog collects it, accepts the spacing and dashes a paste brings with it, and reports a rejected code in place so the user can try again instead of dead-ending the wizard. The Safe 7 as a selectable model in the create-wallet-from-device wizard, with its own image. Classification of Trezor connect and open failures, so the loading screen can offer a retry hint when the device is merely off, locked or not yet paired, rather than the "check application logs" error that every failure produces today. Cancelling, on the device or in the dialog, now ends the wizard quietly instead of as an error, which is what the second argument to walletCreatedFromDevice carries. C++17 is set unconditionally rather than only for DEV_MODE checkouts, because the monero revision carrying THP requires it. That hunk stands on its own and can be dropped if it would rather go in with the submodule bump. No submodule bump is included here. The matching monero change is a separate pull request, and moving the pointer before that merges would pin the GUI to a personal fork.
1 parent 687f129 commit 5e96d1e

18 files changed

Lines changed: 841 additions & 24 deletions

CMakeLists.txt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ option(WITH_UPDATER "Regularly check for new updates" ON)
1919
option(DEV_MODE "Checkout latest monero master on build" OFF)
2020
cmake_dependent_option(QML_TESTS "Build QML tests" ON "NOT STATIC;NOT ANDROID;NOT IOS" OFF)
2121

22-
if(DEV_MODE)
23-
# DEV_MODE checks out the monero submodule to master, which requires C++17.
24-
set(CMAKE_CXX_STANDARD 17)
25-
else()
26-
set(CMAKE_CXX_STANDARD 14)
27-
endif()
22+
set(CMAKE_CXX_STANDARD 17)
2823
set(CMAKE_CXX_STANDARD_REQUIRED ON)
2924

3025
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_SOURCE_DIR}/cmake")
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright (c) 2026, The Monero Project
2+
//
3+
// All rights reserved.
4+
//
5+
// Redistribution and use in source and binary forms, with or without modification, are
6+
// permitted provided that the following conditions are met:
7+
//
8+
// 1. Redistributions of source code must retain the above copyright notice, this list of
9+
// conditions and the following disclaimer.
10+
//
11+
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
12+
// of conditions and the following disclaimer in the documentation and/or other
13+
// materials provided with the distribution.
14+
//
15+
// 3. Neither the name of the copyright holder nor the names of its contributors may be
16+
// used to endorse or promote products derived from this software without specific
17+
// prior written permission.
18+
//
19+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
20+
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
21+
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22+
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24+
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26+
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
27+
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
29+
import QtQuick 2.9
30+
import QtQuick.Controls 2.0
31+
import QtQuick.Layouts 1.1
32+
33+
import "../components" as MoneroComponents
34+
35+
// Modal that prompts the user for the 6-digit pairing code shown on a
36+
// Trezor Safe 7 during the THP CodeEntry pairing flow.
37+
FocusScope {
38+
id: root
39+
visible: false
40+
41+
// onAcceptedCallback is invoked with the entered code (string of 6
42+
// ASCII digits). onRejectedCallback is invoked when the user cancels.
43+
// Both are cleared once one of them has fired.
44+
property var onAcceptedCallback
45+
property var onRejectedCallback
46+
47+
// Message shown above the input, set by the caller when a previous
48+
// code was rejected by the device.
49+
property string errorText
50+
51+
function open() {
52+
// Same modal behaviour as PasswordDialog: a wallet pool thread
53+
// is blocked waiting for the code, so the UI behind the dialog
54+
// must not accept input (e.g. starting a second wallet open
55+
// would hang forever on the same device).
56+
leftPanel.enabled = false;
57+
middlePanel.enabled = false;
58+
wizard.enabled = false;
59+
titleBar.state = "essentials";
60+
root.visible = true;
61+
codeInput.text = "";
62+
codeInput.forceActiveFocus();
63+
}
64+
65+
function close() {
66+
leftPanel.enabled = true;
67+
middlePanel.enabled = true;
68+
wizard.enabled = !wizard.deviceWalletCreationInProgress;
69+
if (rootItem.state == "wizard") {
70+
titleBar.state = "essentials";
71+
} else {
72+
titleBar.state = "default";
73+
}
74+
root.visible = false;
75+
root.errorText = "";
76+
}
77+
78+
function onOk() {
79+
if (codeInput.text.length !== 6) {
80+
return;
81+
}
82+
var entered = codeInput.text;
83+
var callback = root.onAcceptedCallback;
84+
root.onAcceptedCallback = null;
85+
root.onRejectedCallback = null;
86+
root.close();
87+
if (callback) {
88+
callback(entered);
89+
}
90+
}
91+
92+
function onCancel() {
93+
if (!root.visible) {
94+
return;
95+
}
96+
var callback = root.onRejectedCallback;
97+
root.onAcceptedCallback = null;
98+
root.onRejectedCallback = null;
99+
root.close();
100+
if (callback) {
101+
callback();
102+
}
103+
}
104+
105+
Keys.enabled: root.visible
106+
Keys.onEscapePressed: root.onCancel()
107+
108+
ColumnLayout {
109+
id: mainLayout
110+
spacing: 10
111+
anchors.fill: parent
112+
anchors.margins: 35
113+
114+
ColumnLayout {
115+
id: column
116+
117+
Layout.fillWidth: true
118+
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
119+
Layout.maximumWidth: 480
120+
121+
Label {
122+
Layout.fillWidth: true
123+
text: qsTr("Trezor Safe 7 - pair this computer") + translationManager.emptyString
124+
font.pixelSize: 18
125+
font.family: MoneroComponents.Style.fontBold.name
126+
font.bold: true
127+
color: MoneroComponents.Style.defaultFontColor
128+
}
129+
130+
Label {
131+
Layout.fillWidth: true
132+
Layout.topMargin: 12
133+
text: qsTr("Your Trezor is showing a 6-digit code on its screen. " +
134+
"Type that exact code here to confirm this computer is allowed to talk to it. " +
135+
"After this one-time pairing, the computer is remembered and you won't be asked again.") + translationManager.emptyString
136+
font.pixelSize: 14
137+
font.family: MoneroComponents.Style.fontLight.name
138+
color: MoneroComponents.Style.defaultFontColor
139+
wrapMode: Text.WordWrap
140+
}
141+
142+
Label {
143+
id: errorTextLabel
144+
visible: text !== ""
145+
text: root.errorText
146+
Layout.fillWidth: true
147+
Layout.topMargin: 12
148+
font.pixelSize: 14
149+
font.family: MoneroComponents.Style.fontLight.name
150+
color: MoneroComponents.Style.errorColor
151+
wrapMode: Text.WordWrap
152+
}
153+
154+
MoneroComponents.Input {
155+
id: codeInput
156+
focus: true
157+
Layout.topMargin: 16
158+
Layout.fillWidth: true
159+
horizontalAlignment: TextInput.AlignHCenter
160+
verticalAlignment: TextInput.AlignVCenter
161+
font.family: MoneroComponents.Style.fontBold.name
162+
font.pixelSize: 32
163+
font.letterSpacing: 6
164+
bottomPadding: 12
165+
leftPadding: 10
166+
topPadding: 12
167+
color: MoneroComponents.Style.defaultFontColor
168+
selectionColor: MoneroComponents.Style.textSelectionColor
169+
selectedTextColor: MoneroComponents.Style.textSelectedColor
170+
inputMethodHints: Qt.ImhDigitsOnly
171+
172+
// js replacement for `RegExpValidator { regExp: /[0-9]{0,6}/ }`,
173+
// which rejects a paste outright: a code copied as
174+
// "12 34 56" or "123-456" would insert nothing at all.
175+
onTextChanged: {
176+
var digits = codeInput.text.replace(/[^0-9]/g, "").substring(0, 6);
177+
if (digits !== codeInput.text) {
178+
codeInput.text = digits;
179+
codeInput.cursorPosition = digits.length;
180+
}
181+
}
182+
183+
background: Rectangle {
184+
radius: 2
185+
border.color: MoneroComponents.Style.inputBorderColorActive
186+
border.width: 1
187+
color: MoneroComponents.Style.blackTheme ? "black" : "#A9FFFFFF"
188+
}
189+
190+
Keys.enabled: root.visible
191+
Keys.onEnterPressed: root.onOk()
192+
Keys.onReturnPressed: root.onOk()
193+
Keys.onEscapePressed: root.onCancel()
194+
}
195+
196+
Label {
197+
Layout.fillWidth: true
198+
Layout.topMargin: 8
199+
text: qsTr("Tip: nobody, not even a fake Trezor, can guess this code. " +
200+
"If you mistype it, pairing starts over and the device shows a fresh code.") + translationManager.emptyString
201+
font.pixelSize: 12
202+
font.italic: true
203+
font.family: MoneroComponents.Style.fontLight.name
204+
color: MoneroComponents.Style.dimmedFontColor
205+
wrapMode: Text.WordWrap
206+
}
207+
208+
RowLayout {
209+
spacing: 16
210+
Layout.topMargin: 16
211+
Layout.alignment: Qt.AlignRight
212+
213+
MoneroComponents.StandardButton {
214+
primary: false
215+
small: true
216+
width: 120
217+
fontSize: 14
218+
text: qsTr("Cancel") + translationManager.emptyString
219+
onClicked: root.onCancel()
220+
}
221+
MoneroComponents.StandardButton {
222+
small: true
223+
width: 120
224+
fontSize: 14
225+
text: qsTr("Confirm") + translationManager.emptyString
226+
enabled: codeInput.text.length === 6
227+
onClicked: root.onOk()
228+
}
229+
}
230+
}
231+
}
232+
}

components/ProcessingSplash.qml

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,74 @@ Rectangle {
4343
border.width: 1
4444
z: 11
4545
property alias messageText: messageTitle.text
46+
property alias subMessageText: messageSub.text
47+
48+
// Optional Retry / Cancel row, shown instead of leaving the user
49+
// stuck on a splash they can't dismiss. To use it, set
50+
// retryCallback and cancelCallback, set showActionButtons, then
51+
// call show(). Leaving retryCallback null hides Retry.
52+
property bool showActionButtons: false
53+
property var retryCallback: null
54+
property var cancelCallback: null
4655

4756
width: 100
4857
height: 50
4958

59+
focus: visible && showActionButtons
60+
Keys.onPressed: {
61+
if (!root.showActionButtons) return;
62+
if (event.key === Qt.Key_Escape) {
63+
root.fireCancel();
64+
event.accepted = true;
65+
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
66+
if (cancelButton.activeFocus || !retryButton.visible) {
67+
root.fireCancel();
68+
} else {
69+
root.fireRetry();
70+
}
71+
event.accepted = true;
72+
}
73+
}
74+
5075
function show() {
5176
root.visible = true;
77+
if (root.showActionButtons) {
78+
if (retryButton.visible) {
79+
retryButton.forceActiveFocus();
80+
} else {
81+
cancelButton.forceActiveFocus();
82+
}
83+
}
5284
}
5385

5486
function close() {
5587
root.visible = false;
88+
root.showActionButtons = false;
89+
root.retryCallback = null;
90+
root.cancelCallback = null;
91+
}
92+
93+
function fireRetry() {
94+
var cb = root.retryCallback;
95+
root.close();
96+
if (cb) cb();
97+
}
98+
99+
function fireCancel() {
100+
var cb = root.cancelCallback;
101+
root.close();
102+
if (cb) cb();
56103
}
57104

58105
ColumnLayout {
59106
id: rootLayout
60107

61-
anchors.centerIn: parent
62-
108+
// Anchor to the splash sides so a wrapping sub-message stays
109+
// inside the box; anchors.centerIn leaves the layout width
110+
// unconstrained and long messages spill outside.
111+
anchors.left: parent.left
112+
anchors.right: parent.right
113+
anchors.verticalCenter: parent.verticalCenter
63114
anchors.leftMargin: 30
64115
anchors.rightMargin: 30
65116

@@ -79,7 +130,9 @@ Rectangle {
79130
}
80131

81132
BusyIndicator {
82-
running: parent.visible
133+
// Nothing is in progress while the action row waits for
134+
// the user, so the spinner stops.
135+
running: parent.visible && !root.showActionButtons
83136
anchors.centerIn: imgLogo
84137
style: BusyIndicatorStyle {
85138
indicator: Image {
@@ -108,5 +161,49 @@ Rectangle {
108161
themeTransition: false
109162
color: MoneroComponents.Style.defaultFontColor
110163
}
164+
165+
MoneroComponents.TextPlain {
166+
id: messageSub
167+
text: ""
168+
visible: text.length > 0
169+
font.pixelSize: 15
170+
horizontalAlignment: Text.AlignHCenter
171+
wrapMode: Text.WordWrap
172+
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
173+
Layout.fillWidth: true
174+
Layout.leftMargin: 10
175+
Layout.rightMargin: 10
176+
themeTransition: false
177+
color: MoneroComponents.Style.dimmedFontColor
178+
}
179+
180+
RowLayout {
181+
id: actionRow
182+
visible: root.showActionButtons
183+
spacing: 16
184+
Layout.alignment: Qt.AlignHCenter
185+
Layout.topMargin: 4
186+
187+
MoneroComponents.StandardButton {
188+
id: cancelButton
189+
primary: !retryButton.visible
190+
small: false
191+
text: qsTr("Cancel") + translationManager.emptyString
192+
onClicked: root.fireCancel()
193+
KeyNavigation.tab: retryButton.visible ? retryButton : cancelButton
194+
KeyNavigation.backtab: retryButton.visible ? retryButton : cancelButton
195+
}
196+
197+
MoneroComponents.StandardButton {
198+
id: retryButton
199+
visible: root.retryCallback !== null
200+
primary: true
201+
small: false
202+
text: qsTr("Try again") + translationManager.emptyString
203+
onClicked: root.fireRetry()
204+
KeyNavigation.tab: cancelButton
205+
KeyNavigation.backtab: cancelButton
206+
}
207+
}
111208
}
112209
}

images/trezor7.png

95.3 KB
Loading

0 commit comments

Comments
 (0)