diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index c84390be8..5c847b774 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -34,7 +34,9 @@ add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}") add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}") add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}") -include(../.cache/agw_rsa_public_keys.cmake) +if(AMNEZIA_QR_PAIRING_ALLOW) + include(../.cache/agw_rsa_public_keys.cmake) +endif() if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) set(PACKAGES ${PACKAGES} Widgets) @@ -204,8 +206,8 @@ list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp) target_link_libraries(${PROJECT} PRIVATE ${LIBS}) target_compile_definitions(${PROJECT} PRIVATE "MZ_$") -if(AMNEZIA_LOCAL_GATEWAY) - target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_LOCAL_GATEWAY) +if(AMNEZIA_QR_PAIRING_ALLOW) + target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW) endif() if(AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY) diff --git a/client/amneziaApplication.cpp b/client/amneziaApplication.cpp index 008cc345d..0b95ca7f5 100644 --- a/client/amneziaApplication.cpp +++ b/client/amneziaApplication.cpp @@ -251,6 +251,10 @@ bool AmneziaApplication::parseCommands() #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) void AmneziaApplication::startLocalServer() { +#ifdef AMNEZIA_QR_PAIRING_ALLOW + return; +#endif + const QString serverName("AmneziaVPNInstance"); QLocalServer::removeServer(serverName); diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index bf0384082..f6a2c829d 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -86,7 +86,7 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const } #endif -#ifdef AMNEZIA_LOCAL_GATEWAY +#ifdef AMNEZIA_QR_PAIRING_ALLOW { const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl); const QString host = gatewayUrl.host().toLower(); diff --git a/client/core/repositories/secureAppSettingsRepository.cpp b/client/core/repositories/secureAppSettingsRepository.cpp index 33cba3e53..3d6a9e0ac 100644 --- a/client/core/repositories/secureAppSettingsRepository.cpp +++ b/client/core/repositories/secureAppSettingsRepository.cpp @@ -16,7 +16,7 @@ using namespace amnezia; namespace { -#ifdef AMNEZIA_LOCAL_GATEWAY +#ifdef AMNEZIA_QR_PAIRING_ALLOW // Prefer 127.0.0.1 with local mock (tools/local_gateway listens on 0.0.0.0:8080); avoids LAN/IPv6 ambiguity in dev. constexpr char gatewayEndpoint[] = "http://127.0.0.1:8080/"; #else diff --git a/client/ui/controllers/api/pairingUiController.cpp b/client/ui/controllers/api/pairingUiController.cpp index 98ae0bb58..d057e057e 100644 --- a/client/ui/controllers/api/pairingUiController.cpp +++ b/client/ui/controllers/api/pairingUiController.cpp @@ -143,6 +143,15 @@ QString PairingUiController::tvStatusMessage() const return m_tvStatusMessage; } +int PairingUiController::tvPairingWaitWindowSeconds() const +{ + if (!m_pairingController) { + return 30; + } + const int msec = m_pairingController->pairingLongPollTimeoutMsecs(); + return qMax(1, (msec + 999) / 1000); +} + bool PairingUiController::phonePairingBusy() const { return m_phonePairingBusy; diff --git a/client/ui/controllers/api/pairingUiController.h b/client/ui/controllers/api/pairingUiController.h index aabbcde00..cb71bdf85 100644 --- a/client/ui/controllers/api/pairingUiController.h +++ b/client/ui/controllers/api/pairingUiController.h @@ -24,6 +24,8 @@ class PairingUiController : public QObject Q_PROPERTY(QString tvSessionUuid READ tvSessionUuid NOTIFY tvSessionUuidChanged) Q_PROPERTY(bool tvPairingBusy READ tvPairingBusy NOTIFY tvPairingBusyChanged) Q_PROPERTY(QString tvStatusMessage READ tvStatusMessage NOTIFY tvStatusMessageChanged) + /** Long-poll window for generate_qr (seconds), for receive UI countdown. */ + Q_PROPERTY(int tvPairingWaitWindowSeconds READ tvPairingWaitWindowSeconds NOTIFY tvQrCodesChanged) Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged) Q_PROPERTY(QString phoneStatusMessage READ phoneStatusMessage NOTIFY phoneStatusMessageChanged) @@ -41,6 +43,7 @@ public: QString tvSessionUuid() const; bool tvPairingBusy() const; QString tvStatusMessage() const; + int tvPairingWaitWindowSeconds() const; bool phonePairingBusy() const; QString phoneStatusMessage() const; diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 837259c66..642380390 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -80,7 +80,9 @@ namespace PageLoader PageSetupWizardApiPremiumInfo, PageSetupWizardApiTrialEmail, - PageSettingsApiQrPairing, + PageSettingsApiQrPairingDev, + PageSettingsApiQrPairingSend, + PageSetupWizardApiQrPairingReceive, PageDevMenu }; diff --git a/client/ui/qml/Pages2/PageDevMenu.qml b/client/ui/qml/Pages2/PageDevMenu.qml index b6b029939..cd5730047 100644 --- a/client/ui/qml/Pages2/PageDevMenu.qml +++ b/client/ui/qml/Pages2/PageDevMenu.qml @@ -90,12 +90,27 @@ PageType { footer: ColumnLayout { width: listView.width - SwitcherType { + LabelWithButtonType { Layout.fillWidth: true Layout.topMargin: 24 Layout.rightMargin: 16 Layout.leftMargin: 16 + text: qsTr("QR pairing (full dev UI)") + descriptionText: qsTr("Receive + send on one device for local gateway / QA") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsApiQrPairingDev) + } + } + + SwitcherType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + text: qsTr("Dev gateway environment") checked: SettingsController.isDevGatewayEnv onToggled: function() { diff --git a/client/ui/qml/Pages2/PageSettingsApiQrPairing.qml b/client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml similarity index 91% rename from client/ui/qml/Pages2/PageSettingsApiQrPairing.qml rename to client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml index 331f29396..a8af8b4bb 100644 --- a/client/ui/qml/Pages2/PageSettingsApiQrPairing.qml +++ b/client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import QRCodeReader 1.0 -import PageEnum 1.0 import Style 1.0 import "../Controls2" @@ -46,13 +45,22 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 - text: qsTr("QR pairing") + text: qsTr("QR pairing (dev — single device)") font.pixelSize: 28 font.bold: true color: AmneziaStyle.color.paleGray wrapMode: Text.Wrap } + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + color: AmneziaStyle.color.goldenApricot + text: qsTr("Developer / QA: receive and send on one device (e.g. with local gateway). Not shown in production menus unless opened from Dev menu.") + wrapMode: Text.Wrap + } + ParagraphTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -88,8 +96,6 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("Cancel receive") - // Do not use defaultColor: transparent here: when enabled, BasicButtonType paints that - // as the idle background, so midnightBlack label sits on the page — invisible until hover. enabled: PairingUiController.tvPairingBusy clickedFunc: function() { PairingUiController.cancelTvQrSession() @@ -105,7 +111,6 @@ PageType { wrapMode: Text.Wrap } - // SVG QR from qrCodeUtils has a tiny viewBox (~45px); without a sized container + sourceSize it stays small. Item { id: qrBox Layout.fillWidth: true @@ -182,7 +187,6 @@ PageType { visible: Layout.preferredHeight > 0 clip: true - // QRCodeReader is a QObject (not Item): no anchors; preview rect via setCameraSize like PageSetupWizardQrReader. QRCodeReader { id: pairingQrReader @@ -246,11 +250,22 @@ PageType { } function onTvPairingConfigReceived() { + root.pairingCameraOpen = false + pairingQrReader.stopReading() + qrImage.source = "" PageController.showNotificationMessage(qsTr("Configuration received from gateway")) + Qt.callLater(function() { + PageController.closePage() + }) } function onPhonePairingSucceeded() { + root.pairingCameraOpen = false + pairingQrReader.stopReading() PageController.showNotificationMessage(qsTr("Configuration sent")) + Qt.callLater(function() { + PageController.closePage() + }) } function onPairingUuidFromScan(uuid) { diff --git a/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml b/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml new file mode 100644 index 000000000..18cdb0ca4 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QRCodeReader 1.0 +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property bool pairingCameraOpen: false + + Connections { + target: root + function onVisibleChanged() { + if (!root.visible) { + pairingQrReader.stopReading() + root.pairingCameraOpen = false + PairingUiController.cancelAllPairingActivity() + } + } + } + + FlickableType { + anchors.fill: parent + contentHeight: layout.implicitHeight + + ColumnLayout { + id: layout + width: root.width + spacing: 8 + + BackButtonType { + Layout.topMargin: 20 + PageController.safeAreaTopMargin + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + text: qsTr("Transfer subscription (QR)") + font.pixelSize: 28 + font.bold: true + color: AmneziaStyle.color.paleGray + wrapMode: Text.Wrap + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("Scan the session QR shown on the receiving device, then send this server’s Amnezia Premium configuration through the gateway.") + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + text: qsTr("Send from this subscription") + font.pixelSize: 18 + font.bold: true + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + id: uuidField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + headerText: qsTr("QR session UUID") + textField.placeholderText: qsTr("Paste UUID from the other device’s QR") + } + + BasicButtonType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + visible: Qt.platform.os === "android" || Qt.platform.os === "ios" + text: { + if (Qt.platform.os === "ios" && root.pairingCameraOpen) { + return qsTr("Hide camera") + } + return qsTr("Scan QR code") + } + enabled: !PairingUiController.phonePairingBusy + clickedFunc: function() { + if (Qt.platform.os === "android") { + PairingUiController.openPairingQrScanner() + } else { + root.pairingCameraOpen = !root.pairingCameraOpen + } + } + } + + Item { + id: cameraSlot + Layout.fillWidth: true + Layout.preferredHeight: (root.pairingCameraOpen && Qt.platform.os === "ios") ? 220 : 0 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + visible: Layout.preferredHeight > 0 + clip: true + + QRCodeReader { + id: pairingQrReader + + onCodeReaded: function(code) { + if (PairingUiController.applyScannedTextAsPairingUuid(code)) { + pairingQrReader.stopReading() + root.pairingCameraOpen = false + PageController.showNotificationMessage(qsTr("Session ID filled from QR")) + } + } + } + + onVisibleChanged: { + if (!visible) { + pairingQrReader.stopReading() + return + } + if (Qt.platform.os === "ios") { + Qt.callLater(function() { + var p = cameraSlot.mapToItem(root, 0, 0) + pairingQrReader.setCameraSize(Qt.rect(p.x, p.y, cameraSlot.width, cameraSlot.height)) + pairingQrReader.startReading() + }) + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: PairingUiController.phonePairingBusy ? qsTr("Sending…") : qsTr("Send from current subscription") + enabled: !PairingUiController.phonePairingBusy + clickedFunc: function() { + PairingUiController.submitPhonePairing(uuidField.textField.text, ServersUiController.getProcessedServerIndex()) + } + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 24 + PageController.safeAreaBottomMargin + visible: PairingUiController.phoneStatusMessage.length > 0 + text: PairingUiController.phoneStatusMessage + wrapMode: Text.Wrap + } + } + } + + Connections { + target: PairingUiController + + function onPhonePairingSucceeded() { + root.pairingCameraOpen = false + pairingQrReader.stopReading() + PageController.showNotificationMessage(qsTr("Configuration sent")) + Qt.callLater(function() { + PageController.closePage() + }) + } + + function onPairingUuidFromScan(uuid) { + uuidField.textField.text = uuid + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 0a8154d69..b373654c8 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -378,12 +378,12 @@ PageType { LabelWithButtonType { Layout.fillWidth: true - text: qsTr("QR pairing (beta)") - descriptionText: qsTr("Transfer config via gateway using a QR code") + text: qsTr("Transfer by QR (send)") + descriptionText: qsTr("Scan the session QR from the receiving device and send this subscription via the gateway") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - PageController.goToPage(PageEnum.PageSettingsApiQrPairing) + PageController.goToPage(PageEnum.PageSettingsApiQrPairingSend) } } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiQrPairingReceive.qml b/client/ui/qml/Pages2/PageSetupWizardApiQrPairingReceive.qml new file mode 100644 index 000000000..ac33ff54c --- /dev/null +++ b/client/ui/qml/Pages2/PageSetupWizardApiQrPairingReceive.qml @@ -0,0 +1,264 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property int qrImageIndex: 0 + property int pairingSecondsLeft: 0 + + function formatMmSs(totalSec) { + if (totalSec <= 0) { + return "0:00" + } + const m = Math.floor(totalSec / 60) + const s = totalSec % 60 + return m + (s < 10 ? ":0" : ":") + s + } + + function scrollPairingToBottom() { + receiveScroll.contentY = Math.max(0, receiveScroll.contentHeight - receiveScroll.height) + } + + Timer { + id: scrollToBottomRetryTimer + interval: 48 + repeat: true + property int retries: 0 + onTriggered: { + root.scrollPairingToBottom() + retries++ + if (retries >= 12) { + stop() + } + } + onRunningChanged: { + if (!running) { + retries = 0 + } + } + } + + Timer { + id: pairingCountdownTimer + interval: 1000 + repeat: true + running: PairingUiController.tvPairingBusy && PairingUiController.tvQrCodesCount > 0 && root.pairingSecondsLeft > 0 + onTriggered: { + if (root.pairingSecondsLeft > 0) { + root.pairingSecondsLeft-- + } + } + } + + Connections { + target: root + function onVisibleChanged() { + if (!root.visible) { + PairingUiController.cancelAllPairingActivity() + scrollToBottomRetryTimer.stop() + pairingCountdownTimer.stop() + root.pairingSecondsLeft = 0 + } + } + } + + FlickableType { + id: receiveScroll + anchors.fill: parent + contentHeight: layout.implicitHeight + + Behavior on contentY { + NumberAnimation { + duration: 320 + easing.type: Easing.OutCubic + } + } + + onContentHeightChanged: { + if (PairingUiController.tvQrCodesCount > 0) { + Qt.callLater(root.scrollPairingToBottom) + } + } + + ColumnLayout { + id: layout + width: root.width + spacing: 8 + + BackButtonType { + Layout.topMargin: 20 + PageController.safeAreaTopMargin + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + text: qsTr("Get Premium server from mobile") + font.pixelSize: 28 + font.bold: true + color: AmneziaStyle.color.paleGray + wrapMode: Text.Wrap + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + color: AmneziaStyle.color.goldenApricot + text: qsTr("Amnezia Premium only. Someone who already has this subscription in Amnezia on a phone or tablet must send it to you; otherwise the session expires.") + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 12 + text: qsTr("How to get the server from a mobile device") + font.pixelSize: 18 + font.bold: true + color: AmneziaStyle.color.mutedGray + wrapMode: Text.Wrap + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("On this device (TV, tablet, or second phone):\n1) In the “Start receiving” section, tap “Start and show QR” and leave this screen open until the transfer finishes or times out.\n\nOn the mobile device that already has Amnezia Premium:\n2) Open Amnezia VPN → Settings (gear).\n3) Select your Amnezia Premium API server in the list, then open its details screen.\n4) Choose “Transfer by QR (send)”.\n5) Scan the QR code shown on this device, or paste the session ID if you copy it from this screen.\n6) Tap “Send from current subscription” and wait. When the gateway completes pairing, this device receives the configuration and adds the server.") + wrapMode: Text.Wrap + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + visible: PairingUiController.tvStatusMessage.length > 0 + text: PairingUiController.tvStatusMessage + wrapMode: Text.Wrap + } + + Item { + id: qrBox + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.preferredHeight: PairingUiController.tvQrCodesCount > 0 ? width : 0 + visible: PairingUiController.tvQrCodesCount > 0 + + Image { + id: qrImage + anchors.fill: parent + fillMode: Image.PreserveAspectFit + sourceSize: Qt.size(2048, 2048) + source: PairingUiController.tvQrCodesCount > 0 ? PairingUiController.tvQrCodes[root.qrImageIndex] : "" + + MouseArea { + anchors.fill: parent + enabled: PairingUiController.tvQrCodesCount > 1 + onClicked: { + root.qrImageIndex = (root.qrImageIndex + 1) % PairingUiController.tvQrCodesCount + } + } + } + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + visible: PairingUiController.tvPairingBusy && PairingUiController.tvQrCodesCount > 0 + color: AmneziaStyle.color.mutedGray + text: qsTr("This QR code will refresh in %1. If the session expires, tap Start again for a new code.") + .arg(root.formatMmSs(root.pairingSecondsLeft)) + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + text: qsTr("Start receiving") + font.pixelSize: 18 + font.bold: true + color: AmneziaStyle.color.mutedGray + } + + BasicButtonType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: PairingUiController.tvPairingBusy ? qsTr("Waiting…") : qsTr("Start and show QR") + enabled: !PairingUiController.tvPairingBusy && !PairingUiController.phonePairingBusy + clickedFunc: function() { + PairingUiController.startTvQrSession() + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("Cancel receive") + enabled: PairingUiController.tvPairingBusy + clickedFunc: function() { + PairingUiController.cancelTvQrSession() + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 24 + PageController.safeAreaBottomMargin + } + } + } + + Connections { + target: PairingUiController + + function onTvQrCodesChanged() { + root.qrImageIndex = 0 + if (PairingUiController.tvQrCodesCount > 0) { + root.pairingSecondsLeft = PairingUiController.tvPairingWaitWindowSeconds + scrollToBottomRetryTimer.retries = 0 + scrollToBottomRetryTimer.start() + Qt.callLater(function() { + root.scrollPairingToBottom() + }) + Qt.callLater(function() { + Qt.callLater(function() { + root.scrollPairingToBottom() + }) + }) + } + } + + function onTvSessionUuidChanged() { + root.qrImageIndex = 0 + } + + function onTvPairingConfigReceived() { + scrollToBottomRetryTimer.stop() + root.pairingSecondsLeft = 0 + qrImage.source = "" + PageController.showNotificationMessage(qsTr("Configuration received from gateway")) + Qt.callLater(function() { + PageController.closePage() + }) + } + } + +} diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 1f3318933..f7c12bc63 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -269,6 +269,7 @@ PageType { selfHostVpn, backupRestore, fileOpen, + gatewayQrPairingAddServer, qrScan, restorePurchases, siteLink @@ -343,6 +344,19 @@ PageType { } } + QtObject { + id: gatewayQrPairingAddServer + + property bool featuredAmneziaConnection: false + property string title: qsTr("Get Premium server from mobile") + property string description: qsTr("Premium · QR transfer — steps inside") + property string imageSource: "qrc:/images/controls/qr-code.svg" + property bool isVisible: true + property var handler: function() { + PageController.goToPage(PageEnum.PageSetupWizardApiQrPairingReceive) + } + } + QtObject { id: qrScan diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index 30084b6b9..7eb5ab27c 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -85,7 +85,9 @@ Pages2/PageSettingsAbout.qml Pages2/PageSettingsApiAvailableCountries.qml Pages2/PageSettingsApiServerInfo.qml - Pages2/PageSettingsApiQrPairing.qml + Pages2/PageSettingsApiQrPairingDev.qml + Pages2/PageSettingsApiQrPairingSend.qml + Pages2/PageSetupWizardApiQrPairingReceive.qml Pages2/PageSettingsApplication.qml Pages2/PageSettingsAppSplitTunneling.qml Pages2/PageSettingsBackup.qml