fixed scanner phone & fix UI/UX

This commit is contained in:
dranik
2026-05-08 09:56:04 +03:00
parent ab12a0b3f0
commit 433ecb448f
15 changed files with 497 additions and 170 deletions
+1
View File
@@ -34,6 +34,7 @@ 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}")
set(AMNEZIA_QR_PAIRING_ALLOW ON)
if(AMNEZIA_QR_PAIRING_ALLOW)
include(../.cache/agw_rsa_public_keys.cmake)
endif()
@@ -2,14 +2,11 @@
#include <QJsonDocument>
#include <QSysInfo>
#include <QUrl>
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/networkUtilities.h"
#include "version.h"
using namespace amnezia;
@@ -22,22 +19,6 @@ constexpr qsizetype kPairingMaxQrUuidChars = 128;
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
bool isLocalGatewayHost(const QString &gatewayUrl)
{
if (gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("[::1]"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("::1"), Qt::CaseInsensitive)) {
return true;
}
#ifdef AMNEZIA_QR_PAIRING_ALLOW
const QUrl u(gatewayUrl);
return NetworkUtilities::hostIsPrivateLanAddress(u.host());
#else
return false;
#endif
}
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
{
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
@@ -76,6 +57,14 @@ ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingCont
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
{
const QString msgProbe = obj.value(QStringLiteral("message")).toString();
if (msgProbe.contains(QStringLiteral("limit"), Qt::CaseInsensitive)
&& (msgProbe.contains(QStringLiteral("device"), Qt::CaseInsensitive)
|| msgProbe.contains(QStringLiteral("maximum"), Qt::CaseInsensitive)
|| msgProbe.contains(QStringLiteral("max"), Qt::CaseInsensitive))) {
return ErrorCode::ApiConfigLimitError;
}
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
if (apiStatus != ErrorCode::NoError) {
return apiStatus;
@@ -127,10 +116,23 @@ ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &respo
return interpretGenerateQrJson(obj, outPayload);
}
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody)
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName)
{
if (outOptionalDisplayName) {
outOptionalDisplayName->clear();
}
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretScanQrJson(obj);
const ErrorCode err = interpretScanQrJson(obj);
if (err != ErrorCode::NoError) {
return err;
}
if (outOptionalDisplayName) {
const QString deviceName = obj.value(QStringLiteral("device_name")).toString().trimmed();
if (!deviceName.isEmpty()) {
*outOptionalDisplayName = deviceName;
}
}
return ErrorCode::NoError;
}
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey)
@@ -154,10 +156,6 @@ PairingController::PairingController(SecureAppSettingsRepository *appSettingsRep
int PairingController::pairingLongPollTimeoutMsecs() const
{
const QString endpoint = m_appSettingsRepository->getGatewayEndpoint();
if (isLocalGatewayHost(endpoint)) {
return 120 * 1000;
}
return 30 * 1000;
}
@@ -32,7 +32,7 @@ public:
const QJsonArray &supportedProtocols, const QString &apiKey) const;
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody);
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName = nullptr);
/** Length bounds before `scan_qr` (avoids huge JSON / abuse). */
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey);
+20
View File
@@ -61,6 +61,26 @@ private slots:
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::NoError);
}
void scanQr_messageOk_extractsDeviceName()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("OK");
o[QStringLiteral("device_name")] = QStringLiteral("TestPhone");
const QByteArray body = QJsonDocument(o).toJson();
QString name;
QCOMPARE(PairingController::parseScanQrResponseBody(body, &name), ErrorCode::NoError);
QCOMPARE(name, QStringLiteral("TestPhone"));
}
void scanQr_deviceLimitMessage()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("Device limit reached for subscription");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiConfigLimitError);
}
void scanQr_http403()
{
QJsonObject o;
@@ -1,8 +1,10 @@
#include "pairingUiController.h"
#include <QCoreApplication>
#include <QDataStream>
#include <QDebug>
#include <QIODevice>
#include <QMetaObject>
#include <QRegularExpression>
#include <QTimer>
#include <QUuid>
@@ -117,6 +119,25 @@ PairingUiController::~PairingUiController()
#endif
}
void PairingUiController::setPendingPhonePairingUuid(const QString &uuid)
{
const QString trimmed = uuid.trimmed();
if (m_pendingPhonePairingUuid == trimmed) {
return;
}
m_pendingPhonePairingUuid = trimmed;
emit pendingPhonePairingUuidChanged();
}
void PairingUiController::clearPendingPhonePairingUuid()
{
if (m_pendingPhonePairingUuid.isEmpty()) {
return;
}
m_pendingPhonePairingUuid.clear();
emit pendingPhonePairingUuidChanged();
}
void PairingUiController::setTvPairingUiPhase(int phase)
{
if (m_tvPairingUiPhase == phase) {
@@ -172,9 +193,25 @@ bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
bool PairingUiController::tryConsumeAndroidQrScan(const QString &code)
{
if (!g_pairingUiForAndroidQr) {
qWarning() << "[PairingUi] tryConsumeAndroidQrScan: no controller (g_pairingUiForAndroidQr null)";
return false;
}
return g_pairingUiForAndroidQr->applyScannedTextAsPairingUuid(code);
PairingUiController *const ctl = g_pairingUiForAndroidQr;
bool consumed = false;
const QString codeCopy = code;
QObject *const app = QCoreApplication::instance();
if (!app) {
return false;
}
// CameraActivity / ML Kit may invoke JNI from a non-Qt thread. Signals and QML must run on the Qt GUI thread.
QMetaObject::invokeMethod(
app,
[ctl, codeCopy, &consumed]() {
consumed = ctl->applyScannedTextAsPairingUuid(codeCopy);
},
Qt::BlockingQueuedConnection);
qInfo() << "[PairingUi] tryConsumeAndroidQrScan consumed=" << consumed << "rawLen=" << codeCopy.size();
return consumed;
}
#endif
@@ -418,6 +455,12 @@ void PairingUiController::cancelAllPairingActivity()
m_phoneStatusMessage.clear();
emit phoneStatusMessageChanged();
clearPendingPhonePairingUuid();
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
m_lastSuccessfulPhonePairingDisplayName.clear();
emit lastSuccessfulPhonePairingDisplayNameChanged();
}
cancelTvQrSession();
}
@@ -484,6 +527,11 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn
++m_phoneSessionGeneration;
const quint64 phoneGeneration = m_phoneSessionGeneration;
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
m_lastSuccessfulPhonePairingDisplayName.clear();
emit lastSuccessfulPhonePairingDisplayNameChanged();
}
m_phoneStatusMessage = tr("Sending…");
emit phoneStatusMessageChanged();
setPhoneBusy(true);
@@ -533,18 +581,32 @@ void PairingUiController::dispatchPhoneScanQrAttempt(const QString &qrUuid, cons
m_phoneNetworkReply.clear();
ErrorCode logicalErr = result.first;
QString scanDisplayName;
if (logicalErr == ErrorCode::NoError) {
logicalErr = PairingController::parseScanQrResponseBody(result.second);
logicalErr = PairingController::parseScanQrResponseBody(result.second, &scanDisplayName);
}
if (logicalErr == ErrorCode::NoError) {
setPhoneBusy(false);
m_phoneStatusMessage = tr("Sent successfully");
emit phoneStatusMessageChanged();
if (m_lastSuccessfulPhonePairingDisplayName != scanDisplayName) {
m_lastSuccessfulPhonePairingDisplayName = scanDisplayName;
emit lastSuccessfulPhonePairingDisplayNameChanged();
}
clearPendingPhonePairingUuid();
emit phonePairingSucceeded();
return;
}
if (logicalErr == ErrorCode::ApiConfigLimitError) {
setPhoneBusy(false);
m_phoneStatusMessage.clear();
emit phoneStatusMessageChanged();
emit phonePairingRejectedDeviceLimit();
return;
}
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
const int delayMs = pairingRetryDelayMs(retryAttempt);
QTimer::singleShot(delayMs, this, [this, qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols,
@@ -29,6 +29,10 @@ class PairingUiController : public QObject
Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged)
Q_PROPERTY(QString phoneStatusMessage READ phoneStatusMessage NOTIFY phoneStatusMessageChanged)
Q_PROPERTY(QString pendingPhonePairingUuid READ pendingPhonePairingUuid WRITE setPendingPhonePairingUuid NOTIFY
pendingPhonePairingUuidChanged)
Q_PROPERTY(QString lastSuccessfulPhonePairingDisplayName READ lastSuccessfulPhonePairingDisplayName NOTIFY
lastSuccessfulPhonePairingDisplayNameChanged)
/** TV flow for QA: 0=idle, 1=waitingForPeer, 2=error, 3=sessionExpired */
Q_PROPERTY(int tvPairingUiPhase READ tvPairingUiPhase NOTIFY tvPairingUiPhaseChanged)
@@ -47,6 +51,9 @@ public:
bool phonePairingBusy() const;
QString phoneStatusMessage() const;
QString pendingPhonePairingUuid() const { return m_pendingPhonePairingUuid; }
void setPendingPhonePairingUuid(const QString &uuid);
QString lastSuccessfulPhonePairingDisplayName() const { return m_lastSuccessfulPhonePairingDisplayName; }
int tvPairingUiPhase() const { return m_tvPairingUiPhase; }
#if defined(Q_OS_ANDROID)
@@ -68,6 +75,8 @@ public slots:
/** If \a raw contains a session UUID (not vpn://), emits pairingUuidFromScan and returns true. */
bool applyScannedTextAsPairingUuid(const QString &raw);
Q_INVOKABLE void clearPendingPhonePairingUuid();
signals:
void errorOccurred(amnezia::ErrorCode errorCode);
void tvQrCodesChanged();
@@ -76,9 +85,13 @@ signals:
void tvStatusMessageChanged();
void phonePairingBusyChanged();
void phoneStatusMessageChanged();
void pendingPhonePairingUuidChanged();
void lastSuccessfulPhonePairingDisplayNameChanged();
void tvPairingConfigReceived();
void phonePairingSucceeded();
/** scan_qr rejected: subscription device quota full (no generic error dialog). */
void phonePairingRejectedDeviceLimit();
void pairingUuidFromScan(const QString &uuid);
void tvPairingUiPhaseChanged();
@@ -109,6 +122,8 @@ private:
bool m_phonePairingBusy = false;
QString m_phoneStatusMessage;
QString m_pendingPhonePairingUuid;
QString m_lastSuccessfulPhonePairingDisplayName;
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_phoneWatcher;
QPointer<QNetworkReply> m_phoneNetworkReply;
quint64 m_phoneSessionGeneration { 0 };
@@ -83,6 +83,7 @@ namespace PageLoader
PageSettingsApiQrPairingDev,
PageSettingsApiQrPairingSend,
PageSetupWizardApiQrPairingReceive,
PageSettingsApiDeviceLimit,
PageDevMenu
};
@@ -1,5 +1,7 @@
#include "apiAccountInfoModel.h"
#include <QtGlobal>
#include <QDateTime>
#include <QJsonObject>
@@ -106,6 +108,19 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
case IsInAppPurchaseRole: {
return m_accountInfoData.isInAppPurchase;
}
case ActiveDeviceCountRole: {
return m_accountInfoData.activeDeviceCount;
}
case MaxDeviceCountRole: {
return m_accountInfoData.maxDeviceCount;
}
case AvailableDeviceSlotsRole: {
if (m_accountInfoData.maxDeviceCount <= 0) {
return 1 << 20;
}
const int spare = m_accountInfoData.maxDeviceCount - m_accountInfoData.activeDeviceCount;
return qMax(0, spare);
}
}
return QVariant();
@@ -205,6 +220,9 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[IsInAppPurchaseRole] = "isInAppPurchase";
roles[ActiveDeviceCountRole] = "activeDeviceCount";
roles[MaxDeviceCountRole] = "maxDeviceCount";
roles[AvailableDeviceSlotsRole] = "availableDeviceSlots";
return roles;
}
+4 -1
View File
@@ -25,7 +25,10 @@ public:
IsProtocolSelectionSupportedRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
IsInAppPurchaseRole
IsInAppPurchaseRole,
ActiveDeviceCountRole,
MaxDeviceCountRole,
AvailableDeviceSlotsRole
};
explicit ApiAccountInfoModel(QObject *parent = nullptr);
@@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
readonly property int activeDevices: ApiAccountInfoModel.data("activeDeviceCount")
readonly property int maxDevices: ApiAccountInfoModel.data("maxDeviceCount")
FlickableType {
anchors.fill: parent
contentHeight: layout.implicitHeight
ColumnLayout {
id: layout
width: root.width
spacing: 16
BackButtonType {
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
text: qsTr("Device limit reached")
font.pixelSize: 28
font.bold: true
color: AmneziaStyle.color.paleGray
wrapMode: Text.Wrap
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: root.maxDevices > 0
? qsTr("The maximum number of devices is already in use (%1 of %2). Remove a device to add a new one.")
.arg(root.activeDevices).arg(root.maxDevices)
: qsTr("The maximum number of devices for this subscription is already in use. Remove a device to add a new one.")
wrapMode: Text.Wrap
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 24 + PageController.safeAreaBottomMargin
text: qsTr("View All Devices")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
PageController.closePage()
}
}
}
}
}
@@ -19,6 +19,43 @@ import "../Components"
PageType {
id: root
function openAddDeviceViaQr() {
const maxC = ApiAccountInfoModel.data("maxDeviceCount")
const activeC = ApiAccountInfoModel.data("activeDeviceCount")
if (maxC > 0 && activeC >= maxC) {
PageController.goToPage(PageEnum.PageSettingsApiDeviceLimit)
} else {
PageController.goToPage(PageEnum.PageSettingsApiQrPairingSend)
}
}
Connections {
target: PairingUiController
function onPhonePairingSucceeded() {
if (!root.visible) {
return
}
const serverIndex = ServersUiController.getProcessedServerIndex()
SubscriptionUiController.getAccountInfo(serverIndex, true)
SubscriptionUiController.updateApiDevicesModel()
const label = PairingUiController.lastSuccessfulPhonePairingDisplayName
if (label.length > 0) {
PageController.showNotificationMessage(
qsTr("%1 has been added to your subscription").arg(label))
} else {
PageController.showNotificationMessage(qsTr("New device has been added to your subscription"))
}
}
function onPhonePairingRejectedDeviceLimit() {
if (!root.visible) {
return
}
PageController.goToPage(PageEnum.PageSettingsApiDeviceLimit)
}
}
ListViewType {
id: listView
@@ -46,6 +83,41 @@ PageType {
descriptionText: qsTr("Manage currently connected devices")
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 20
implicitHeight: 52
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Add Device via QR Code")
clickedFunc: function() {
root.openAddDeviceViaQr()
}
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 12
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
font.pixelSize: 13
color: AmneziaStyle.color.mutedGray
text: qsTr("On the other device, tap + at the bottom, then choose Connect to Amnezia Premium")
}
WarningType {
Layout.topMargin: 16
Layout.rightMargin: 16
@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import QRCodeReader 1.0
import PageEnum 1.0
import Style 1.0
import "../Controls2"
@@ -13,19 +14,15 @@ import "../Components"
PageType {
id: root
property bool pairingCameraOpen: false
/** iOS AVFoundation can fire the same QR repeatedly; avoid stacking identical toasts. */
property int lastPairingScanToastClockMs: 0
/** 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
function notifyPairingScanSuccess() {
const now = new Date().getTime()
if (now - root.lastPairingScanToastClockMs < 1600) {
return
}
root.lastPairingScanToastClockMs = now
PageController.showNotificationMessage(
qsTr("QR session ID captured. Tap Send from current subscription to complete pairing."))
}
property bool pairingCameraOpen: false
property int lastInvalidPairingQrToastClockMs: 0
/** iOS may deliver many QR frames; guard duplicate step transitions. */
property bool addDeviceConfirmNavigationScheduled: false
Timer {
id: pairingCameraKickTimer
@@ -50,14 +47,25 @@ PageType {
pairingQrReader.startReading()
}
Component.onDestruction: {
if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
PairingUiController.cancelAllPairingActivity()
}
}
Connections {
target: root
function onVisibleChanged() {
if (!root.visible) {
if (root.visible) {
root.addDeviceConfirmNavigationScheduled = false
} else {
pairingCameraKickTimer.stop()
pairingQrReader.stopReading()
root.pairingCameraOpen = false
PairingUiController.cancelAllPairingActivity()
root.pairingWizardStep = 0
if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
PairingUiController.cancelAllPairingActivity()
}
}
}
}
@@ -90,146 +98,215 @@ PageType {
FlickableType {
anchors.fill: parent
contentHeight: layout.implicitHeight
interactive: contentHeight > height
ColumnLayout {
id: layout
width: root.width
spacing: 8
spacing: 0
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 servers 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 devices 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()
backButtonFunction: function() {
if (root.pairingWizardStep === 1) {
PairingUiController.cancelAllPairingActivity()
root.pairingWizardStep = 0
root.addDeviceConfirmNavigationScheduled = false
} else {
root.pairingCameraOpen = !root.pairingCameraOpen
PageController.closePage()
}
}
}
Item {
id: cameraSlot
StackLayout {
id: stepStack
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
currentIndex: root.pairingWizardStep
QRCodeReader {
id: pairingQrReader
ColumnLayout {
Layout.fillWidth: true
spacing: 8
onCodeReaded: function(code) {
if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
pairingQrReader.stopReading()
root.pairingCameraOpen = false
root.notifyPairingScanSuccess()
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 (Qt.platform.os === "android") {
PairingUiController.openPairingQrScanner()
} else {
root.pairingCameraOpen = !root.pairingCameraOpen
}
}
}
}
onVisibleChanged: {
if (!visible) {
pairingQrReader.stopReading()
return
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 devices “receive config” screen."))
}
}
}
}
onVisibleChanged: {
if (!visible) {
pairingQrReader.stopReading()
return
}
if (Qt.platform.os === "ios") {
pairingCameraKickTimer.restart()
}
}
}
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
}
}
}
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())
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
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Devices available with Amnezia Premium: %1").arg(ApiAccountInfoModel.data("availableDeviceSlots"))
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
}
}
}
}
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
if (root.addDeviceConfirmNavigationScheduled) {
return
}
root.addDeviceConfirmNavigationScheduled = true
pairingQrReader.stopReading()
root.pairingCameraOpen = false
PairingUiController.pendingPhonePairingUuid = uuid
Qt.callLater(function() {
root.pairingWizardStep = 1
})
}
}
}
@@ -375,20 +375,6 @@ PageType {
visible: footer.isVisibleForAmneziaFree
}
LabelWithButtonType {
Layout.fillWidth: true
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.PageSettingsApiQrPairingSend)
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32
+1
View File
@@ -87,6 +87,7 @@
<file>Pages2/PageSettingsApiServerInfo.qml</file>
<file>Pages2/PageSettingsApiQrPairingDev.qml</file>
<file>Pages2/PageSettingsApiQrPairingSend.qml</file>
<file>Pages2/PageSettingsApiDeviceLimit.qml</file>
<file>Pages2/PageSetupWizardApiQrPairingReceive.qml</file>
<file>Pages2/PageSettingsApplication.qml</file>
<file>Pages2/PageSettingsAppSplitTunneling.qml</file>
+4 -4
View File
@@ -34,8 +34,8 @@ var (
sessions = map[string]*pairingSession{}
// Configured from flags / env in main().
pairingSessionTTL = 120 * time.Second
longPollWaitLimit = 120 * time.Second
pairingSessionTTL = 30 * time.Second
longPollWaitLimit = 30 * time.Second
rateLimitExcessAfter = 0 // Set to 5 to mimic "more than 5 requests per 24h". 0 = first amnezia-free request may return CAPTCHA.
// No trailing slash; used by POST /v1/updater_endpoint so remote clients (e.g. iOS) poll the Mac, not 127.0.0.1 on-device.
publicUpdaterBaseURL string
@@ -644,8 +644,8 @@ func main() {
publicFlag := flag.String("public-base", strings.TrimSpace(os.Getenv("LOCAL_GATEWAY_PUBLIC_BASE")),
"Base URL without trailing slash for /v1/updater_endpoint (required for iOS-on-LAN). Env: LOCAL_GATEWAY_PUBLIC_BASE")
autoPublic := flag.Bool("auto-public", true, "If public-base empty, derive http://<first-lan-ipv4>:port")
pairTTL := flag.Duration("pairing-ttl", 120*time.Second, "QR pairing session TTL")
longPoll := flag.Duration("long-poll", 120*time.Second, "Long-poll max wait for POST /api/v1/generate_qr")
pairTTL := flag.Duration("pairing-ttl", 30*time.Second, "QR pairing session TTL")
longPoll := flag.Duration("long-poll", 30*time.Second, "Long-poll max wait for POST /api/v1/generate_qr")
rateN := flag.Int("rate-limit-excess-after", 0, "Amnezia Free: allow N requests per 24h window before rate-limit/CAPTCHA (0=tight)")
flag.Parse()