mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +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(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||||
|
|
||||||
|
set(AMNEZIA_QR_PAIRING_ALLOW ON)
|
||||||
if(AMNEZIA_QR_PAIRING_ALLOW)
|
if(AMNEZIA_QR_PAIRING_ALLOW)
|
||||||
include(../.cache/agw_rsa_public_keys.cmake)
|
include(../.cache/agw_rsa_public_keys.cmake)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
#include <QUrl>
|
|
||||||
|
|
||||||
#include "core/controllers/gatewayController.h"
|
#include "core/controllers/gatewayController.h"
|
||||||
#include "core/repositories/secureAppSettingsRepository.h"
|
#include "core/repositories/secureAppSettingsRepository.h"
|
||||||
#include "core/utils/api/apiUtils.h"
|
#include "core/utils/api/apiUtils.h"
|
||||||
#include "core/utils/constants/apiConstants.h"
|
#include "core/utils/constants/apiConstants.h"
|
||||||
#include "core/utils/constants/apiKeys.h"
|
#include "core/utils/constants/apiKeys.h"
|
||||||
#include "core/utils/networkUtilities.h"
|
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
using namespace amnezia;
|
using namespace amnezia;
|
||||||
@@ -22,22 +19,6 @@ constexpr qsizetype kPairingMaxQrUuidChars = 128;
|
|||||||
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
|
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
|
||||||
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
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 applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||||
{
|
{
|
||||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||||
@@ -76,6 +57,14 @@ ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingCont
|
|||||||
|
|
||||||
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
|
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);
|
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||||
if (apiStatus != ErrorCode::NoError) {
|
if (apiStatus != ErrorCode::NoError) {
|
||||||
return apiStatus;
|
return apiStatus;
|
||||||
@@ -127,10 +116,23 @@ ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &respo
|
|||||||
return interpretGenerateQrJson(obj, outPayload);
|
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();
|
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)
|
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey)
|
||||||
@@ -154,10 +156,6 @@ PairingController::PairingController(SecureAppSettingsRepository *appSettingsRep
|
|||||||
|
|
||||||
int PairingController::pairingLongPollTimeoutMsecs() const
|
int PairingController::pairingLongPollTimeoutMsecs() const
|
||||||
{
|
{
|
||||||
const QString endpoint = m_appSettingsRepository->getGatewayEndpoint();
|
|
||||||
if (isLocalGatewayHost(endpoint)) {
|
|
||||||
return 120 * 1000;
|
|
||||||
}
|
|
||||||
return 30 * 1000;
|
return 30 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public:
|
|||||||
const QJsonArray &supportedProtocols, const QString &apiKey) const;
|
const QJsonArray &supportedProtocols, const QString &apiKey) const;
|
||||||
|
|
||||||
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
|
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). */
|
/** Length bounds before `scan_qr` (avoids huge JSON / abuse). */
|
||||||
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey);
|
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);
|
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()
|
void scanQr_http403()
|
||||||
{
|
{
|
||||||
QJsonObject o;
|
QJsonObject o;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#include "pairingUiController.h"
|
#include "pairingUiController.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDataStream>
|
#include <QDataStream>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
|
#include <QMetaObject>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
@@ -117,6 +119,25 @@ PairingUiController::~PairingUiController()
|
|||||||
#endif
|
#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)
|
void PairingUiController::setTvPairingUiPhase(int phase)
|
||||||
{
|
{
|
||||||
if (m_tvPairingUiPhase == phase) {
|
if (m_tvPairingUiPhase == phase) {
|
||||||
@@ -172,9 +193,25 @@ bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
|
|||||||
bool PairingUiController::tryConsumeAndroidQrScan(const QString &code)
|
bool PairingUiController::tryConsumeAndroidQrScan(const QString &code)
|
||||||
{
|
{
|
||||||
if (!g_pairingUiForAndroidQr) {
|
if (!g_pairingUiForAndroidQr) {
|
||||||
|
qWarning() << "[PairingUi] tryConsumeAndroidQrScan: no controller (g_pairingUiForAndroidQr null)";
|
||||||
return false;
|
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
|
#endif
|
||||||
|
|
||||||
@@ -418,6 +455,12 @@ void PairingUiController::cancelAllPairingActivity()
|
|||||||
m_phoneStatusMessage.clear();
|
m_phoneStatusMessage.clear();
|
||||||
emit phoneStatusMessageChanged();
|
emit phoneStatusMessageChanged();
|
||||||
|
|
||||||
|
clearPendingPhonePairingUuid();
|
||||||
|
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
|
||||||
|
m_lastSuccessfulPhonePairingDisplayName.clear();
|
||||||
|
emit lastSuccessfulPhonePairingDisplayNameChanged();
|
||||||
|
}
|
||||||
|
|
||||||
cancelTvQrSession();
|
cancelTvQrSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +527,11 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn
|
|||||||
++m_phoneSessionGeneration;
|
++m_phoneSessionGeneration;
|
||||||
const quint64 phoneGeneration = m_phoneSessionGeneration;
|
const quint64 phoneGeneration = m_phoneSessionGeneration;
|
||||||
|
|
||||||
|
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
|
||||||
|
m_lastSuccessfulPhonePairingDisplayName.clear();
|
||||||
|
emit lastSuccessfulPhonePairingDisplayNameChanged();
|
||||||
|
}
|
||||||
|
|
||||||
m_phoneStatusMessage = tr("Sending…");
|
m_phoneStatusMessage = tr("Sending…");
|
||||||
emit phoneStatusMessageChanged();
|
emit phoneStatusMessageChanged();
|
||||||
setPhoneBusy(true);
|
setPhoneBusy(true);
|
||||||
@@ -533,18 +581,32 @@ void PairingUiController::dispatchPhoneScanQrAttempt(const QString &qrUuid, cons
|
|||||||
m_phoneNetworkReply.clear();
|
m_phoneNetworkReply.clear();
|
||||||
|
|
||||||
ErrorCode logicalErr = result.first;
|
ErrorCode logicalErr = result.first;
|
||||||
|
QString scanDisplayName;
|
||||||
if (logicalErr == ErrorCode::NoError) {
|
if (logicalErr == ErrorCode::NoError) {
|
||||||
logicalErr = PairingController::parseScanQrResponseBody(result.second);
|
logicalErr = PairingController::parseScanQrResponseBody(result.second, &scanDisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logicalErr == ErrorCode::NoError) {
|
if (logicalErr == ErrorCode::NoError) {
|
||||||
setPhoneBusy(false);
|
setPhoneBusy(false);
|
||||||
m_phoneStatusMessage = tr("Sent successfully");
|
m_phoneStatusMessage = tr("Sent successfully");
|
||||||
emit phoneStatusMessageChanged();
|
emit phoneStatusMessageChanged();
|
||||||
|
if (m_lastSuccessfulPhonePairingDisplayName != scanDisplayName) {
|
||||||
|
m_lastSuccessfulPhonePairingDisplayName = scanDisplayName;
|
||||||
|
emit lastSuccessfulPhonePairingDisplayNameChanged();
|
||||||
|
}
|
||||||
|
clearPendingPhonePairingUuid();
|
||||||
emit phonePairingSucceeded();
|
emit phonePairingSucceeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (logicalErr == ErrorCode::ApiConfigLimitError) {
|
||||||
|
setPhoneBusy(false);
|
||||||
|
m_phoneStatusMessage.clear();
|
||||||
|
emit phoneStatusMessageChanged();
|
||||||
|
emit phonePairingRejectedDeviceLimit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
|
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
|
||||||
const int delayMs = pairingRetryDelayMs(retryAttempt);
|
const int delayMs = pairingRetryDelayMs(retryAttempt);
|
||||||
QTimer::singleShot(delayMs, this, [this, qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols,
|
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(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged)
|
||||||
Q_PROPERTY(QString phoneStatusMessage READ phoneStatusMessage NOTIFY phoneStatusMessageChanged)
|
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 */
|
/** TV flow for QA: 0=idle, 1=waitingForPeer, 2=error, 3=sessionExpired */
|
||||||
Q_PROPERTY(int tvPairingUiPhase READ tvPairingUiPhase NOTIFY tvPairingUiPhaseChanged)
|
Q_PROPERTY(int tvPairingUiPhase READ tvPairingUiPhase NOTIFY tvPairingUiPhaseChanged)
|
||||||
|
|
||||||
@@ -47,6 +51,9 @@ public:
|
|||||||
|
|
||||||
bool phonePairingBusy() const;
|
bool phonePairingBusy() const;
|
||||||
QString phoneStatusMessage() 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; }
|
int tvPairingUiPhase() const { return m_tvPairingUiPhase; }
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID)
|
#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. */
|
/** If \a raw contains a session UUID (not vpn://), emits pairingUuidFromScan and returns true. */
|
||||||
bool applyScannedTextAsPairingUuid(const QString &raw);
|
bool applyScannedTextAsPairingUuid(const QString &raw);
|
||||||
|
|
||||||
|
Q_INVOKABLE void clearPendingPhonePairingUuid();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(amnezia::ErrorCode errorCode);
|
void errorOccurred(amnezia::ErrorCode errorCode);
|
||||||
void tvQrCodesChanged();
|
void tvQrCodesChanged();
|
||||||
@@ -76,9 +85,13 @@ signals:
|
|||||||
void tvStatusMessageChanged();
|
void tvStatusMessageChanged();
|
||||||
void phonePairingBusyChanged();
|
void phonePairingBusyChanged();
|
||||||
void phoneStatusMessageChanged();
|
void phoneStatusMessageChanged();
|
||||||
|
void pendingPhonePairingUuidChanged();
|
||||||
|
void lastSuccessfulPhonePairingDisplayNameChanged();
|
||||||
|
|
||||||
void tvPairingConfigReceived();
|
void tvPairingConfigReceived();
|
||||||
void phonePairingSucceeded();
|
void phonePairingSucceeded();
|
||||||
|
/** scan_qr rejected: subscription device quota full (no generic error dialog). */
|
||||||
|
void phonePairingRejectedDeviceLimit();
|
||||||
|
|
||||||
void pairingUuidFromScan(const QString &uuid);
|
void pairingUuidFromScan(const QString &uuid);
|
||||||
void tvPairingUiPhaseChanged();
|
void tvPairingUiPhaseChanged();
|
||||||
@@ -109,6 +122,8 @@ private:
|
|||||||
|
|
||||||
bool m_phonePairingBusy = false;
|
bool m_phonePairingBusy = false;
|
||||||
QString m_phoneStatusMessage;
|
QString m_phoneStatusMessage;
|
||||||
|
QString m_pendingPhonePairingUuid;
|
||||||
|
QString m_lastSuccessfulPhonePairingDisplayName;
|
||||||
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_phoneWatcher;
|
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_phoneWatcher;
|
||||||
QPointer<QNetworkReply> m_phoneNetworkReply;
|
QPointer<QNetworkReply> m_phoneNetworkReply;
|
||||||
quint64 m_phoneSessionGeneration { 0 };
|
quint64 m_phoneSessionGeneration { 0 };
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ namespace PageLoader
|
|||||||
PageSettingsApiQrPairingDev,
|
PageSettingsApiQrPairingDev,
|
||||||
PageSettingsApiQrPairingSend,
|
PageSettingsApiQrPairingSend,
|
||||||
PageSetupWizardApiQrPairingReceive,
|
PageSetupWizardApiQrPairingReceive,
|
||||||
|
PageSettingsApiDeviceLimit,
|
||||||
|
|
||||||
PageDevMenu
|
PageDevMenu
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "apiAccountInfoModel.h"
|
#include "apiAccountInfoModel.h"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
|
||||||
@@ -106,6 +108,19 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
|||||||
case IsInAppPurchaseRole: {
|
case IsInAppPurchaseRole: {
|
||||||
return m_accountInfoData.isInAppPurchase;
|
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();
|
return QVariant();
|
||||||
@@ -205,6 +220,9 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
|
|||||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||||
roles[IsInAppPurchaseRole] = "isInAppPurchase";
|
roles[IsInAppPurchaseRole] = "isInAppPurchase";
|
||||||
|
roles[ActiveDeviceCountRole] = "activeDeviceCount";
|
||||||
|
roles[MaxDeviceCountRole] = "maxDeviceCount";
|
||||||
|
roles[AvailableDeviceSlotsRole] = "availableDeviceSlots";
|
||||||
|
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ public:
|
|||||||
IsProtocolSelectionSupportedRole,
|
IsProtocolSelectionSupportedRole,
|
||||||
IsSubscriptionExpiredRole,
|
IsSubscriptionExpiredRole,
|
||||||
IsSubscriptionExpiringSoonRole,
|
IsSubscriptionExpiringSoonRole,
|
||||||
IsInAppPurchaseRole
|
IsInAppPurchaseRole,
|
||||||
|
ActiveDeviceCountRole,
|
||||||
|
MaxDeviceCountRole,
|
||||||
|
AvailableDeviceSlotsRole
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ApiAccountInfoModel(QObject *parent = nullptr);
|
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 {
|
PageType {
|
||||||
id: root
|
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 {
|
ListViewType {
|
||||||
id: listView
|
id: listView
|
||||||
|
|
||||||
@@ -46,6 +83,41 @@ PageType {
|
|||||||
descriptionText: qsTr("Manage currently connected devices")
|
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 {
|
WarningType {
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
import QRCodeReader 1.0
|
import QRCodeReader 1.0
|
||||||
|
import PageEnum 1.0
|
||||||
import Style 1.0
|
import Style 1.0
|
||||||
|
|
||||||
import "../Controls2"
|
import "../Controls2"
|
||||||
@@ -13,19 +14,15 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool pairingCameraOpen: false
|
/** 0 = scan QR, 1 = confirm before sending subscription */
|
||||||
/** iOS AVFoundation can fire the same QR repeatedly; avoid stacking identical toasts. */
|
property int pairingWizardStep: 0
|
||||||
property int lastPairingScanToastClockMs: 0
|
/** True after optimistic close: keep request running in background while page is closing. */
|
||||||
|
property bool keepPhonePairingInBackgroundOnClose: false
|
||||||
|
|
||||||
function notifyPairingScanSuccess() {
|
property bool pairingCameraOpen: false
|
||||||
const now = new Date().getTime()
|
property int lastInvalidPairingQrToastClockMs: 0
|
||||||
if (now - root.lastPairingScanToastClockMs < 1600) {
|
/** iOS may deliver many QR frames; guard duplicate step transitions. */
|
||||||
return
|
property bool addDeviceConfirmNavigationScheduled: false
|
||||||
}
|
|
||||||
root.lastPairingScanToastClockMs = now
|
|
||||||
PageController.showNotificationMessage(
|
|
||||||
qsTr("QR session ID captured. Tap Send from current subscription to complete pairing."))
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: pairingCameraKickTimer
|
id: pairingCameraKickTimer
|
||||||
@@ -50,17 +47,28 @@ PageType {
|
|||||||
pairingQrReader.startReading()
|
pairingQrReader.startReading()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
|
||||||
|
PairingUiController.cancelAllPairingActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onVisibleChanged() {
|
function onVisibleChanged() {
|
||||||
if (!root.visible) {
|
if (root.visible) {
|
||||||
|
root.addDeviceConfirmNavigationScheduled = false
|
||||||
|
} else {
|
||||||
pairingCameraKickTimer.stop()
|
pairingCameraKickTimer.stop()
|
||||||
pairingQrReader.stopReading()
|
pairingQrReader.stopReading()
|
||||||
root.pairingCameraOpen = false
|
root.pairingCameraOpen = false
|
||||||
|
root.pairingWizardStep = 0
|
||||||
|
if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
|
||||||
PairingUiController.cancelAllPairingActivity()
|
PairingUiController.cancelAllPairingActivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
@@ -90,22 +98,41 @@ PageType {
|
|||||||
FlickableType {
|
FlickableType {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: layout.implicitHeight
|
contentHeight: layout.implicitHeight
|
||||||
|
interactive: contentHeight > height
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
width: root.width
|
width: root.width
|
||||||
spacing: 8
|
spacing: 0
|
||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
Layout.topMargin: 20 + PageController.safeAreaTopMargin
|
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 {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
text: qsTr("Transfer subscription (QR)")
|
text: qsTr("Add device via QR")
|
||||||
font.pixelSize: 28
|
font.pixelSize: 28
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: AmneziaStyle.color.paleGray
|
color: AmneziaStyle.color.paleGray
|
||||||
@@ -116,34 +143,15 @@ PageType {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 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.")
|
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
|
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 {
|
BasicButtonType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
|
Layout.topMargin: 16
|
||||||
visible: Qt.platform.os === "android" || Qt.platform.os === "ios"
|
visible: Qt.platform.os === "android" || Qt.platform.os === "ios"
|
||||||
text: {
|
text: {
|
||||||
if (Qt.platform.os === "ios" && root.pairingCameraOpen) {
|
if (Qt.platform.os === "ios" && root.pairingCameraOpen) {
|
||||||
@@ -174,10 +182,20 @@ PageType {
|
|||||||
id: pairingQrReader
|
id: pairingQrReader
|
||||||
|
|
||||||
onCodeReaded: function(code) {
|
onCodeReaded: function(code) {
|
||||||
|
if (root.addDeviceConfirmNavigationScheduled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
|
if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
|
||||||
|
root.addDeviceConfirmNavigationScheduled = true
|
||||||
pairingQrReader.stopReading()
|
pairingQrReader.stopReading()
|
||||||
root.pairingCameraOpen = false
|
root.pairingCameraOpen = false
|
||||||
root.notifyPairingScanSuccess()
|
} 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."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,43 +211,102 @@ PageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicButtonType {
|
ParagraphTextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
text: PairingUiController.phonePairingBusy ? qsTr("Sending…") : qsTr("Send from current subscription")
|
Layout.bottomMargin: 24 + PageController.safeAreaBottomMargin
|
||||||
enabled: !PairingUiController.phonePairingBusy
|
visible: root.pairingWizardStep === 0 && PairingUiController.phoneStatusMessage.length > 0
|
||||||
clickedFunc: function() {
|
text: PairingUiController.phoneStatusMessage
|
||||||
PairingUiController.submitPhonePairing(uuidField.textField.text, ServersUiController.getProcessedServerIndex())
|
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
|
||||||
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
ParagraphTextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.bottomMargin: 24 + PageController.safeAreaBottomMargin
|
text: qsTr("Devices available with Amnezia Premium: %1").arg(ApiAccountInfoModel.data("availableDeviceSlots"))
|
||||||
visible: PairingUiController.phoneStatusMessage.length > 0
|
|
||||||
text: PairingUiController.phoneStatusMessage
|
|
||||||
wrapMode: Text.Wrap
|
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 {
|
Connections {
|
||||||
target: PairingUiController
|
target: PairingUiController
|
||||||
|
|
||||||
function onPhonePairingSucceeded() {
|
|
||||||
root.pairingCameraOpen = false
|
|
||||||
pairingQrReader.stopReading()
|
|
||||||
PageController.showNotificationMessage(qsTr("Configuration sent"))
|
|
||||||
Qt.callLater(function() {
|
|
||||||
PageController.closePage()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPairingUuidFromScan(uuid) {
|
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
|
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 {
|
LabelWithButtonType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32
|
Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
<file>Pages2/PageSettingsApiServerInfo.qml</file>
|
<file>Pages2/PageSettingsApiServerInfo.qml</file>
|
||||||
<file>Pages2/PageSettingsApiQrPairingDev.qml</file>
|
<file>Pages2/PageSettingsApiQrPairingDev.qml</file>
|
||||||
<file>Pages2/PageSettingsApiQrPairingSend.qml</file>
|
<file>Pages2/PageSettingsApiQrPairingSend.qml</file>
|
||||||
|
<file>Pages2/PageSettingsApiDeviceLimit.qml</file>
|
||||||
<file>Pages2/PageSetupWizardApiQrPairingReceive.qml</file>
|
<file>Pages2/PageSetupWizardApiQrPairingReceive.qml</file>
|
||||||
<file>Pages2/PageSettingsApplication.qml</file>
|
<file>Pages2/PageSettingsApplication.qml</file>
|
||||||
<file>Pages2/PageSettingsAppSplitTunneling.qml</file>
|
<file>Pages2/PageSettingsAppSplitTunneling.qml</file>
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ var (
|
|||||||
sessions = map[string]*pairingSession{}
|
sessions = map[string]*pairingSession{}
|
||||||
|
|
||||||
// Configured from flags / env in main().
|
// Configured from flags / env in main().
|
||||||
pairingSessionTTL = 120 * time.Second
|
pairingSessionTTL = 30 * time.Second
|
||||||
longPollWaitLimit = 120 * 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.
|
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.
|
// 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
|
publicUpdaterBaseURL string
|
||||||
@@ -644,8 +644,8 @@ func main() {
|
|||||||
publicFlag := flag.String("public-base", strings.TrimSpace(os.Getenv("LOCAL_GATEWAY_PUBLIC_BASE")),
|
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")
|
"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")
|
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")
|
pairTTL := flag.Duration("pairing-ttl", 30*time.Second, "QR pairing session TTL")
|
||||||
longPoll := flag.Duration("long-poll", 120*time.Second, "Long-poll max wait for POST /api/v1/generate_qr")
|
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)")
|
rateN := flag.Int("rate-limit-excess-after", 0, "Amnezia Free: allow N requests per 24h window before rate-limit/CAPTCHA (0=tight)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user