fixed open Qr QML & add check error code & add test

This commit is contained in:
dranik
2026-05-07 19:15:28 +03:00
parent 2cb7b30d8a
commit 5583c0a2a9
20 changed files with 884 additions and 76 deletions
+246 -38
View File
@@ -1,7 +1,13 @@
#include "pairingUiController.h"
#include <QRegularExpression>
#include <QTimer>
#include <QUuid>
#if defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#endif
#include "core/controllers/gatewayController.h"
#include "core/models/serverConfig.h"
#include "core/models/api/apiV2ServerConfig.h"
@@ -14,8 +20,33 @@ namespace
{
constexpr auto kGenerateQrPath = "%1api/v1/generate_qr";
constexpr auto kScanQrPath = "%1api/v1/scan_qr";
constexpr int kPairingRetryMaxAttempts = 3;
bool isPairingRetriableError(ErrorCode code)
{
switch (code) {
case ErrorCode::ApiPairingRateLimitedError:
case ErrorCode::ApiPairingServiceUnavailableError:
case ErrorCode::ApiConfigDownloadError:
return true;
default:
return false;
}
}
int pairingRetryDelayMs(int zeroBasedAttempt)
{
constexpr int baseMs = 500;
return baseMs * (1 << zeroBasedAttempt);
}
} // namespace
#if defined(Q_OS_ANDROID)
namespace {
PairingUiController *g_pairingUiForAndroidQr = nullptr;
}
#endif
PairingUiController::PairingUiController(PairingController *pairingController, ServersController *serversController,
SubscriptionController *subscriptionController,
SecureAppSettingsRepository *appSettingsRepository, QObject *parent)
@@ -25,8 +56,63 @@ PairingUiController::PairingUiController(PairingController *pairingController, S
m_subscriptionController(subscriptionController),
m_appSettingsRepository(appSettingsRepository)
{
#if defined(Q_OS_ANDROID)
g_pairingUiForAndroidQr = this;
#endif
}
PairingUiController::~PairingUiController()
{
#if defined(Q_OS_ANDROID)
if (g_pairingUiForAndroidQr == this) {
g_pairingUiForAndroidQr = nullptr;
}
#endif
}
void PairingUiController::setTvPairingUiPhase(int phase)
{
if (m_tvPairingUiPhase == phase) {
return;
}
m_tvPairingUiPhase = phase;
emit tvPairingUiPhaseChanged();
}
void PairingUiController::openPairingQrScanner()
{
#if defined(Q_OS_ANDROID)
AndroidController::instance()->startQrReaderActivity();
#endif
}
bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
{
const QString t = raw.trimmed();
if (t.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) {
return false;
}
static const QRegularExpression re(QStringLiteral(
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"));
const QRegularExpressionMatch m = re.match(t);
if (!m.hasMatch()) {
return false;
}
const QString uuid = m.captured(0);
emit pairingUuidFromScan(uuid);
return true;
}
#if defined(Q_OS_ANDROID)
bool PairingUiController::tryConsumeAndroidQrScan(const QString &code)
{
if (!g_pairingUiForAndroidQr) {
return false;
}
return g_pairingUiForAndroidQr->applyScannedTextAsPairingUuid(code);
}
#endif
QVariantList PairingUiController::tvQrCodes() const
{
QVariantList list;
@@ -93,6 +179,18 @@ void PairingUiController::resetTvQrDisplay()
emit tvSessionUuidChanged();
}
QString PairingUiController::tvFailureMessage(ErrorCode code) const
{
switch (code) {
case ErrorCode::ApiConfigTimeoutError:
return tr("QR session expired. Tap Start to show a new QR code.");
case ErrorCode::ApiConfigAlreadyAdded:
return tr("This configuration is already on the device.");
default:
return tr("Pairing failed");
}
}
void PairingUiController::startTvQrSession()
{
if (!m_pairingController || !m_appSettingsRepository) {
@@ -108,6 +206,9 @@ void PairingUiController::startTvQrSession()
m_tvWatcher.clear();
}
++m_tvSessionGeneration;
const quint64 generation = m_tvSessionGeneration;
m_tvSessionUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QByteArray qrPayload = m_tvSessionUuid.toUtf8();
m_tvQrCodes = qrCodeUtils::generateQrCodeImageSeries(qrPayload);
@@ -118,6 +219,19 @@ void PairingUiController::startTvQrSession()
emit tvStatusMessageChanged();
setTvBusy(true);
setTvPairingUiPhase(1);
dispatchTvGenerateQrAttempt(generation, 0);
}
void PairingUiController::dispatchTvGenerateQrAttempt(quint64 generation, int retryAttempt)
{
if (!m_pairingController || !m_appSettingsRepository) {
return;
}
if (generation != m_tvSessionGeneration) {
return;
}
const bool isTestPurchase = false;
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
@@ -126,12 +240,15 @@ void PairingUiController::startTvQrSession()
m_appSettingsRepository->isStrictKillSwitchEnabled());
const QJsonObject payload = m_pairingController->buildGenerateQrPayload(m_tvSessionUuid);
const QFuture<QPair<ErrorCode, QByteArray>> future = gatewayController->postAsync(QString::fromLatin1(kGenerateQrPath), payload);
QNetworkReply *replyRaw = nullptr;
const QFuture<QPair<ErrorCode, QByteArray>> future =
gatewayController->postAsync(QString::fromLatin1(kGenerateQrPath), payload, &replyRaw);
m_tvNetworkReply = replyRaw;
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>(this);
m_tvWatcher = watcher;
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, this,
[this, gatewayController, watcher]() {
[this, gatewayController, watcher, generation, retryAttempt]() {
Q_UNUSED(gatewayController);
const auto result = watcher->result();
watcher->deleteLater();
@@ -139,33 +256,67 @@ void PairingUiController::startTvQrSession()
m_tvWatcher.clear();
}
setTvBusy(false);
if (result.first != ErrorCode::NoError) {
m_tvStatusMessage = tr("Pairing failed");
emit tvStatusMessageChanged();
emit errorOccurred(result.first);
if (generation != m_tvSessionGeneration) {
return;
}
m_tvNetworkReply.clear();
PairingController::QrPairingConfigPayload out;
const ErrorCode parseErr = PairingController::parseGenerateQrResponseBody(result.second, out);
if (parseErr != ErrorCode::NoError) {
m_tvStatusMessage = tr("Pairing failed");
ErrorCode logicalErr = result.first;
if (logicalErr == ErrorCode::NoError) {
logicalErr = PairingController::parseGenerateQrResponseBody(result.second, out);
}
if (logicalErr == ErrorCode::NoError) {
ServerConfig importedConfig;
const ErrorCode impErr = m_subscriptionController->importServerFromQrPairingResponse(
out.config, out.serviceInfo, out.supportedProtocols, importedConfig);
Q_UNUSED(importedConfig);
setTvBusy(false);
if (impErr != ErrorCode::NoError) {
setTvPairingUiPhase(2);
m_tvStatusMessage = tvFailureMessage(impErr);
emit tvStatusMessageChanged();
emit errorOccurred(impErr);
resetTvQrDisplay();
return;
}
resetTvQrDisplay();
m_tvStatusMessage = tr("Configuration received");
emit tvStatusMessageChanged();
emit errorOccurred(parseErr);
emit tvPairingConfigReceived();
setTvPairingUiPhase(0);
return;
}
m_tvStatusMessage = tr("Configuration received");
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
const int delayMs = pairingRetryDelayMs(retryAttempt);
QTimer::singleShot(delayMs, this, [this, generation, retryAttempt]() {
if (generation != m_tvSessionGeneration) {
return;
}
dispatchTvGenerateQrAttempt(generation, retryAttempt + 1);
});
return;
}
setTvBusy(false);
setTvPairingUiPhase(logicalErr == ErrorCode::ApiConfigTimeoutError ? 3 : 2);
m_tvStatusMessage = tvFailureMessage(logicalErr);
emit tvStatusMessageChanged();
emit tvPairingConfigReceived();
emit errorOccurred(logicalErr);
});
watcher->setFuture(future);
}
void PairingUiController::cancelTvQrSession()
{
++m_tvSessionGeneration;
if (m_tvNetworkReply) {
m_tvNetworkReply->abort();
}
m_tvNetworkReply.clear();
if (m_tvWatcher) {
m_tvWatcher->disconnect();
m_tvWatcher->deleteLater();
@@ -175,6 +326,26 @@ void PairingUiController::cancelTvQrSession()
m_tvStatusMessage.clear();
emit tvStatusMessageChanged();
resetTvQrDisplay();
setTvPairingUiPhase(0);
}
void PairingUiController::cancelAllPairingActivity()
{
++m_phoneSessionGeneration;
if (m_phoneNetworkReply) {
m_phoneNetworkReply->abort();
}
m_phoneNetworkReply.clear();
if (m_phoneWatcher) {
m_phoneWatcher->disconnect();
m_phoneWatcher->deleteLater();
m_phoneWatcher.clear();
}
setPhoneBusy(false);
m_phoneStatusMessage.clear();
emit phoneStatusMessageChanged();
cancelTvQrSession();
}
void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIndex)
@@ -224,29 +395,50 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn
return;
}
const ErrorCode fieldErr = PairingController::validatePairingScanFields(trimmedUuid, vpnKey, apiKey);
if (fieldErr != ErrorCode::NoError) {
emit errorOccurred(fieldErr);
return;
}
++m_phoneSessionGeneration;
const quint64 phoneGeneration = m_phoneSessionGeneration;
m_phoneStatusMessage = tr("Sending…");
emit phoneStatusMessageChanged();
setPhoneBusy(true);
runPhonePairingRequest(trimmedUuid, apiV2->apiConfig.isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey);
dispatchPhoneScanQrAttempt(trimmedUuid, apiV2->apiConfig.isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey,
phoneGeneration, 0);
}
void PairingUiController::runPhonePairingRequest(const QString &qrUuid, const bool isTestPurchase, const QString &vpnKey,
const QJsonObject &serviceInfo, const QJsonArray &supportedProtocols,
const QString &apiKey)
void PairingUiController::dispatchPhoneScanQrAttempt(const QString &qrUuid, const bool isTestPurchase, const QString &vpnKey,
const QJsonObject &serviceInfo, const QJsonArray &supportedProtocols,
const QString &apiKey, quint64 generation, int retryAttempt)
{
if (!m_pairingController || !m_appSettingsRepository) {
return;
}
if (generation != m_phoneSessionGeneration) {
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
const QJsonObject payload = m_pairingController->buildScanQrPayload(qrUuid, vpnKey, serviceInfo, supportedProtocols, apiKey);
const QFuture<QPair<ErrorCode, QByteArray>> future = gatewayController->postAsync(QString::fromLatin1(kScanQrPath), payload);
QNetworkReply *replyRaw = nullptr;
const QFuture<QPair<ErrorCode, QByteArray>> future =
gatewayController->postAsync(QString::fromLatin1(kScanQrPath), payload, &replyRaw);
m_phoneNetworkReply = replyRaw;
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>(this);
m_phoneWatcher = watcher;
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, this,
[this, gatewayController, watcher]() {
[this, gatewayController, watcher, generation, retryAttempt, qrUuid, isTestPurchase, vpnKey, serviceInfo,
supportedProtocols, apiKey]() {
Q_UNUSED(gatewayController);
const auto result = watcher->result();
watcher->deleteLater();
@@ -254,26 +446,42 @@ void PairingUiController::runPhonePairingRequest(const QString &qrUuid, const bo
m_phoneWatcher.clear();
}
if (generation != m_phoneSessionGeneration) {
return;
}
m_phoneNetworkReply.clear();
ErrorCode logicalErr = result.first;
if (logicalErr == ErrorCode::NoError) {
logicalErr = PairingController::parseScanQrResponseBody(result.second);
}
if (logicalErr == ErrorCode::NoError) {
setPhoneBusy(false);
m_phoneStatusMessage = tr("Sent successfully");
emit phoneStatusMessageChanged();
emit phonePairingSucceeded();
return;
}
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
const int delayMs = pairingRetryDelayMs(retryAttempt);
QTimer::singleShot(delayMs, this, [this, qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols,
apiKey, generation, retryAttempt]() {
if (generation != m_phoneSessionGeneration) {
return;
}
dispatchPhoneScanQrAttempt(qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey,
generation, retryAttempt + 1);
});
return;
}
setPhoneBusy(false);
if (result.first != ErrorCode::NoError) {
m_phoneStatusMessage = tr("Send failed");
emit phoneStatusMessageChanged();
emit errorOccurred(result.first);
return;
}
const ErrorCode parseErr = PairingController::parseScanQrResponseBody(result.second);
if (parseErr != ErrorCode::NoError) {
m_phoneStatusMessage = tr("Send failed");
emit phoneStatusMessageChanged();
emit errorOccurred(parseErr);
return;
}
m_phoneStatusMessage = tr("Sent successfully");
m_phoneStatusMessage = tr("Send failed");
emit phoneStatusMessageChanged();
emit phonePairingSucceeded();
emit errorOccurred(logicalErr);
});
watcher->setFuture(future);
}