import QtQuick import QtQuick.Controls import QtQuick.Layouts import QRCodeReader 1.0 import PageEnum 1.0 import Style 1.0 import "./" import "../Controls2" import "../Controls2/TextTypes" import "../Config" import "../Components" PageType { id: root /** 0 = scan QR, 1 = confirm before sending subscription */ property int pairingWizardStep: 0 /** True after optimistic close: keep request running in background while page is closing. */ property bool keepPhonePairingInBackgroundOnClose: false property bool pairingCameraOpen: false property int lastInvalidPairingQrToastClockMs: 0 /** iOS may deliver many QR frames; guard duplicate step transitions. */ property bool addDeviceConfirmNavigationScheduled: false /** Mobile: waiting for camera permission before starting scan UI / Android scanner. */ property bool awaitingCameraPermissionForScan: false /** After denial on scan screen: user may enable camera in settings. */ property bool waitingSettingsReturnForScan: false Timer { id: pairingCameraKickTimer interval: 180 repeat: false onTriggered: root.restartPairingIosCamera() } function startPairingScanAfterPermission() { if (Qt.platform.os === "android") { PairingUiController.openPairingQrScanner() } else if (Qt.platform.os === "ios") { root.pairingCameraOpen = true } } function showScanCameraDeniedDrawer() { showQuestionDrawer( qsTr("Camera access is required"), qsTr("Allow camera access to scan the pairing QR code. You can enable it in the system settings for Amnezia VPN."), qsTr("Open settings"), qsTr("Cancel"), function() { PairingUiController.openPairingCameraAppSettings() }, function() { root.waitingSettingsReturnForScan = false }) } function tryResumeScanAfterCameraSettings() { if (!root.waitingSettingsReturnForScan || !root.visible || root.pairingWizardStep !== 0) { return } if (PairingUiController.isPairingCameraAccessGranted()) { root.waitingSettingsReturnForScan = false root.startPairingScanAfterPermission() } } function restartPairingIosCamera() { if (Qt.platform.os !== "ios" || !root.pairingCameraOpen) { return } if (cameraSlot.width < 32 || cameraSlot.height < 32) { console.info("[PairingQr] cameraSlot too small wxh=", cameraSlot.width, cameraSlot.height, "retry") pairingCameraKickTimer.restart() return } var p = cameraSlot.mapToItem(root, 0, 0) console.info("[PairingQr] start preview frame", p.x, p.y, cameraSlot.width, cameraSlot.height) pairingQrReader.stopReading() pairingQrReader.setCameraSize(Qt.rect(Math.round(p.x), Math.round(p.y), Math.round(cameraSlot.width), Math.round(cameraSlot.height))) pairingQrReader.startReading() } Component.onDestruction: { if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) { PairingUiController.cancelAllPairingActivity() } } Connections { target: root function onVisibleChanged() { if (root.visible) { root.addDeviceConfirmNavigationScheduled = false } else { pairingCameraKickTimer.stop() pairingQrReader.stopReading() root.pairingCameraOpen = false root.pairingWizardStep = 0 root.waitingSettingsReturnForScan = false if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) { PairingUiController.cancelAllPairingActivity() } } } } Connections { target: Qt.application function onStateChanged() { if (Qt.application.state !== Qt.ApplicationActive) { return } root.tryResumeScanAfterCameraSettings() } } Connections { target: SettingsController enabled: Qt.platform.os === "android" function onActivityResumed() { root.tryResumeScanAfterCameraSettings() } } Connections { target: root function onPairingCameraOpenChanged() { if (!root.pairingCameraOpen) { pairingCameraKickTimer.stop() pairingQrReader.stopReading() return } if (Qt.platform.os === "ios") { pairingCameraKickTimer.restart() } } } Connections { target: cameraSlot enabled: Qt.platform.os === "ios" && root.pairingCameraOpen function onWidthChanged() { pairingCameraKickTimer.restart() } function onHeightChanged() { pairingCameraKickTimer.restart() } } FlickableType { anchors.fill: parent contentHeight: layout.implicitHeight interactive: contentHeight > height ColumnLayout { id: layout width: root.width spacing: 0 BackButtonType { Layout.topMargin: 20 + PageController.safeAreaTopMargin backButtonFunction: function() { if (root.pairingWizardStep === 1) { PairingUiController.cancelAllPairingActivity() root.pairingWizardStep = 0 root.addDeviceConfirmNavigationScheduled = false } else { PageController.closePage() } } } StackLayout { id: stepStack Layout.fillWidth: true currentIndex: root.pairingWizardStep ColumnLayout { Layout.fillWidth: true spacing: 8 Label { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 text: qsTr("Add device via 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 device you want to add. You will confirm before the subscription is sent.") wrapMode: Text.Wrap } BasicButtonType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 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 (!PairingUiController.isPairingCameraAccessGranted()) { root.awaitingCameraPermissionForScan = true PairingUiController.requestPairingCameraAccess() return } 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 (root.addDeviceConfirmNavigationScheduled) { return } if (PairingUiController.applyScannedTextAsPairingUuid(code)) { root.addDeviceConfirmNavigationScheduled = true pairingQrReader.stopReading() root.pairingCameraOpen = false } else { const now = new Date().getTime() if (now - root.lastInvalidPairingQrToastClockMs >= 2200) { root.lastInvalidPairingQrToastClockMs = now PageController.showNotificationMessage( qsTr("This QR code is not a pairing session. Show the code from the other device’s “receive config” screen.")) } } } } onVisibleChanged: { if (!visible) { pairingQrReader.stopReading() return } if (Qt.platform.os === "ios") { pairingCameraKickTimer.restart() } } } ParagraphTextType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 24 + PageController.safeAreaBottomMargin visible: root.pairingWizardStep === 0 && PairingUiController.phoneStatusMessage.length > 0 text: PairingUiController.phoneStatusMessage wrapMode: Text.Wrap } } ColumnLayout { Layout.fillWidth: true spacing: 16 Label { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 text: qsTr("Add a new device to the subscription?") font.pixelSize: 28 font.bold: true color: AmneziaStyle.color.paleGray wrapMode: Text.Wrap } BasicButtonType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 16 text: qsTr("Add Device") defaultColor: AmneziaStyle.color.paleGray hoveredColor: AmneziaStyle.color.lightGray pressedColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.midnightBlack clickedFunc: function() { root.keepPhonePairingInBackgroundOnClose = true PairingUiController.submitPhonePairing(PairingUiController.pendingPhonePairingUuid, ServersUiController.getProcessedServerIndex()) Qt.callLater(function() { PageController.closePage() }) } } BasicButtonType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.paleGray borderColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Cancel") clickedFunc: function() { PairingUiController.cancelAllPairingActivity() root.pairingWizardStep = 0 root.addDeviceConfirmNavigationScheduled = false } } } } } } Connections { target: PairingUiController function onPairingCameraAccessFinished(granted) { if (!root.awaitingCameraPermissionForScan) { return } root.awaitingCameraPermissionForScan = false if (granted) { root.startPairingScanAfterPermission() } else { root.waitingSettingsReturnForScan = true root.showScanCameraDeniedDrawer() } } function onPairingUuidFromScan(uuid) { if (root.addDeviceConfirmNavigationScheduled) { return } root.addDeviceConfirmNavigationScheduled = true pairingQrReader.stopReading() root.pairingCameraOpen = false PairingUiController.pendingPhonePairingUuid = uuid Qt.callLater(function() { root.pairingWizardStep = 1 }) } } }