diff --git a/CMakeLists.txt b/CMakeLists.txt index 7507e80c57..185a74a9df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,12 +19,7 @@ option(WITH_UPDATER "Regularly check for new updates" ON) option(DEV_MODE "Checkout latest monero master on build" OFF) cmake_dependent_option(QML_TESTS "Build QML tests" ON "NOT STATIC;NOT ANDROID;NOT IOS" OFF) -if(DEV_MODE) - # DEV_MODE checks out the monero submodule to master, which requires C++17. - set(CMAKE_CXX_STANDARD 17) -else() - set(CMAKE_CXX_STANDARD 14) -endif() +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_SOURCE_DIR}/cmake") diff --git a/components/DevicePairingCodeDialog.qml b/components/DevicePairingCodeDialog.qml new file mode 100644 index 0000000000..0fd77f7c72 --- /dev/null +++ b/components/DevicePairingCodeDialog.qml @@ -0,0 +1,232 @@ +// Copyright (c) 2026, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import QtQuick 2.9 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.1 + +import "../components" as MoneroComponents + +// Modal that prompts the user for the 6-digit pairing code shown on a +// Trezor Safe 7 during the THP CodeEntry pairing flow. +FocusScope { + id: root + visible: false + + // onAcceptedCallback is invoked with the entered code (string of 6 + // ASCII digits). onRejectedCallback is invoked when the user cancels. + // Both are cleared once one of them has fired. + property var onAcceptedCallback + property var onRejectedCallback + + // Message shown above the input, set by the caller when a previous + // code was rejected by the device. + property string errorText + + function open() { + // Same modal behaviour as PasswordDialog: a wallet pool thread + // is blocked waiting for the code, so the UI behind the dialog + // must not accept input (e.g. starting a second wallet open + // would hang forever on the same device). + leftPanel.enabled = false; + middlePanel.enabled = false; + wizard.enabled = false; + titleBar.state = "essentials"; + root.visible = true; + codeInput.text = ""; + codeInput.forceActiveFocus(); + } + + function close() { + leftPanel.enabled = true; + middlePanel.enabled = true; + wizard.enabled = !wizard.deviceWalletCreationInProgress; + if (rootItem.state == "wizard") { + titleBar.state = "essentials"; + } else { + titleBar.state = "default"; + } + root.visible = false; + root.errorText = ""; + } + + function onOk() { + if (codeInput.text.length !== 6) { + return; + } + var entered = codeInput.text; + var callback = root.onAcceptedCallback; + root.onAcceptedCallback = null; + root.onRejectedCallback = null; + root.close(); + if (callback) { + callback(entered); + } + } + + function onCancel() { + if (!root.visible) { + return; + } + var callback = root.onRejectedCallback; + root.onAcceptedCallback = null; + root.onRejectedCallback = null; + root.close(); + if (callback) { + callback(); + } + } + + Keys.enabled: root.visible + Keys.onEscapePressed: root.onCancel() + + ColumnLayout { + id: mainLayout + spacing: 10 + anchors.fill: parent + anchors.margins: 35 + + ColumnLayout { + id: column + + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.maximumWidth: 480 + + Label { + Layout.fillWidth: true + text: qsTr("Trezor Safe 7 - pair this computer") + translationManager.emptyString + font.pixelSize: 18 + font.family: MoneroComponents.Style.fontBold.name + font.bold: true + color: MoneroComponents.Style.defaultFontColor + } + + Label { + Layout.fillWidth: true + Layout.topMargin: 12 + text: qsTr("Your Trezor is showing a 6-digit code on its screen. " + + "Type that exact code here to confirm this computer is allowed to talk to it. " + + "After this one-time pairing, the computer is remembered and you won't be asked again.") + translationManager.emptyString + font.pixelSize: 14 + font.family: MoneroComponents.Style.fontLight.name + color: MoneroComponents.Style.defaultFontColor + wrapMode: Text.WordWrap + } + + Label { + id: errorTextLabel + visible: text !== "" + text: root.errorText + Layout.fillWidth: true + Layout.topMargin: 12 + font.pixelSize: 14 + font.family: MoneroComponents.Style.fontLight.name + color: MoneroComponents.Style.errorColor + wrapMode: Text.WordWrap + } + + MoneroComponents.Input { + id: codeInput + focus: true + Layout.topMargin: 16 + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignHCenter + verticalAlignment: TextInput.AlignVCenter + font.family: MoneroComponents.Style.fontBold.name + font.pixelSize: 32 + font.letterSpacing: 6 + bottomPadding: 12 + leftPadding: 10 + topPadding: 12 + color: MoneroComponents.Style.defaultFontColor + selectionColor: MoneroComponents.Style.textSelectionColor + selectedTextColor: MoneroComponents.Style.textSelectedColor + inputMethodHints: Qt.ImhDigitsOnly + + // js replacement for `RegExpValidator { regExp: /[0-9]{0,6}/ }`, + // which rejects a paste outright: a code copied as + // "12 34 56" or "123-456" would insert nothing at all. + onTextChanged: { + var digits = codeInput.text.replace(/[^0-9]/g, "").substring(0, 6); + if (digits !== codeInput.text) { + codeInput.text = digits; + codeInput.cursorPosition = digits.length; + } + } + + background: Rectangle { + radius: 2 + border.color: MoneroComponents.Style.inputBorderColorActive + border.width: 1 + color: MoneroComponents.Style.blackTheme ? "black" : "#A9FFFFFF" + } + + Keys.enabled: root.visible + Keys.onEnterPressed: root.onOk() + Keys.onReturnPressed: root.onOk() + Keys.onEscapePressed: root.onCancel() + } + + Label { + Layout.fillWidth: true + Layout.topMargin: 8 + text: qsTr("Tip: nobody, not even a fake Trezor, can guess this code. " + + "If you mistype it, pairing starts over and the device shows a fresh code.") + translationManager.emptyString + font.pixelSize: 12 + font.italic: true + font.family: MoneroComponents.Style.fontLight.name + color: MoneroComponents.Style.dimmedFontColor + wrapMode: Text.WordWrap + } + + RowLayout { + spacing: 16 + Layout.topMargin: 16 + Layout.alignment: Qt.AlignRight + + MoneroComponents.StandardButton { + primary: false + small: true + width: 120 + fontSize: 14 + text: qsTr("Cancel") + translationManager.emptyString + onClicked: root.onCancel() + } + MoneroComponents.StandardButton { + small: true + width: 120 + fontSize: 14 + text: qsTr("Confirm") + translationManager.emptyString + enabled: codeInput.text.length === 6 + onClicked: root.onOk() + } + } + } + } +} diff --git a/components/ProcessingSplash.qml b/components/ProcessingSplash.qml index 469cc1fad8..ada8738411 100644 --- a/components/ProcessingSplash.qml +++ b/components/ProcessingSplash.qml @@ -43,23 +43,74 @@ Rectangle { border.width: 1 z: 11 property alias messageText: messageTitle.text + property alias subMessageText: messageSub.text + + // Optional Retry / Cancel row, shown instead of leaving the user + // stuck on a splash they can't dismiss. To use it, set + // retryCallback and cancelCallback, set showActionButtons, then + // call show(). Leaving retryCallback null hides Retry. + property bool showActionButtons: false + property var retryCallback: null + property var cancelCallback: null width: 100 height: 50 + focus: visible && showActionButtons + Keys.onPressed: { + if (!root.showActionButtons) return; + if (event.key === Qt.Key_Escape) { + root.fireCancel(); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (cancelButton.activeFocus || !retryButton.visible) { + root.fireCancel(); + } else { + root.fireRetry(); + } + event.accepted = true; + } + } + function show() { root.visible = true; + if (root.showActionButtons) { + if (retryButton.visible) { + retryButton.forceActiveFocus(); + } else { + cancelButton.forceActiveFocus(); + } + } } function close() { root.visible = false; + root.showActionButtons = false; + root.retryCallback = null; + root.cancelCallback = null; + } + + function fireRetry() { + var cb = root.retryCallback; + root.close(); + if (cb) cb(); + } + + function fireCancel() { + var cb = root.cancelCallback; + root.close(); + if (cb) cb(); } ColumnLayout { id: rootLayout - anchors.centerIn: parent - + // Anchor to the splash sides so a wrapping sub-message stays + // inside the box; anchors.centerIn leaves the layout width + // unconstrained and long messages spill outside. + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 30 anchors.rightMargin: 30 @@ -79,7 +130,9 @@ Rectangle { } BusyIndicator { - running: parent.visible + // Nothing is in progress while the action row waits for + // the user, so the spinner stops. + running: parent.visible && !root.showActionButtons anchors.centerIn: imgLogo style: BusyIndicatorStyle { indicator: Image { @@ -108,5 +161,49 @@ Rectangle { themeTransition: false color: MoneroComponents.Style.defaultFontColor } + + MoneroComponents.TextPlain { + id: messageSub + text: "" + visible: text.length > 0 + font.pixelSize: 15 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + themeTransition: false + color: MoneroComponents.Style.dimmedFontColor + } + + RowLayout { + id: actionRow + visible: root.showActionButtons + spacing: 16 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 4 + + MoneroComponents.StandardButton { + id: cancelButton + primary: !retryButton.visible + small: false + text: qsTr("Cancel") + translationManager.emptyString + onClicked: root.fireCancel() + KeyNavigation.tab: retryButton.visible ? retryButton : cancelButton + KeyNavigation.backtab: retryButton.visible ? retryButton : cancelButton + } + + MoneroComponents.StandardButton { + id: retryButton + visible: root.retryCallback !== null + primary: true + small: false + text: qsTr("Try again") + translationManager.emptyString + onClicked: root.fireRetry() + KeyNavigation.tab: cancelButton + KeyNavigation.backtab: cancelButton + } + } } } diff --git a/images/trezor7.png b/images/trezor7.png new file mode 100644 index 0000000000..e5cdbcff65 Binary files /dev/null and b/images/trezor7.png differ diff --git a/main.qml b/main.qml index 0345f5d29f..cd52cdc6dc 100644 --- a/main.qml +++ b/main.qml @@ -98,6 +98,7 @@ ApplicationWindow { property int estimatedBlockchainSize: persistentSettings.pruneBlockchain ? 110 : 270 // GB property alias viewState: rootItem.state property string prevSplashText; + property string prevSplashSubText; property bool splashDisplayedBeforeButtonRequest; property bool themeTransition: false property int backgroundSyncType: Wallet.BackgroundSync_Off; @@ -282,6 +283,7 @@ ApplicationWindow { if (isQuitting) return; isQuitting = true; + devicePairingCodeDialog.onCancel(); closeWallet(function() { gracefulShutdownComplete(); }) @@ -312,6 +314,7 @@ ApplicationWindow { currentWallet.deviceButtonRequest.disconnect(onDeviceButtonRequest); currentWallet.deviceButtonPressed.disconnect(onDeviceButtonPressed); currentWallet.walletPassphraseNeeded.disconnect(onWalletPassphraseNeededWallet); + currentWallet.walletPairingCodeNeeded.disconnect(onWalletPairingCodeNeededWallet); currentWallet.transactionCommitted.disconnect(onTransactionCommitted); middlePanel.paymentClicked.disconnect(handlePayment); middlePanel.sweepUnmixableClicked.disconnect(handleSweepUnmixable); @@ -363,6 +366,7 @@ ApplicationWindow { currentWallet.deviceButtonRequest.connect(onDeviceButtonRequest); currentWallet.deviceButtonPressed.connect(onDeviceButtonPressed); currentWallet.walletPassphraseNeeded.connect(onWalletPassphraseNeededWallet); + currentWallet.walletPairingCodeNeeded.connect(onWalletPairingCodeNeededWallet); currentWallet.transactionCommitted.connect(onTransactionCommitted); currentWallet.proxyAddress = Qt.binding(persistentSettings.getWalletProxyAddress); middlePanel.paymentClicked.connect(handlePayment); @@ -498,7 +502,10 @@ ApplicationWindow { } } else { prevSplashText = splash.messageText; + prevSplashSubText = splash.subMessageText; splashDisplayedBeforeButtonRequest = splash.visible; + // showProcessingSplash clears the subtitle, so onDeviceButtonPressed + // puts prevSplashSubText back once the device input is done. appWindow.showProcessingSplash(qsTr("Please proceed to the device...")); } } @@ -510,6 +517,7 @@ ApplicationWindow { } else { if (splashDisplayedBeforeButtonRequest){ appWindow.showProcessingSplash(prevSplashText); + splash.subMessageText = prevSplashSubText; } else { hideProcessingSplash(); } @@ -536,6 +544,78 @@ ApplicationWindow { appWindow.showStatusMessage(qsTr("Repairing incompatible wallet cache. Resyncing wallet."),6); return; default: + // Trezor errors get categorical UX: friendly retry + // hint, neutral cancel, a fresh pairing code, or a + // red protocol error. The category comes from the + // device exception the wallet API classified, not + // from the text of the error. + var es = wallet.errorString || ""; + var trezorError = wallet.trezorError; + if (trezorError === Wallet.TrezorError_Cancelled) { + // Deliberate user action, e.g. an on-device + // dismiss: neutral status hint, no red error. + console.log("Trezor open cancelled by user: " + es) + appWindow.showStatusMessage( + qsTr("Wallet open cancelled.") + translationManager.emptyString, + 5); + closeWallet(); + // Back to the wizard. This matters for the + // daemon-switch reopen flow, where rootItem.state + // is already "normal" and the user would be left + // in the wallet view with no wallet. + if (rootItem.state !== "wizard") { + rootItem.state = "wizard"; + } + return; + } + if (trezorError === Wallet.TrezorError_Unreachable) { + // The wallet password is still cached in + // appWindow.walletPassword from the first attempt, + // so Retry can re-fire openWalletAsync without + // asking for it again. + console.log("Trezor not reachable on wallet open: " + es) + closeWallet(); + appWindow.showRetryableTrezorErrorSplash( + qsTr("Couldn't reach your Trezor") + translationManager.emptyString, + qsTr("Make sure your Trezor is connected and unlocked, then try again.") + + translationManager.emptyString, + function() { + appWindow.initialize(); + }, + function() { + if (rootItem.state !== "wizard") { + rootItem.state = "wizard"; + } + }); + return; + } + if (trezorError === Wallet.TrezorError_PairingRejected) { + // The device rejected the pairing code the user + // typed. Reopening restarts pairing: the device + // shows a fresh code and the pairing dialog + // re-prompts, carrying the message below. + console.log("Trezor pairing code rejected: " + es) + devicePairingCodeDialog.errorText = + qsTr("That code did not match. Check the code on your device and try again.") + + translationManager.emptyString; + appWindow.initialize(); + return; + } + if (trezorError === Wallet.TrezorError_FirmwareUnsupported) { + // Nothing to retry: the device needs different + // firmware before it can open a Monero wallet. + console.error("Trezor firmware without Monero support: ", es) + closeWallet(); + informationPopup.title = qsTr("Trezor firmware without Monero support") + translationManager.emptyString; + informationPopup.text = qsTr("This Trezor runs firmware that has no Monero support, such as the bitcoin-only build. Install the standard firmware with Trezor Suite, then open the wallet again.") + translationManager.emptyString; + informationPopup.icon = StandardIcon.Critical; + informationPopup.onCloseCallback = null; + informationPopup.open(); + if (rootItem.state !== "wizard") { + rootItem.state = "wizard"; + } + return; + } // opening with password but password doesn't match console.error("Error opening wallet with password: ", wallet.errorString); passwordDialog.showError(qsTr("Couldn't open wallet: ") + wallet.errorString); @@ -595,6 +675,34 @@ ApplicationWindow { devicePassphraseDialog.open(on_device) } + function onWalletPairingCodeNeededManager(){ + // The wizard binds the same manager signal and prompts with its + // own splash wording while it creates a wallet from a device. + if (wizard.deviceWalletCreationInProgress) { + return; + } + onWalletPairingCodeNeeded(walletManager) + } + + function onWalletPairingCodeNeededWallet(){ + onWalletPairingCodeNeeded(currentWallet) + } + + function onWalletPairingCodeNeeded(handler){ + hideProcessingSplash(); + + console.log(">>> wallet pairing code needed: "); + devicePairingCodeDialog.onAcceptedCallback = function(code) { + handler.onPairingCodeEntered(code, false); + appWindow.onWalletOpening(); + } + devicePairingCodeDialog.onRejectedCallback = function() { + handler.onPairingCodeEntered("", true); + appWindow.onWalletOpening(); + } + devicePairingCodeDialog.open(); + } + function onWalletUpdate(stoppedBackgroundSync) { if (!currentWallet) return; @@ -1026,6 +1134,11 @@ ApplicationWindow { transaction.setFilename(path); } appWindow.showProcessingSplash(qsTr("Sending transaction ...")); + // Signing on a hardware wallet takes one tap per input, which can + // run to tens of seconds with no other sign of progress. + if (currentWallet && currentWallet.isHwBacked()) { + splash.subMessageText = qsTr("Confirm each transaction step on your hardware wallet. Each input requires a tap on the device.") + translationManager.emptyString; + } currentWallet.commitTransactionAsync(transaction); } @@ -1165,8 +1278,15 @@ ApplicationWindow { function showProcessingSplash(message) { console.log("Displaying processing splash") + // Drop any stale Retry/Cancel state so a regular splash never + // renders the action row of a previous error. + splash.showActionButtons = false; + splash.retryCallback = null; + splash.cancelCallback = null; if (typeof message != 'undefined') { splash.messageText = message + // Callers that want a subtitle assign it after this call. + splash.subMessageText = "" } leftPanel.enabled = false; @@ -1177,6 +1297,7 @@ ApplicationWindow { function hideProcessingSplash() { console.log("Hiding processing splash") + splash.subMessageText = ""; splash.close(); if (!passwordDialog.visible) { @@ -1186,6 +1307,32 @@ ApplicationWindow { } } + // Surface a retry-recoverable failure on the splash with a Retry / + // Cancel button row instead of bouncing the user back to the + // password dialog, so retrying after reconnecting the device does + // not require re-typing the wallet password. + // + // The splash is modal, so cancelling has to put the panels back. + // That happens here rather than in each caller's cancel callback: + // a caller that forgot would leave the app permanently disabled. + function showRetryableTrezorErrorSplash(title, subMessage, retryCallback, cancelCallback) { + splash.messageText = title; + splash.subMessageText = subMessage; + splash.retryCallback = retryCallback; + splash.cancelCallback = function() { + hideProcessingSplash(); + if (cancelCallback) { + cancelCallback(); + } + }; + splash.showActionButtons = true; + + leftPanel.enabled = false; + middlePanel.enabled = false; + titleBar.enabled = false; + splash.show(); + } + // close wallet and show wizard function showWizard(){ walletInitialized = false; @@ -1391,6 +1538,7 @@ ApplicationWindow { walletManager.deviceButtonPressed.connect(onDeviceButtonPressed); walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete); walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeededManager); + walletManager.walletPairingCodeNeeded.connect(onWalletPairingCodeNeededManager); IPC.uriHandler.connect(onUriHandler); if(typeof daemonManager != "undefined") { @@ -1803,6 +1951,13 @@ ApplicationWindow { anchors.fill: parent } + DevicePairingCodeDialog { + id: devicePairingCodeDialog + visible: false + z: parent.z + 1 + anchors.fill: parent + } + InputDialog { id: inputDialog visible: false @@ -1832,7 +1987,10 @@ ApplicationWindow { ProcessingSplash { id: splash width: appWindow.width / 2 - height: appWindow.height / 2.66 + // The Retry/Cancel row needs more room than a plain splash. + height: showActionButtons + ? appWindow.height / 1.9 + : appWindow.height / 2.66 x: (appWindow.width - width) / 2 y: (appWindow.height - height) / 2 messageText: qsTr("Please wait...") + translationManager.emptyString @@ -1947,7 +2105,7 @@ ApplicationWindow { source: blurredArea radius: 64 visible: passwordDialog.visible || inputDialog.visible || splash.visible || updateDialog.visible || - devicePassphraseDialog.visible || txConfirmationPopup.visible || successfulTxPopup.visible || + devicePassphraseDialog.visible || devicePairingCodeDialog.visible || txConfirmationPopup.visible || successfulTxPopup.visible || remoteNodeDialog.visible } @@ -2155,10 +2313,13 @@ ApplicationWindow { visible: false property alias text: statusMessageText.text anchors.bottom: parent.bottom - width: statusMessageText.contentWidth + 20 + // Cap the width so a long message wraps inside the toast instead + // of spilling off the right edge, and grow the height to match. + width: Math.min(statusMessageText.implicitWidth + 20, + Math.max(appWindow.width - 80, 320)) + height: statusMessageText.paintedHeight + 20 anchors.horizontalCenter: parent.horizontalCenter color: MoneroComponents.Style.blackTheme ? "black" : "white" - height: 40 MoneroComponents.TextPlain { id: statusMessageText anchors.fill: parent @@ -2166,6 +2327,8 @@ ApplicationWindow { font.pixelSize: 14 color: MoneroComponents.Style.defaultFontColor themeTransition: false + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter } } @@ -2218,6 +2381,10 @@ ApplicationWindow { } + // An unanswered pairing prompt holds a wallet pool thread, which + // would then never finish. Cancel it so the thread unwinds. + devicePairingCodeDialog.onCancel(); + // If daemon is running - prompt user before exiting if(daemonManager == undefined || persistentSettings.useRemoteNode) { closeAccepted(); diff --git a/qml.qrc b/qml.qrc index 9223423dc8..599ab41029 100644 --- a/qml.qrc +++ b/qml.qrc @@ -111,6 +111,7 @@ components/ProgressBar.qml components/StandardDialog.qml components/DevicePassphraseDialog.qml + components/DevicePairingCodeDialog.qml pages/Sign.qml components/DaemonManagerDialog.qml version.js @@ -286,6 +287,7 @@ images/ledgerFlex.png images/trezor3.png images/trezor5.png + images/trezor7.png images/trezorT.png images/trezorT@2x.png qtquickcontrols2.conf diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bcbda89cfe..9942b02782 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ file(GLOB SOURCE_FILES "libwalletqt/WalletListenerImpl.cpp" "libwalletqt/Wallet.cpp" "libwalletqt/PassphraseHelper.cpp" + "libwalletqt/PairingCodeHelper.cpp" "libwalletqt/PendingTransaction.cpp" "libwalletqt/TransactionHistory.cpp" "libwalletqt/TransactionInfo.cpp" @@ -25,6 +26,7 @@ file(GLOB SOURCE_FILES "libwalletqt/WalletManager.h" "libwalletqt/Wallet.h" "libwalletqt/PassphraseHelper.h" + "libwalletqt/PairingCodeHelper.h" "libwalletqt/PendingTransaction.h" "libwalletqt/TransactionHistory.h" "libwalletqt/TransactionInfo.h" diff --git a/src/libwalletqt/PairingCodeHelper.cpp b/src/libwalletqt/PairingCodeHelper.cpp new file mode 100644 index 0000000000..a6dfa3edad --- /dev/null +++ b/src/libwalletqt/PairingCodeHelper.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2026, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "PairingCodeHelper.h" +#include +#include + +Monero::optional PairingCodeHelper::onDevicePairingCodeRequest() +{ + qDebug() << __FUNCTION__; + QMutexLocker locker(&m_mutex); + m_abort = false; + m_answered = false; + m_code.clear(); + + if (m_prompter != nullptr) { + m_prompter->onWalletPairingCodeNeeded(); + } + + while (!m_answered) { + m_cond.wait(&m_mutex); + } + + if (m_abort) { + return Monero::optional(std::string{}); + } + auto result = m_code.toStdString(); + m_code.clear(); + return Monero::optional(result); +} + +void PairingCodeHelper::onPairingCodeEntered(const QString &code, bool entry_abort) +{ + qDebug() << __FUNCTION__; + QMutexLocker locker(&m_mutex); + m_code = code; + m_abort = entry_abort; + m_answered = true; + m_cond.wakeAll(); +} diff --git a/src/libwalletqt/PairingCodeHelper.h b/src/libwalletqt/PairingCodeHelper.h new file mode 100644 index 0000000000..d9aca2f70d --- /dev/null +++ b/src/libwalletqt/PairingCodeHelper.h @@ -0,0 +1,79 @@ +// Copyright (c) 2026, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef MONERO_GUI_PAIRINGCODEHELPER_H +#define MONERO_GUI_PAIRINGCODEHELPER_H + +#include +#include +#include +#include +#include + +/** + * THP CodeEntry pairing code helper. The wallet's worker thread blocks + * inside onDevicePairingCodeRequest waiting for the user to type the + * 6-digit code shown on the Trezor. The QML side displays a modal, + * collects the code, and calls onPairingCodeEntered to wake the worker. + * + * Mirrors the existing PassphraseHelper pattern. + */ +class PairingCodePrompter { +public: + virtual void onWalletPairingCodeNeeded() = 0; + virtual ~PairingCodePrompter() = default; +}; + +class PairingCodeReceiver { +public: + virtual void onPairingCodeEntered(const QString &code, bool entry_abort) = 0; + virtual ~PairingCodeReceiver() = default; +}; + +class PairingCodeHelper { +public: + PairingCodeHelper(PairingCodePrompter *prompter = nullptr): m_prompter(prompter) {} + PairingCodeHelper(const PairingCodeHelper &h): PairingCodeHelper(h.m_prompter) {} + + // Blocks until onPairingCodeEntered is called. Returns the code as + // a UTF-8 string (ASCII digits in practice). Returns an empty + // string on user cancel. + Monero::optional onDevicePairingCodeRequest(); + + void onPairingCodeEntered(const QString &code, bool entry_abort); + +private: + PairingCodePrompter *m_prompter; + QWaitCondition m_cond; + QMutex m_mutex; + QString m_code; + bool m_abort = false; + bool m_answered = false; +}; + +#endif // MONERO_GUI_PAIRINGCODEHELPER_H diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 64f968707b..9fbab619c3 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -92,6 +92,11 @@ Wallet::Status Wallet::status() const return static_cast(m_walletImpl->status()); } +Wallet::TrezorError Wallet::trezorError() const +{ + return static_cast(m_walletImpl->trezorError()); +} + NetworkType::Type Wallet::nettype() const { return static_cast(m_walletImpl->nettype()); @@ -1173,6 +1178,19 @@ void Wallet::onPassphraseEntered(const QString &passphrase, bool enter_on_device } } +void Wallet::onWalletPairingCodeNeeded() +{ + emit this->walletPairingCodeNeeded(); +} + +void Wallet::onPairingCodeEntered(const QString &code, bool entry_abort) +{ + if (m_walletListener != nullptr) + { + m_walletListener->onPairingCodeEntered(code, entry_abort); + } +} + Wallet::Wallet(Monero::Wallet *w, QObject *parent) : QObject(parent) , m_walletImpl(w) diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 1cac75a505..14b1ee9d97 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -43,6 +43,7 @@ #include "PendingTransaction.h" // we need to have an access to the PendingTransaction::Priority enum here; #include "UnsignedTransaction.h" #include "NetworkType.h" +#include "PairingCodeHelper.h" #include "PassphraseHelper.h" #include "WalletListenerImpl.h" @@ -61,7 +62,7 @@ class SubaddressModel; class SubaddressAccount; class SubaddressAccountModel; -class Wallet : public QObject, public PassprasePrompter +class Wallet : public QObject, public PassprasePrompter, public PairingCodePrompter { Q_OBJECT Q_PROPERTY(bool disconnected READ disconnected NOTIFY disconnectedChanged) @@ -69,6 +70,7 @@ class Wallet : public QObject, public PassprasePrompter Q_PROPERTY(QString seed READ getSeed) Q_PROPERTY(QString seedLanguage READ getSeedLanguage) Q_PROPERTY(Status status READ status) + Q_PROPERTY(TrezorError trezorError READ trezorError) Q_PROPERTY(NetworkType::Type nettype READ nettype) // Q_PROPERTY(ConnectionStatus connected READ connected) Q_PROPERTY(quint32 currentSubaddressAccount READ currentSubaddressAccount NOTIFY currentSubaddressAccountChanged) @@ -120,6 +122,21 @@ class Wallet : public QObject, public PassprasePrompter Q_ENUM(BackgroundSyncType) + //! Category of the Trezor failure behind the last open or create, so + //! QML can offer the recovery that fits without matching the English + //! text of an error. + enum TrezorError { + TrezorError_None = Monero::Wallet::TrezorError_None, + TrezorError_Unreachable = Monero::Wallet::TrezorError_Unreachable, + TrezorError_Cancelled = Monero::Wallet::TrezorError_Cancelled, + TrezorError_Protocol = Monero::Wallet::TrezorError_Protocol, + TrezorError_Other = Monero::Wallet::TrezorError_Other, + TrezorError_FirmwareUnsupported = Monero::Wallet::TrezorError_FirmwareUnsupported, + TrezorError_PairingRejected = Monero::Wallet::TrezorError_PairingRejected + }; + + Q_ENUM(TrezorError) + //! returns mnemonic seed QString getSeed() const; @@ -132,6 +149,9 @@ class Wallet : public QObject, public PassprasePrompter //! returns last operation's status Status status() const; + //! returns the category of the Trezor failure behind the last status + TrezorError trezorError() const; + //! returns network type of the wallet. NetworkType::Type nettype() const; @@ -374,6 +394,10 @@ class Wallet : public QObject, public PassprasePrompter Q_INVOKABLE void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort=false); virtual void onWalletPassphraseNeeded(bool on_device) override; + // THP CodeEntry pairing code entry for Trezor Safe 7 + Q_INVOKABLE void onPairingCodeEntered(const QString &code, bool entry_abort=false); + virtual void onWalletPairingCodeNeeded() override; + // TODO: setListenter() when it implemented in API signals: // emitted on every event happened with wallet @@ -397,6 +421,7 @@ class Wallet : public QObject, public PassprasePrompter void deviceButtonRequest(quint64 buttonCode); void deviceButtonPressed(); void walletPassphraseNeeded(bool onDevice); + void walletPairingCodeNeeded(); void transactionCommitted(bool status, PendingTransaction *t, const QStringList& txid); void heightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) const; void deviceShowAddressShowed(); diff --git a/src/libwalletqt/WalletListenerImpl.cpp b/src/libwalletqt/WalletListenerImpl.cpp index d2ccc24442..db1dd96162 100644 --- a/src/libwalletqt/WalletListenerImpl.cpp +++ b/src/libwalletqt/WalletListenerImpl.cpp @@ -32,6 +32,7 @@ WalletListenerImpl::WalletListenerImpl(Wallet * w) : m_wallet(w) , m_phelper(w) + , m_pchelper(w) { } @@ -95,3 +96,15 @@ Monero::optional WalletListenerImpl::onDevicePassphraseRequest(bool qDebug() << __FUNCTION__; return m_phelper.onDevicePassphraseRequest(on_device); } + +Monero::optional WalletListenerImpl::onDevicePairingCodeRequest() +{ + qDebug() << __FUNCTION__; + return m_pchelper.onDevicePairingCodeRequest(); +} + +void WalletListenerImpl::onPairingCodeEntered(const QString &code, bool entry_abort) +{ + qDebug() << __FUNCTION__; + m_pchelper.onPairingCodeEntered(code, entry_abort); +} diff --git a/src/libwalletqt/WalletListenerImpl.h b/src/libwalletqt/WalletListenerImpl.h index 46f38fdf32..b6720b787e 100644 --- a/src/libwalletqt/WalletListenerImpl.h +++ b/src/libwalletqt/WalletListenerImpl.h @@ -30,11 +30,12 @@ #define MONERO_GUI_WALLETLISTENERIMPL_H #include "wallet/api/wallet2_api.h" +#include "PairingCodeHelper.h" #include "PassphraseHelper.h" class Wallet; -class WalletListenerImpl : public Monero::WalletListener, public PassphraseReceiver +class WalletListenerImpl : public Monero::WalletListener, public PassphraseReceiver, public PairingCodeReceiver { public: WalletListenerImpl(Wallet * w); @@ -60,9 +61,14 @@ class WalletListenerImpl : public Monero::WalletListener, public PassphraseRecei virtual Monero::optional onDevicePassphraseRequest(bool & on_device) override; + virtual Monero::optional onDevicePairingCodeRequest() override; + + virtual void onPairingCodeEntered(const QString &code, bool entry_abort) override; + private: Wallet * m_wallet; PassphraseHelper m_phelper; + PairingCodeHelper m_pchelper; }; #endif //MONERO_GUI_WALLETLISTENERIMPL_H diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index 73046afa1f..8699f2bc30 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -46,10 +46,10 @@ #include "qt/updater.h" #include "qt/ScopeGuard.h" -class WalletPassphraseListenerImpl : public Monero::WalletListener, public PassphraseReceiver +class WalletPassphraseListenerImpl : public Monero::WalletListener, public PassphraseReceiver, public PairingCodeReceiver { public: - WalletPassphraseListenerImpl(WalletManager * mgr): m_mgr(mgr), m_phelper(mgr) {} + WalletPassphraseListenerImpl(WalletManager * mgr): m_mgr(mgr), m_phelper(mgr), m_pchelper(mgr) {} virtual void moneySpent(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; }; virtual void moneyReceived(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; }; @@ -70,6 +70,18 @@ class WalletPassphraseListenerImpl : public Monero::WalletListener, public Pass return m_phelper.onDevicePassphraseRequest(on_device); } + virtual void onPairingCodeEntered(const QString &code, bool entry_abort) override + { + qDebug() << __FUNCTION__; + m_pchelper.onPairingCodeEntered(code, entry_abort); + } + + virtual Monero::optional onDevicePairingCodeRequest() override + { + qDebug() << __FUNCTION__; + return m_pchelper.onDevicePairingCodeRequest(); + } + virtual void onDeviceButtonRequest(uint64_t code) override { qDebug() << __FUNCTION__; @@ -85,6 +97,7 @@ class WalletPassphraseListenerImpl : public Monero::WalletListener, public Pass private: WalletManager * m_mgr; PassphraseHelper m_phelper; + PairingCodeHelper m_pchelper; }; Wallet *WalletManager::createWallet(const QString &path, const QString &password, @@ -108,9 +121,14 @@ Wallet *WalletManager::openWallet(const QString &path, const QString &password, m_mutex_passphraseReceiver.lock(); m_passphraseReceiver = &tmpListener; m_mutex_passphraseReceiver.unlock(); + m_mutex_pairingCodeReceiver.lock(); + m_pairingCodeReceiver = &tmpListener; + m_mutex_pairingCodeReceiver.unlock(); const auto cleanup = sg::make_scope_guard([this]() noexcept { QMutexLocker passphrase_locker(&m_mutex_passphraseReceiver); this->m_passphraseReceiver = nullptr; + QMutexLocker pairing_locker(&m_mutex_pairingCodeReceiver); + this->m_pairingCodeReceiver = nullptr; }); if (m_currentWallet) { @@ -178,9 +196,14 @@ Wallet *WalletManager::createWalletFromDevice(const QString &path, const QString m_mutex_passphraseReceiver.lock(); m_passphraseReceiver = &tmpListener; m_mutex_passphraseReceiver.unlock(); + m_mutex_pairingCodeReceiver.lock(); + m_pairingCodeReceiver = &tmpListener; + m_mutex_pairingCodeReceiver.unlock(); const auto cleanup = sg::make_scope_guard([this]() noexcept { QMutexLocker passphrase_locker(&m_mutex_passphraseReceiver); this->m_passphraseReceiver = nullptr; + QMutexLocker pairing_locker(&m_mutex_pairingCodeReceiver); + this->m_pairingCodeReceiver = nullptr; }); if (m_currentWallet) { @@ -589,6 +612,20 @@ void WalletManager::onPassphraseEntered(const QString &passphrase, bool enter_on } } +void WalletManager::onWalletPairingCodeNeeded() +{ + emit this->walletPairingCodeNeeded(); +} + +void WalletManager::onPairingCodeEntered(const QString &code, bool entry_abort) +{ + QMutexLocker locker(&m_mutex_pairingCodeReceiver); + if (m_pairingCodeReceiver != nullptr) + { + m_pairingCodeReceiver->onPairingCodeEntered(code, entry_abort); + } +} + QString WalletManager::proxyAddress() const { QMutexLocker locker(&m_proxyMutex); diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index 649a9f9289..3b4362f592 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -38,6 +38,7 @@ #include #include "qt/FutureScheduler.h" #include "NetworkType.h" +#include "PairingCodeHelper.h" #include "PassphraseHelper.h" class Wallet; @@ -45,7 +46,7 @@ namespace Monero { struct WalletManager; } -class WalletManager : public QObject, public PassprasePrompter +class WalletManager : public QObject, public PassprasePrompter, public PairingCodePrompter { Q_OBJECT Q_PROPERTY(bool connected READ connected) @@ -195,6 +196,9 @@ class WalletManager : public QObject, public PassprasePrompter Q_INVOKABLE void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort=false); virtual void onWalletPassphraseNeeded(bool on_device) override; + Q_INVOKABLE void onPairingCodeEntered(const QString &code, bool entry_abort=false); + virtual void onWalletPairingCodeNeeded() override; + QString proxyAddress() const; void setProxyAddress(QString address); @@ -203,6 +207,7 @@ class WalletManager : public QObject, public PassprasePrompter void walletOpened(Wallet * wallet); void walletCreated(Wallet * wallet); void walletPassphraseNeeded(bool onDevice); + void walletPairingCodeNeeded(); void deviceButtonRequest(quint64 buttonCode); void deviceButtonPressed(); void checkUpdatesComplete( @@ -226,6 +231,8 @@ public slots: QPointer m_currentWallet; PassphraseReceiver * m_passphraseReceiver; QMutex m_mutex_passphraseReceiver; + PairingCodeReceiver * m_pairingCodeReceiver = nullptr; + QMutex m_mutex_pairingCodeReceiver; QString m_proxyAddress; mutable QMutex m_proxyMutex; FutureScheduler m_scheduler; diff --git a/wizard/WizardController.qml b/wizard/WizardController.qml index 6e722d893c..d3af259927 100644 --- a/wizard/WizardController.qml +++ b/wizard/WizardController.qml @@ -35,6 +35,7 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.2 import QtQuick.Dialogs 1.2 import moneroComponents.Wallet 1.0 +import moneroComponents.WalletManager 1.0 import "../js/Wizard.js" as Wizard import "../js/Windows.js" as Windows @@ -48,7 +49,7 @@ Rectangle { anchors.fill: parent signal useMoneroClicked() - signal walletCreatedFromDevice(bool success) + signal walletCreatedFromDevice(bool success, bool cancelled) function restart(generatingNewSeed) { // Clear up any state, including `m_wallet`, which @@ -425,6 +426,7 @@ Rectangle { function disconnect(){ walletManager.walletCreated.disconnect(onWalletCreated); walletManager.walletPassphraseNeeded.disconnect(onWalletPassphraseNeeded); + walletManager.walletPairingCodeNeeded.disconnect(onWalletPairingCodeNeeded); walletManager.deviceButtonRequest.disconnect(onDeviceButtonRequest); walletManager.deviceButtonPressed.disconnect(onDeviceButtonPressed); } @@ -432,6 +434,7 @@ Rectangle { function connect(){ walletManager.walletCreated.connect(onWalletCreated); walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded); + walletManager.walletPairingCodeNeeded.connect(onWalletPairingCodeNeeded); walletManager.deviceButtonRequest.connect(onDeviceButtonRequest); walletManager.deviceButtonPressed.connect(onDeviceButtonPressed); } @@ -456,6 +459,9 @@ Rectangle { var kdfRounds = persistentSettings.kdfRounds; var restoreHeight = wizardController.walletOptionsRestoreHeight; var subaddressLookahead = wizardController.walletOptionsSubaddressLookahead; + // Device descriptor. The segment before the first ':' selects the + // device class, always "Trezor" here; an empty transport path + // matches every USB transport and the first one wins. var deviceName = wizardController.walletOptionsDeviceName; connect(); @@ -470,6 +476,7 @@ Rectangle { wizardController.enabled = true; splash.close() + var cancelled = false; var success = wallet.status === Wallet.Status_Ok; if (success) { wizardController.m_wallet = wallet; @@ -479,13 +486,59 @@ Rectangle { wizardController.walletOptionsRestoreHeight = wizardController.m_wallet.walletCreationHeight; } } else { + // The wallet-create path hits the same Trezor error + // categories as wallet-open. The raw errorString is worth + // logging but is wallet2-internal phrasing that does not + // tell the user what to do. console.log(wallet.errorString) - appWindow.showStatusMessage(qsTr(wallet.errorString), 5); + var es = wallet.errorString || ""; + var trezorError = wallet.trezorError; + if (trezorError === Wallet.TrezorError_Unreachable) { + // Retry re-fires createWalletFromDevice with the same + // wizard options; they all persist on wizardController. + walletManager.closeWallet(); + // Unwire the handlers first, otherwise the retry path's + // connect() double-binds onWalletCreated. + disconnect(); + appWindow.showRetryableTrezorErrorSplash( + qsTr("Couldn't reach your Trezor") + + translationManager.emptyString, + qsTr("Make sure your Trezor is connected and unlocked, then try again.") + + translationManager.emptyString, + function() { wizardController.createWalletFromDevice(); }, + function() { walletCreatedFromDevice(false, true); }); + return; + } else if (trezorError === Wallet.TrezorError_PairingRejected) { + // The device rejected the pairing code the user just + // typed. Starting over makes it show a fresh code, and + // the pairing dialog re-prompts carrying the message. + walletManager.closeWallet(); + disconnect(); + devicePairingCodeDialog.errorText = + qsTr("That code did not match. Check the code on your device and try again.") + + translationManager.emptyString; + wizardController.createWalletFromDevice(); + return; + } else if (trezorError === Wallet.TrezorError_Cancelled) { + appWindow.showStatusMessage( + qsTr("Wallet creation cancelled.") + translationManager.emptyString, + 5); + cancelled = true; + } else if (trezorError === Wallet.TrezorError_FirmwareUnsupported) { + // Nothing to retry: the device needs different firmware + // before it can hold a Monero wallet. + appWindow.showStatusMessage( + qsTr("This Trezor runs firmware that has no Monero support, such as the bitcoin-only build. Install the standard firmware with Trezor Suite, then try again.") + + translationManager.emptyString, + 10); + } else { + appWindow.showStatusMessage(qsTr(wallet.errorString), 5); + } walletManager.closeWallet(); } disconnect(); - walletCreatedFromDevice(success); + walletCreatedFromDevice(success, cancelled); } function onWalletPassphraseNeeded(on_device){ @@ -508,6 +561,22 @@ Rectangle { devicePassphraseDialog.open(on_device) } + function onWalletPairingCodeNeeded(){ + splash.close() + + console.log(">>> wallet pairing code needed: "); + devicePairingCodeDialog.onAcceptedCallback = function(code) { + walletManager.onPairingCodeEntered(code, false); + creatingWalletDeviceSplash(); + } + devicePairingCodeDialog.onRejectedCallback = function() { + walletManager.onPairingCodeEntered("", true); + creatingWalletDeviceSplash(); + } + + devicePairingCodeDialog.open() + } + function onDeviceButtonRequest(code){ deviceAttentionSplash(); } diff --git a/wizard/WizardCreateDevice1.qml b/wizard/WizardCreateDevice1.qml index 268091c3e2..3a70382f80 100644 --- a/wizard/WizardCreateDevice1.qml +++ b/wizard/WizardCreateDevice1.qml @@ -61,6 +61,7 @@ Rectangle { ListElement { column1: "Trezor Model T"; column2: "Trezor";} ListElement { column1: "Trezor Safe 3"; column2: "Trezor";} ListElement { column1: "Trezor Safe 5"; column2: "Trezor";} + ListElement { column1: "Trezor Safe 7"; column2: "Trezor";} } ColumnLayout { @@ -174,6 +175,8 @@ Rectangle { return "qrc:///images/trezor3.png"; } else if (trezorType == "Trezor Safe 5") { return "qrc:///images/trezor5.png"; + } else if (trezorType == "Trezor Safe 7") { + return "qrc:///images/trezor7.png"; } } else if (hardwareWalletType == "Ledger") { if (ledgerType == "Ledger Nano S") { @@ -297,11 +300,11 @@ Rectangle { } } - function onCreateWalletFromDeviceCompleted(written){ + function onCreateWalletFromDeviceCompleted(written, cancelled){ hideProcessingSplash(); if(written){ wizardStateView.state = "wizardCreateWallet3"; - } else { + } else if(!cancelled){ errorMsg.text = qsTr("Error writing wallet from hardware device. Check application logs.") + translationManager.emptyString; } wizardController.walletCreatedFromDevice.disconnect(onCreateWalletFromDeviceCompleted);