mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
fixed scanner phone & fix UI/UX
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 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()
|
||||
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 device’s “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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user