From f1481b1b1fd0b2bcc2854a76014924a17af5f87c Mon Sep 17 00:00:00 2001 From: vkamn Date: Wed, 29 Oct 2025 23:24:24 +0800 Subject: [PATCH 1/4] feat: add async post in gateway controller (#1963) --- client/core/controllers/gatewayController.cpp | 121 ++++++++++++++---- client/core/controllers/gatewayController.h | 14 ++ .../ui/controllers/api/apiNewsController.cpp | 37 +++--- client/ui/controllers/api/apiNewsController.h | 1 + client/ui/qml/Pages2/PageSettings.qml | 16 ++- 5 files changed, 143 insertions(+), 46 deletions(-) diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index f626189e1..bba0ef9d9 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "QBlockCipher.h" @@ -50,24 +51,25 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool { } -ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload) { + EncryptedRequestData encRequestData; + encRequestData.errorCode = ErrorCode::NoError; + #ifdef Q_OS_IOS IosController::Instance()->requestInetAccess(); QThread::msleep(10); #endif - QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); - - request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl)); + encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs); + encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); + encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl)); // bypass killSwitch exceptions for API-gateway #ifdef AMNEZIA_DESKTOP if (m_isStrictKillSwitchEnabled) { - QString host = QUrl(request.url()).host(); + QString host = QUrl(encRequestData.request.url()).host(); QString ip = NetworkUtilities::getIPAddress(host); if (!ip.isEmpty()) { IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip }); @@ -76,14 +78,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api #endif QSimpleCrypto::QBlockCipher blockCipher; - QByteArray key = blockCipher.generatePrivateSalt(32); - QByteArray iv = blockCipher.generatePrivateSalt(32); - QByteArray salt = blockCipher.generatePrivateSalt(8); + encRequestData.key = blockCipher.generatePrivateSalt(32); + encRequestData.iv = blockCipher.generatePrivateSalt(32); + encRequestData.salt = blockCipher.generatePrivateSalt(8); QJsonObject keyPayload; - keyPayload[configKey::aesKey] = QString(key.toBase64()); - keyPayload[configKey::aesIv] = QString(iv.toBase64()); - keyPayload[configKey::aesSalt] = QString(salt.toBase64()); + keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64()); + keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64()); + keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64()); QByteArray encryptedKeyPayload; QByteArray encryptedApiPayload; @@ -98,24 +100,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } catch (...) { Utils::logException(); qCritical() << "error loading public key from environment variables"; - return ErrorCode::ApiMissingAgwPublicKey; + encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey; + return encRequestData; } encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); EVP_PKEY_free(publicKey); - encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); - } catch (...) { // todo change error handling in QSimpleCrypto? + encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, "", encRequestData.salt); + } catch (...) { Utils::logException(); qCritical() << "error when encrypting the request body"; - return ErrorCode::ApiConfigDecryptionError; + encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError; + return encRequestData; } QJsonObject requestBody; requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); + encRequestData.requestBody = QJsonDocument(requestBody).toJson(); + return encRequestData; +} + +ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +{ + EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); + if (encRequestData.errorCode != ErrorCode::NoError) { + return encRequestData.errorCode; + } + + QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -131,19 +146,19 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api reply->deleteLater(); - if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) { - auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { - request.setUrl(url); - return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); + if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { + auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) { + encRequestData.request.setUrl(url); + return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); }; - auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &key, &iv, - &salt, this](QNetworkReply *reply, const QList &nestedSslErrors) { + auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData, + this](QNetworkReply *reply, const QList &nestedSslErrors) { encryptedResponseBody = reply->readAll(); replyErrorString = reply->errorString(); replyError = reply->error(); httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) { + if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { sslErrors = nestedSslErrors; return false; } @@ -161,7 +176,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } try { - responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); + QSimpleCrypto::QBlockCipher blockCipher; + responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); return ErrorCode::NoError; } catch (...) { // todo change error handling in QSimpleCrypto? Utils::logException(); @@ -170,6 +186,57 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api } } +QFuture> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) +{ + auto promise = QSharedPointer>>::create(); + promise->start(); + + EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); + if (encRequestData.errorCode != ErrorCode::NoError) { + promise->addResult(qMakePair(encRequestData.errorCode, QByteArray())); + promise->finish(); + return promise->future(); + } + + QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); + + auto sslErrors = QSharedPointer>::create(); + + connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { + *sslErrors = errors; + }); + + connect(reply, &QNetworkReply::finished, reply, [=]() { + QByteArray encryptedResponseBody = reply->readAll(); + QString replyErrorString = reply->errorString(); + auto replyError = reply->error(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + reply->deleteLater(); + + auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody); + if (errorCode) { + promise->addResult(qMakePair(errorCode, QByteArray())); + promise->finish(); + return; + } + + QSimpleCrypto::QBlockCipher blockCipher; + try { + QByteArray responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); + promise->addResult(qMakePair(ErrorCode::NoError, responseBody)); + promise->finish(); + } catch (...) { + Utils::logException(); + qCritical() << "error when decrypting the request body"; + promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); + promise->finish(); + } + }); + + return promise->future(); +} + QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) { QNetworkRequest request; diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index d4e5061f7..e13da1ed3 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -1,8 +1,10 @@ #ifndef GATEWAYCONTROLLER_H #define GATEWAYCONTROLLER_H +#include #include #include +#include #include "core/defs.h" @@ -19,8 +21,20 @@ public: const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); + QFuture> postAsync(const QString &endpoint, const QJsonObject apiPayload); private: + struct EncryptedRequestData { + QNetworkRequest request; + QByteArray requestBody; + QByteArray key; + QByteArray iv; + QByteArray salt; + amnezia::ErrorCode errorCode; + }; + + EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload); + QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode); bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = ""); diff --git a/client/ui/controllers/api/apiNewsController.cpp b/client/ui/controllers/api/apiNewsController.cpp index 45afacb1f..a6525c043 100644 --- a/client/ui/controllers/api/apiNewsController.cpp +++ b/client/ui/controllers/api/apiNewsController.cpp @@ -32,7 +32,6 @@ void ApiNewsController::fetchNews() } GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled()); - QByteArray responseBody; QJsonObject payload; payload.insert("locale", m_settings->getAppLanguage().name().split("_").first()); @@ -44,22 +43,26 @@ void ApiNewsController::fetchNews() payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType)); } - ErrorCode errorCode = gatewayController.post(QString("%1v1/news"), payload, responseBody); - if (errorCode != ErrorCode::NoError) { - emit errorOccurred(errorCode); - return; - } - - QJsonDocument doc = QJsonDocument::fromJson(responseBody); - QJsonArray newsArray; - if (doc.isArray()) { - newsArray = doc.array(); - } else if (doc.isObject()) { - QJsonObject obj = doc.object(); - if (obj.value("news").isArray()) { - newsArray = obj.value("news").toArray(); + auto future = gatewayController.postAsync(QString("%1v1/news"), payload); + future.then(this, [this](QPair result) { + auto [errorCode, responseBody] = result; + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return; } - } - m_newsModel->updateModel(newsArray); + QJsonDocument doc = QJsonDocument::fromJson(responseBody); + QJsonArray newsArray; + if (doc.isArray()) { + newsArray = doc.array(); + } else if (doc.isObject()) { + QJsonObject obj = doc.object(); + if (obj.value("news").isArray()) { + newsArray = obj.value("news").toArray(); + } + } + + m_newsModel->updateModel(newsArray); + emit fetchNewsFinished(); + }); } diff --git a/client/ui/controllers/api/apiNewsController.h b/client/ui/controllers/api/apiNewsController.h index e830c6829..17e744ae1 100644 --- a/client/ui/controllers/api/apiNewsController.h +++ b/client/ui/controllers/api/apiNewsController.h @@ -23,6 +23,7 @@ public: signals: void errorOccurred(ErrorCode errorCode); + void fetchNewsFinished(); private: QSharedPointer m_newsModel; diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index f6647d321..5a6e6bd4c 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -14,6 +14,19 @@ import "../Config" PageType { id: root + Connections { + target: ApiNewsController + function onFetchNewsFinished() { + PageController.showBusyIndicator(false) + } + + function onErrorOccurred(errorCode) { + PageController.showErrorMessage(errorCode) + PageController.closePage() + PageController.showBusyIndicator(false) + } + } + ListViewType { id: listView @@ -140,9 +153,8 @@ PageType { return; } PageController.showBusyIndicator(true) - ApiNewsController.fetchNews(); + ApiNewsController.fetchNews() PageController.goToPage(PageEnum.PageSettingsNewsNotifications) - PageController.showBusyIndicator(false) } } From 236daf6b3b258317ccaec261ffc25296d0208c4e Mon Sep 17 00:00:00 2001 From: vkamn Date: Mon, 3 Nov 2025 10:26:22 +0800 Subject: [PATCH 2/4] feat: ad label (#1966) * refactor: ad label desing refatroing * feat: add ad label settings processing * chore: fix ru translations * chore: minor fixes --- client/core/api/apiDefs.h | 8 + client/translations/amneziavpn_ru_RU.ts | 97 +++++++----- .../controllers/api/apiConfigsController.cpp | 16 +- .../controllers/api/apiSettingsController.cpp | 1 + client/ui/models/api/apiAccountInfoModel.cpp | 16 +- client/ui/models/api/apiAccountInfoModel.h | 2 + client/ui/models/servers_model.cpp | 45 +++++- client/ui/models/servers_model.h | 12 ++ client/ui/qml/Components/AdLabel.qml | 149 +++++++++++++----- client/ui/qml/Modules/Style/AmneziaStyle.qml | 2 + client/ui/qml/Pages2/PageHome.qml | 20 +-- 11 files changed, 262 insertions(+), 106 deletions(-) diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h index 151746a45..8e5428558 100644 --- a/client/core/api/apiDefs.h +++ b/client/core/api/apiDefs.h @@ -47,12 +47,14 @@ namespace apiDefs constexpr QLatin1String serverCountryName("server_country_name"); constexpr QLatin1String osVersion("os_version"); + constexpr QLatin1String appLanguage("app_language"); constexpr QLatin1String availableCountries("available_countries"); constexpr QLatin1String activeDeviceCount("active_device_count"); constexpr QLatin1String maxDeviceCount("max_device_count"); constexpr QLatin1String subscriptionEndDate("subscription_end_date"); constexpr QLatin1String issuedConfigs("issued_configs"); + constexpr QLatin1String subscriptionDescription("subscription_description"); constexpr QLatin1String supportInfo("support_info"); constexpr QLatin1String email("email"); @@ -68,6 +70,12 @@ namespace apiDefs constexpr QLatin1String transactionId("transaction_id"); constexpr QLatin1String userCountryCode("user_country_code"); + + constexpr QLatin1String serviceInfo("service_info"); + constexpr QLatin1String isAdVisible("is_ad_visible"); + constexpr QLatin1String adHeader("ad_header"); + constexpr QLatin1String adDescription("ad_description"); + constexpr QLatin1String adEndpoint("ad_endpoint"); } const int requestTimeoutMsecs = 12 * 1000; // 12 secs diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 5c9869306..185c054ea 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -4,9 +4,8 @@ AdLabel - Amnezia Premium - for access to all websites and online resources - Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам + Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам @@ -61,7 +60,7 @@ ApiAccountInfoModel - + Active Активна @@ -71,35 +70,33 @@ Не активна - + %1 out of %2 %1 из %2 - Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to 200 Mbps - Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с + Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с - Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and more. YouTube is not included in the free plan. - Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включён в бесплатный тариф. + Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включён в бесплатный тариф. ApiConfigsController - + %1 installed successfully. %1 успешно установлен. - + API config reloaded Конфигурация API перезагружена - + Successfully changed the country of connection to %1 Страна подключения изменена на %1 @@ -627,27 +624,32 @@ Thank you for staying with us! Продолжить - + Logging enabled Логирование включено - + + Dev gateway enabled + + + + Split tunneling enabled Раздельное туннелирование включено - + Split tunneling disabled Раздельное туннелирование выключено - + VPN protocol VPN-протокол - + Servers Серверы @@ -1579,32 +1581,37 @@ Thank you for staying with us! Настройки - + Servers Серверы - + Connection Соединение - + Application Приложение - + + News & Notifications + Новости и Уведомления + + + Backup Резервное копирование - + About AmneziaVPN Об AmneziaVPN - + Dev console Dev console @@ -2763,6 +2770,14 @@ Thank you for staying with us! Очистить логи + + PageSettingsNewsNotifications + + + News & Notifications + Новости и Уведомления + + PageSettingsServerData @@ -3012,13 +3027,13 @@ Thank you for staying with us! - + Continue Продолжить - + Cancel Отменить @@ -3059,8 +3074,8 @@ Thank you for staying with us! - - + + Sites files (*.json) Файлы сайтов (*.json) @@ -3070,33 +3085,33 @@ Thank you for staying with us! Очистить список сайтов - + Clear site list? Очистить список сайтов? - + All sites will be removed from list. Все сайты будут удалены из списка. - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим @@ -3521,32 +3536,32 @@ Thank you for staying with us! PageSetupWizardViewConfig - + New connection Новое соединение - + Collapse content Свернуть - + Show content Показать - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных. - + Connect Подключиться @@ -4950,12 +4965,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир SettingsController - + All settings have been reset to default values Все настройки сброшены до значений по умолчанию - + Backup file is corrupted Файл резервной копии поврежден diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 026224c5d..41756462c 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -29,7 +29,6 @@ namespace constexpr char uuid[] = "installation_uuid"; constexpr char osVersion[] = "os_version"; constexpr char appVersion[] = "app_version"; - constexpr char appLanguage[] = "app_language"; constexpr char userCountryCode[] = "user_country_code"; constexpr char serverCountryCode[] = "server_country_code"; @@ -65,6 +64,7 @@ namespace { QString osVersion; QString appVersion; + QString appLanguage; QString installationUuid; @@ -84,6 +84,9 @@ namespace if (!appVersion.isEmpty()) { obj[configKey::appVersion] = appVersion; } + if (!appLanguage.isEmpty()) { + obj[apiDefs::key::appLanguage] = appLanguage; + } if (!installationUuid.isEmpty()) { obj[configKey::uuid] = installationUuid; } @@ -223,6 +226,9 @@ namespace if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { apiConfig.insert(apiDefs::key::supportedProtocols, QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::supportedProtocols).toArray()); + + apiConfig.insert(apiDefs::key::serviceInfo, + QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::serviceInfo).toObject()); } serverConfig[configKey::apiConfig] = apiConfig; @@ -285,6 +291,7 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), + m_settings->getAppLanguage().name().split("_").first(), m_settings->getInstallationUuid(true), apiConfigObject.value(configKey::userCountryCode).toString(), serverCountryCode, @@ -325,6 +332,7 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode) GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), + m_settings->getAppLanguage().name().split("_").first(), m_settings->getInstallationUuid(true), apiConfigObject.value(configKey::userCountryCode).toString(), serverCountryCode, @@ -375,7 +383,7 @@ bool ApiConfigsController::fillAvailableServices() { QJsonObject apiPayload; apiPayload[configKey::osVersion] = QSysInfo::productType(); - apiPayload[configKey::appLanguage] = m_settings->getAppLanguage().name().split("_").first(); + apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first(); QByteArray responseBody; ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody); @@ -399,6 +407,7 @@ bool ApiConfigsController::importServiceFromGateway() { GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), + m_settings->getAppLanguage().name().split("_").first(), m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(), "", @@ -457,6 +466,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), + m_settings->getAppLanguage().name().split("_").first(), m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(), newCountryCode, @@ -577,6 +587,7 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent) GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), + m_settings->getAppLanguage().name().split("_").first(), m_settings->getInstallationUuid(true), apiConfigObject.value(configKey::userCountryCode).toString(), apiConfigObject.value(configKey::serverCountryCode).toString(), @@ -616,6 +627,7 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q GatewayRequestData gatewayRequestData { QSysInfo::productType(), QString(APP_VERSION), + m_settings->getAppLanguage().name().split("_").first(), uuid, apiConfigObject.value(configKey::userCountryCode).toString(), serverCountryCode, diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index c4a75a5b8..58ba9af9f 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -62,6 +62,7 @@ bool ApiSettingsController::getAccountInfo(bool reload) apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString(); apiPayload[configKey::authData] = authData; apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION); + apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first(); QByteArray responseBody; diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index bdd7d68d4..0f3a8a4ed 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -31,7 +31,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const return tr("Active"); } - return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("

Inactive") : tr("Active"); + return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("

Inactive") + : tr("Active"); } case EndDateRole: { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { @@ -47,16 +48,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount); } case ServiceDescriptionRole: { - if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { - return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online " - "resources. " - "Speeds up to 200 Mbps"); - } else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { - return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and " - "more. YouTube is not included in the free plan."); - } else { - return ""; - } + return m_accountInfoData.subscriptionDescription; } case IsComponentVisibleRole: { return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2 @@ -101,6 +93,8 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons accountInfoData.configType = apiUtils::getConfigType(serverConfig); + accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString(); + for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) { accountInfoData.supportedProtocols.push_back(protocol.toString()); } diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index f02039677..836bc8926 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -54,6 +54,8 @@ private: apiDefs::ConfigType configType; QStringList supportedProtocols; + + QString subscriptionDescription; }; AccountInfoData m_accountInfoData; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 5d8910d00..1a2bb150a 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -158,6 +158,18 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const QString primaryDns = server.value(config_key::dns1).toString(); return primaryDns == protocols::dns::amneziaDnsIp; } + case IsAdVisibleRole:{ + return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::isAdVisible).toBool(false); + } + case AdHeaderRole: { + return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adHeader).toString(); + } + case AdDescriptionRole: { + return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adDescription).toString(); + } + case AdEndpointRole: { + return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString(); + } } return QVariant(); @@ -403,6 +415,12 @@ QHash ServersModel::roleNames() const roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable"; roles[ApiAvailableCountriesRole] = "apiAvailableCountries"; roles[ApiServerCountryCodeRole] = "apiServerCountryCode"; + + roles[IsAdVisibleRole] = "isAdVisible"; + roles[AdHeaderRole] = "adHeader"; + roles[AdDescriptionRole] = "adDescription"; + roles[AdEndpointRole] = "adEndpoint"; + return roles; } @@ -784,22 +802,22 @@ void ServersModel::recomputeGatewayStacks() const bool wasEmpty = m_gatewayStacks.isEmpty(); GatewayStacks computed; bool hasNewTags = false; - + for (int i = 0; i < m_servers.count(); ++i) { if (data(i, IsServerFromGatewayApiRole).toBool()) { const QJsonObject server = m_servers.at(i).toObject(); const QJsonObject apiConfig = server.value(configKey::apiConfig).toObject(); - + const QString userCountryCode = apiConfig.value(configKey::userCountryCode).toString(); const QString serviceType = apiConfig.value(configKey::serviceType).toString(); - + if (!userCountryCode.isEmpty()) { if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) { hasNewTags = true; } computed.userCountryCodes.insert(userCountryCode); } - + if (!serviceType.isEmpty()) { if (!m_gatewayStacks.serviceTypes.contains(serviceType)) { hasNewTags = true; @@ -808,12 +826,12 @@ void ServersModel::recomputeGatewayStacks() } } } - + m_gatewayStacks = std::move(computed); if (hasNewTags) { emit gatewayStacksExpanded(); } - + if (wasEmpty != m_gatewayStacks.isEmpty()) { emit hasServersFromGatewayApiChanged(); } @@ -885,3 +903,18 @@ bool ServersModel::processedServerIsPremium() const { return apiUtils::isPremiumServer(getServerConfig(m_processedServerIndex)); } + +bool ServersModel::isAdVisible() +{ + return data(m_defaultServerIndex, IsAdVisibleRole).toBool(); +} + +QString ServersModel::adHeader() +{ + return data(m_defaultServerIndex, AdHeaderRole).toString(); +} + +QString ServersModel::adDescription() +{ + return data(m_defaultServerIndex, AdDescriptionRole).toString(); +} diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 973b54189..8a0406bbe 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -47,6 +47,10 @@ public: IsCountrySelectionAvailableRole, ApiAvailableCountriesRole, ApiServerCountryCodeRole, + IsAdVisibleRole, + AdHeaderRole, + AdDescriptionRole, + AdEndpointRole, HasAmneziaDns }; @@ -79,6 +83,10 @@ public: Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged) + Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIndexChanged) + Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIndexChanged) + Q_PROPERTY(QString adDescription READ adDescription NOTIFY defaultServerIndexChanged) + bool processedServerIsPremium() const; public slots: @@ -144,6 +152,10 @@ public slots: bool isApiKeyExpired(const int serverIndex); void removeApiConfig(const int serverIndex); + bool isAdVisible(); + QString adHeader(); + QString adDescription(); + protected: QHash roleNames() const override; diff --git a/client/ui/qml/Components/AdLabel.qml b/client/ui/qml/Components/AdLabel.qml index 3ef0fc699..fedaa0e56 100644 --- a/client/ui/qml/Components/AdLabel.qml +++ b/client/ui/qml/Components/AdLabel.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Shapes -import Qt5Compat.GraphicalEffects import Style 1.0 @@ -13,61 +12,139 @@ import "../Controls2/TextTypes" Rectangle { id: root - property real contentHeight: ad.implicitHeight + ad.anchors.topMargin + ad.anchors.bottomMargin + property real contentHeight: content.implicitHeight + content.anchors.topMargin + content.anchors.bottomMargin + property bool isFocusable: true + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: AmneziaStyle.color.translucentSlateGray } + GradientStop { position: 1.0; color: AmneziaStyle.color.translucentOnyxBlack } + } border.width: 1 - border.color: AmneziaStyle.color.goldenApricot - color: AmneziaStyle.color.transparent + border.color: AmneziaStyle.color.onyxBlack radius: 13 - visible: false - // visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi - // && ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible + visible: ServersModel.isAdVisible - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } - onClicked: function() { - Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("premium")) - } + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + Keys.onEnterPressed: { + Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint")) + } + + Keys.onReturnPressed: { + Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint")) } RowLayout { - id: ad + id: content anchors.fill: parent - anchors.margins: 16 + anchors.leftMargin: 16 + anchors.rightMargin: 12 + anchors.topMargin: 12 + anchors.bottomMargin: 12 + spacing: 20 - Image { - source: "qrc:/images/controls/amnezia.svg" - sourceSize: Qt.size(36, 36) + ColumnLayout { + Layout.fillWidth: true + spacing: 4 - layer { - effect: ColorOverlay { - color: AmneziaStyle.color.paleGray - } + CaptionTextType { + Layout.fillWidth: true + text: ServersModel.adHeader + color: AmneziaStyle.color.paleGray + font.pixelSize: 14 + font.weight: 700 + + textFormat: Text.RichText + } + + CaptionTextType { + Layout.fillWidth: true + text: ServersModel.adDescription + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + lineHeight: 18 + lineHeightMode: Text.FixedHeight + font.pixelSize: 14 + + visible: text !== "" } } - CaptionTextType { - Layout.fillWidth: true - Layout.rightMargin: 10 - Layout.leftMargin: 10 + Item { + implicitWidth: 40 + implicitHeight: 40 + Layout.alignment: Qt.AlignVCenter - text: qsTr("Amnezia Premium - for access to all websites and online resources") - color: AmneziaStyle.color.pearlGray + Rectangle { + id: chevronBackground + anchors.fill: parent + radius: 12 + color: AmneziaStyle.color.transparent + border.width: root.activeFocus ? 1 : 0 + border.color: AmneziaStyle.color.paleGray - lineHeight: 18 - font.pixelSize: 15 - } + Behavior on color { + PropertyAnimation { duration: 200 } + } - ImageButtonType { - image: "qrc:/images/controls/close.svg" - imageColor: AmneziaStyle.color.paleGray + Behavior on border.width { + PropertyAnimation { duration: 200 } + } + } - onClicked: function() { - SettingsController.disableHomeAdLabel() + Image { + anchors.centerIn: parent + source: "qrc:/images/controls/chevron-right.svg" + sourceSize: Qt.size(24, 24) } } } + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onEntered: { + chevronBackground.color = AmneziaStyle.color.slateGray + } + + onExited: { + chevronBackground.color = AmneziaStyle.color.transparent + } + + onPressedChanged: { + chevronBackground.color = pressed ? AmneziaStyle.color.charcoalGray : containsMouse ? AmneziaStyle.color.slateGray : AmneziaStyle.color.transparent + } + + onClicked: function() { + root.forceActiveFocus() + Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint")) + } + } } diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index 4e2e80f06..20b563360 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -28,5 +28,7 @@ QtObject { readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) readonly property color pearlGray: '#EAEAEC' readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26) + readonly property color translucentSlateGray: Qt.rgba(85/255, 86/255, 92/255, 0.13) + readonly property color translucentOnyxBlack: Qt.rgba(28/255, 29/255, 33/255, 0.13) } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index d2289ab03..09a13e67d 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -71,16 +71,6 @@ PageType { anchors.topMargin: 12 anchors.bottomMargin: 16 - AdLabel { - id: adLabel - - Layout.fillWidth: true - Layout.preferredHeight: adLabel.contentHeight - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 22 - } - BasicButtonType { id: loggingButton objectName: "loggingButton" @@ -189,6 +179,16 @@ PageType { parent: root } } + + AdLabel { + id: adLabel + + Layout.fillWidth: true + Layout.preferredHeight: adLabel.contentHeight + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 22 + } } } From e0e126eda87cc45a6df74b5e5f65d72ba6ef9f70 Mon Sep 17 00:00:00 2001 From: vkamn Date: Mon, 3 Nov 2025 10:26:33 +0800 Subject: [PATCH 3/4] chore: bump version (#1969) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fa641b691..6d8a40f7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -set(AMNEZIAVPN_VERSION 4.8.11.0) +set(AMNEZIAVPN_VERSION 4.8.11.1) project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} DESCRIPTION "AmneziaVPN" @@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2095) +set(APP_ANDROID_VERSION_CODE 2096) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From aaf0e070dcfa541b73496552f31ff7e343d1ce82 Mon Sep 17 00:00:00 2001 From: MrMirDan <58086007+MrMirDan@users.noreply.github.com> Date: Mon, 3 Nov 2025 04:27:01 +0200 Subject: [PATCH 4/4] fix: hide description (#1959) --- client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index dc3f63c6a..8388d388d 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -71,7 +71,7 @@ PageType { text: countryName descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : "" - hideDescription: isWorkerExpired ? true : false + hideDescription: isWorkerExpired ? false : true descriptionColor: AmneziaStyle.color.vibrantRed leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"