diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
index 2f8405061..d7c0c28b7 100644
--- a/client/CMakeLists.txt
+++ b/client/CMakeLists.txt
@@ -69,6 +69,7 @@ endif()
qt6_add_resources(QRC ${QRC}
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
+ ${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
${CMAKE_CURRENT_LIST_DIR}/ui/qml/qml.qrc
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
)
diff --git a/client/client_scripts/clientScripts.qrc b/client/client_scripts/clientScripts.qrc
new file mode 100644
index 000000000..1c0ba9909
--- /dev/null
+++ b/client/client_scripts/clientScripts.qrc
@@ -0,0 +1,6 @@
+
+
+ linux_installer.sh
+ mac_installer.sh
+
+
diff --git a/client/client_scripts/linux_installer.sh b/client/client_scripts/linux_installer.sh
new file mode 100644
index 000000000..f2232bc4c
--- /dev/null
+++ b/client/client_scripts/linux_installer.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+EXTRACT_DIR="$1"
+INSTALLER_PATH="$2"
+
+# Create and clean extract directory
+rm -rf "$EXTRACT_DIR"
+mkdir -p "$EXTRACT_DIR"
+
+# Extract TAR archive
+tar -xf "$INSTALLER_PATH" -C "$EXTRACT_DIR"
+if [ $? -ne 0 ]; then
+ echo 'Failed to extract TAR archive'
+ exit 1
+fi
+
+# Find and run installer
+INSTALLER=$(find "$EXTRACT_DIR" -type f -executable)
+if [ -z "$INSTALLER" ]; then
+ echo 'Installer not found'
+ exit 1
+fi
+
+"$INSTALLER"
+EXIT_CODE=$?
+
+# Cleanup
+rm -rf "$EXTRACT_DIR"
+exit $EXIT_CODE
\ No newline at end of file
diff --git a/client/client_scripts/mac_installer.sh b/client/client_scripts/mac_installer.sh
new file mode 100644
index 000000000..41fd504d0
--- /dev/null
+++ b/client/client_scripts/mac_installer.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+EXTRACT_DIR="$1"
+INSTALLER_PATH="$2"
+
+set -e
+
+echo "[AmneziaVPN] Installer package: $INSTALLER_PATH"
+
+if [ ! -f "$INSTALLER_PATH" ]; then
+ echo "[AmneziaVPN] ERROR: Installer package not found: $INSTALLER_PATH"
+ exit 1
+fi
+
+PKG_PATH="$INSTALLER_PATH"
+echo "[AmneziaVPN] Using PKG: $PKG_PATH"
+
+# Optional: basic signature/gatekeeper checks (non-fatal)
+if command -v pkgutil >/dev/null 2>&1; then
+ pkgutil --check-signature "$PKG_PATH" || true
+fi
+if command -v spctl >/dev/null 2>&1; then
+ spctl -a -vvv -t install "$PKG_PATH" || true
+fi
+
+# Run installer with admin privileges via AppleScript (prompts for password)
+echo "[AmneziaVPN] Running installer..."
+OSA_CMD='do shell script "/usr/sbin/installer -pkg '"$PKG_PATH"' -target /" with administrator privileges'
+osascript -e "$OSA_CMD"
+
+STATUS=$?
+if [ $STATUS -ne 0 ]; then
+ echo "[AmneziaVPN] ERROR: installer exited with status $STATUS"
+ exit $STATUS
+fi
+
+echo "[AmneziaVPN] Cleaning up..."
+rm -f "$INSTALLER_PATH" || true
+rm -rf "$EXTRACT_DIR" 2>/dev/null || true
+
+echo "[AmneziaVPN] Installation completed successfully"
+exit 0
diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake
index f348ec8cd..497757757 100644
--- a/client/cmake/sources.cmake
+++ b/client/cmake/sources.cmake
@@ -45,6 +45,7 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
+ ${CLIENT_ROOT_DIR}/core/controllers/updateController.h
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.h
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
@@ -119,6 +120,7 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.cpp
+ ${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.cpp
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.cpp
diff --git a/client/core/controllers/api/subscriptionController.cpp b/client/core/controllers/api/subscriptionController.cpp
index 5f6b5e1c5..ab0840a97 100644
--- a/client/core/controllers/api/subscriptionController.cpp
+++ b/client/core/controllers/api/subscriptionController.cpp
@@ -346,13 +346,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload[apiDefs::key::transactionId] = transactionId;
- GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(),
- m_appSettingsRepository->isDevGatewayEnv(),
- apiDefs::requestTimeoutMsecs,
- m_appSettingsRepository->isStrictKillSwitchEnabled());
-
QByteArray responseBody;
- ErrorCode errorCode = gatewayController.post(QString("%1v1/subscriptions"), apiPayload, responseBody);
+ ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -365,6 +360,9 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
return ErrorCode::ApiPurchaseError;
}
+ QString normalizedKey = key;
+ normalizedKey.replace(QStringLiteral("vpn://"), QString());
+
// Check if server with this VPN key already exists
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
ServerConfig existingServerConfig = m_serversRepository->server(i);
@@ -376,7 +374,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
const ApiV2ServerConfig* apiV2 = existingServerConfig.as();
existingVpnKey = apiV2 ? apiV2->vpnKey() : QString();
}
- if (existingVpnKey == key) {
+ existingVpnKey.replace(QStringLiteral("vpn://"), QString());
+ if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
if (duplicateServerIndex) {
*duplicateServerIndex = i;
}
@@ -385,9 +384,6 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
}
}
- QString normalizedKey = key;
- normalizedKey.replace(QStringLiteral("vpn://"), QString());
-
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(configString);
if (!configUncompressed.isEmpty()) {
@@ -437,6 +433,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
if (!apiV2) {
return ErrorCode::InternalError;
}
+ const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString serviceProtocol = apiV2->serviceProtocol();
ProtocolData protocolData = generateProtocolData(serviceProtocol);
@@ -459,7 +456,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
}
QByteArray responseBody;
- ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
+ ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
ServerConfig expiredServerConfig = serverConfigModel;
@@ -508,7 +505,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(int serverIndex, cons
return ErrorCode::NoError;
}
-ErrorCode SubscriptionController::deactivateDevice(int serverIndex, bool isRemoveEvent)
+ErrorCode SubscriptionController::deactivateDevice(int serverIndex)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
@@ -538,8 +535,9 @@ ErrorCode SubscriptionController::deactivateDevice(int serverIndex, bool isRemov
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
+ const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QByteArray responseBody;
- ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody);
+ ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
return errorCode;
}
@@ -581,8 +579,9 @@ ErrorCode SubscriptionController::deactivateExternalDevice(int serverIndex, cons
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
+ const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QByteArray responseBody;
- ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody);
+ ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
return errorCode;
}
@@ -609,6 +608,7 @@ ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QStr
if (!apiV2) {
return ErrorCode::InternalError;
}
+ const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString protocol = configKey::awg;
ProtocolData protocolData = generateProtocolData(protocol);
@@ -627,7 +627,7 @@ ErrorCode SubscriptionController::exportNativeConfig(int serverIndex, const QStr
appendProtocolDataToApiPayload(protocol, protocolData, apiPayload);
QByteArray responseBody;
- ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody);
+ ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -650,6 +650,7 @@ ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QStr
if (!apiV2) {
return ErrorCode::InternalError;
}
+ const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString protocol = configKey::awg;
QJsonObject authDataJson = apiV2->authData.toJson();
@@ -666,7 +667,7 @@ ErrorCode SubscriptionController::revokeNativeConfig(int serverIndex, const QStr
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
QByteArray responseBody;
- ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody);
+ ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
return errorCode;
}
@@ -760,7 +761,7 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(int serverIndex, QString &
ErrorCode SubscriptionController::validateAndUpdateConfig(int serverIndex, bool hasInstalledContainers)
{
ServerConfig serverConfigModel = m_serversRepository->server(serverIndex);
-
+
apiDefs::ConfigSource configSource;
if (serverConfigModel.isApiV1()) {
configSource = apiDefs::ConfigSource::Telegram;
@@ -774,11 +775,11 @@ ErrorCode SubscriptionController::validateAndUpdateConfig(int serverIndex, bool
removeApiConfig(serverIndex);
return updateServiceFromTelegram(serverIndex);
} else if (configSource == apiDefs::ConfigSource::AmneziaGateway && !hasInstalledContainers) {
- return updateServiceFromGateway(serverIndex, "", false);
+ return updateServiceFromGateway(serverIndex, "", true);
} else if (configSource && isApiKeyExpired(serverIndex)) {
qDebug() << "attempt to update api config by expires_at event";
if (configSource == apiDefs::ConfigSource::AmneziaGateway) {
- return updateServiceFromGateway(serverIndex, "", false);
+ return updateServiceFromGateway(serverIndex, "", true);
} else {
removeApiConfig(serverIndex);
return updateServiceFromTelegram(serverIndex);
@@ -1076,6 +1077,7 @@ QFuture> SubscriptionController::getRenewalLink(int se
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
+ apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig);
auto gatewayController = QSharedPointer::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
@@ -1095,11 +1097,7 @@ QFuture> SubscriptionController::getRenewalLink(int se
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
const QString url = responseJson.value("renewal_url").toString();
- if (url.isEmpty()) {
- promise->addResult(qMakePair(ErrorCode::InternalError, QString()));
- } else {
- promise->addResult(qMakePair(ErrorCode::NoError, url));
- }
+ promise->addResult(qMakePair(ErrorCode::NoError, url));
promise->finish();
});
watcher->setFuture(postFuture);
diff --git a/client/core/controllers/api/subscriptionController.h b/client/core/controllers/api/subscriptionController.h
index 85f4d47a9..172708544 100644
--- a/client/core/controllers/api/subscriptionController.h
+++ b/client/core/controllers/api/subscriptionController.h
@@ -73,7 +73,7 @@ public:
ErrorCode updateServiceFromGateway(int serverIndex, const QString &newCountryCode, bool isConnectEvent);
- ErrorCode deactivateDevice(int serverIndex, bool isRemoveEvent);
+ ErrorCode deactivateDevice(int serverIndex);
ErrorCode deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode);
diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp
index 4e95d5c70..227850b6d 100644
--- a/client/core/controllers/coreController.cpp
+++ b/client/core/controllers/coreController.cpp
@@ -146,6 +146,7 @@ void CoreController::initCoreControllers()
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
m_newsController = new NewsController(m_appSettingsRepository, m_serversController);
+ m_updateController = new UpdateController(m_appSettingsRepository, this);
m_installController = new InstallController(m_serversRepository, m_appSettingsRepository, this);
m_exportController = new ExportController(m_serversRepository, m_appSettingsRepository, this);
@@ -212,6 +213,9 @@ void CoreController::initControllers()
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
+
+ m_updateUiController = new UpdateUiController(m_updateController, this);
+ setQmlContextProperty("UpdateController", m_updateUiController);
}
void CoreController::initAndroidController()
diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h
index 1fb178f0f..0d77c5167 100644
--- a/client/core/controllers/coreController.h
+++ b/client/core/controllers/coreController.h
@@ -26,6 +26,7 @@
#include "ui/controllers/ipSplitTunnelingUiController.h"
#include "ui/controllers/systemController.h"
#include "ui/controllers/languageUiController.h"
+#include "ui/controllers/updateUiController.h"
#include "ui/controllers/api/servicesCatalogUiController.h"
#include "core/controllers/serversController.h"
@@ -39,6 +40,7 @@
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/settingsController.h"
#include "core/controllers/connectionController.h"
+#include "core/controllers/updateController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
@@ -159,6 +161,7 @@ private:
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
AllowedDnsUiController* m_allowedDnsUiController;
LanguageUiController* m_languageUiController;
+ UpdateUiController* m_updateUiController;
SubscriptionUiController* m_subscriptionUiController;
ApiNewsUiController* m_apiNewsUiController;
@@ -173,6 +176,7 @@ private:
ServicesCatalogController* m_servicesCatalogController;
SubscriptionController* m_subscriptionController;
NewsController* m_newsController;
+ UpdateController* m_updateController;
InstallController* m_installController;
ExportController* m_exportController;
ConnectionController* m_connectionController;
diff --git a/client/core/controllers/coreSignalHandlers.cpp b/client/core/controllers/coreSignalHandlers.cpp
index f68eb8f91..449a8af1d 100644
--- a/client/core/controllers/coreSignalHandlers.cpp
+++ b/client/core/controllers/coreSignalHandlers.cpp
@@ -20,6 +20,7 @@
#include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/importUiController.h"
#include "ui/controllers/api/subscriptionUiController.h"
+#include "ui/controllers/updateUiController.h"
#include "ui/models/serversModel.h"
#include "core/controllers/serversController.h"
#include "core/controllers/ipSplitTunnelingController.h"
@@ -83,6 +84,7 @@ void CoreSignalHandlers::initAllHandlers()
initIosImportHandler();
initIosSettingsHandler();
initNotificationHandler();
+ initUpdateFoundHandler();
}
void CoreSignalHandlers::initErrorMessagesHandler()
@@ -410,3 +412,19 @@ void CoreSignalHandlers::initNotificationHandler()
#endif
}
+void CoreSignalHandlers::initUpdateFoundHandler()
+{
+#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
+ connect(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished, m_coreController->m_updateUiController,
+ &UpdateUiController::checkForUpdates);
+
+ connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
+ const QString version = m_coreController->m_updateUiController->getVersion();
+ const QString updateId = version.isEmpty() ? QStringLiteral("update") : QStringLiteral("update-%1").arg(version);
+ m_coreController->m_newsModel->setUpdateNotification(
+ updateId, m_coreController->m_updateUiController->getHeaderText(), m_coreController->m_updateUiController->getChangelogText());
+ emit m_coreController->m_pageController->showChangelogDrawer();
+ });
+#endif
+}
+
diff --git a/client/core/controllers/coreSignalHandlers.h b/client/core/controllers/coreSignalHandlers.h
index 33e567ce8..51f1d2f1d 100644
--- a/client/core/controllers/coreSignalHandlers.h
+++ b/client/core/controllers/coreSignalHandlers.h
@@ -40,6 +40,7 @@ private:
void initIosImportHandler();
void initIosSettingsHandler();
void initNotificationHandler();
+ void initUpdateFoundHandler();
CoreController* m_coreController;
};
diff --git a/client/core/controllers/updateController.cpp b/client/core/controllers/updateController.cpp
new file mode 100644
index 000000000..24009705e
--- /dev/null
+++ b/client/core/controllers/updateController.cpp
@@ -0,0 +1,391 @@
+#include "updateController.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "amneziaApplication.h"
+#include "logger.h"
+#include "version.h"
+#include "core/controllers/gatewayController.h"
+#include "core/utils/constants/apiKeys.h"
+#include "core/utils/errorStrings.h"
+#include "core/utils/selfhosted/scriptsRegistry.h"
+
+namespace
+{
+ Logger logger("UpdateController");
+
+#if defined(Q_OS_WINDOWS)
+ const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_x64.exe");
+ const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
+#elif defined(Q_OS_MACOS)
+ const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos.pkg");
+ const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
+#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
+ const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.tar");
+ const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar";
+#endif
+}
+
+UpdateController::UpdateController(SecureAppSettingsRepository* appSettingsRepository, QObject *parent)
+ : QObject(parent), m_appSettingsRepository(appSettingsRepository)
+{
+}
+
+QString UpdateController::getRawChangelogText() const
+{
+ return m_changelogText;
+}
+
+QString UpdateController::getReleaseDate() const
+{
+ return m_releaseDate;
+}
+
+QString UpdateController::getVersion() const
+{
+ return m_version;
+}
+
+void UpdateController::checkForUpdates()
+{
+ if (m_updateCheckRunning || !m_appSettingsRepository) {
+ return;
+ }
+ m_updateCheckRunning = true;
+
+ fetchGatewayUrl();
+}
+
+void UpdateController::finishUpdateCheck()
+{
+ m_updateCheckRunning = false;
+}
+
+void UpdateController::doGetAsync(const QString &endpoint, std::function onDone)
+{
+ QString fullUrl = m_baseUrl + endpoint;
+
+ QNetworkRequest req;
+ req.setTransferTimeout(7000);
+ req.setUrl(QUrl(fullUrl));
+
+ QNetworkReply *reply = amnApp->networkManager()->get(req);
+ setupNetworkErrorHandling(reply, endpoint);
+
+ QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, endpoint, onDone]() {
+ const bool ok = (reply->error() == QNetworkReply::NoError);
+ QByteArray data;
+ if (ok) {
+ data = reply->readAll();
+ } else {
+ handleNetworkError(reply, endpoint);
+ }
+ reply->deleteLater();
+ onDone(ok, data);
+ });
+}
+
+void UpdateController::fetchGatewayUrl()
+{
+ auto gatewayController = QSharedPointer::create(m_appSettingsRepository->getGatewayEndpoint(),
+ m_appSettingsRepository->isDevGatewayEnv(),
+ 7000,
+ m_appSettingsRepository->isStrictKillSwitchEnabled());
+
+ QJsonObject apiPayload;
+ apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
+ apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
+ apiPayload[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
+
+ // Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
+ QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
+ gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
+ .then(this, [this](QPair result) {
+ auto [err, gatewayResponse] = result;
+ if (err != ErrorCode::NoError) {
+ logger.error() << errorString(err);
+ finishUpdateCheck();
+ return;
+ }
+
+ QJsonObject gatewayData = QJsonDocument::fromJson(gatewayResponse).object();
+
+ QString baseUrl = gatewayData.value("url").toString();
+ if (baseUrl.endsWith('/')) {
+ baseUrl.chop(1);
+ }
+ m_baseUrl = baseUrl;
+
+ fetchVersionInfo();
+ });
+ });
+}
+
+void UpdateController::fetchVersionInfo()
+{
+ doGetAsync("/VERSION", [this](bool ok, QByteArray data) {
+ if (!ok) {
+ finishUpdateCheck();
+ return;
+ }
+ m_version = QString::fromUtf8(data).trimmed();
+
+ if (!isNewVersionAvailable()) {
+ finishUpdateCheck();
+ return;
+ }
+ fetchChangelog();
+ });
+}
+
+void UpdateController::fetchChangelog()
+{
+ doGetAsync("/CHANGELOG", [this](bool ok, QByteArray data) {
+ if (!ok) {
+ m_changelogText.clear();
+ } else {
+ m_changelogText = QString::fromUtf8(data);
+ }
+ fetchReleaseDate();
+ });
+}
+
+void UpdateController::fetchReleaseDate()
+{
+ doGetAsync("/RELEASE_DATE", [this](bool ok, QByteArray data) {
+ if (ok) {
+ m_releaseDate = QString::fromUtf8(data).trimmed();
+ } else {
+ m_releaseDate = QString();
+ }
+
+ m_downloadUrl = composeDownloadUrl();
+ emit updateFound();
+ finishUpdateCheck();
+ });
+}
+
+bool UpdateController::isNewVersionAvailable() const
+{
+ auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION));
+ auto newVersion = QVersionNumber::fromString(m_version);
+ return newVersion > currentVersion;
+}
+
+void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation)
+{
+ QObject::connect(reply, &QNetworkReply::errorOccurred, [reply, operation](QNetworkReply::NetworkError error) {
+ logger.error() << QString("Network error occurred while fetching %1: %2 %3")
+ .arg(operation, reply->errorString(), QString::number(error));
+ });
+
+ QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList &errors) {
+ QStringList errorStrings;
+ for (const QSslError &err : errors) {
+ errorStrings << err.errorString();
+ }
+ logger.error() << QString("SSL errors while fetching %1: %2").arg(operation, errorStrings.join("; "));
+ });
+}
+
+void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
+{
+ if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
+ || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
+ logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
+ } else {
+ QString err = reply->errorString();
+ logger.error() << "Network error code:" << QString::number(static_cast(reply->error()));
+ logger.error() << "Error message:" << err;
+ logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
+ }
+}
+
+QString UpdateController::composeDownloadUrl() const
+{
+#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
+ const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
+ return m_baseUrl + "/" + fileName;
+#else
+ return QString();
+#endif
+}
+
+void UpdateController::runInstaller()
+{
+#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
+ if (m_downloadUrl.isEmpty()) {
+ logger.error() << "Download URL is empty";
+ return;
+ }
+
+ QNetworkRequest request;
+ request.setTransferTimeout(30000);
+ request.setUrl(m_downloadUrl);
+
+ QNetworkReply *reply = amnApp->networkManager()->get(request);
+
+ QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
+ if (reply->error() == QNetworkReply::NoError) {
+ QFile file(kInstallerLocalPath);
+ if (!file.open(QIODevice::WriteOnly)) {
+ logger.error() << "Failed to open installer file for writing:" << kInstallerLocalPath << "Error:" << file.errorString();
+ reply->deleteLater();
+ return;
+ }
+
+ if (file.write(reply->readAll()) == -1) {
+ logger.error() << "Failed to write installer data to file:" << kInstallerLocalPath << "Error:" << file.errorString();
+ file.close();
+ reply->deleteLater();
+ return;
+ }
+
+ file.close();
+
+ #if defined(Q_OS_WINDOWS)
+ runWindowsInstaller(kInstallerLocalPath);
+ #elif defined(Q_OS_MACOS)
+ runMacInstaller(kInstallerLocalPath);
+ #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
+ runLinuxInstaller(kInstallerLocalPath);
+ #endif
+ } else {
+ if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
+ || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
+ logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
+ } else {
+ QString err = reply->errorString();
+ logger.error() << QString::fromUtf8(reply->readAll());
+ logger.error() << "Network error code:" << QString::number(static_cast(reply->error()));
+ logger.error() << "Error message:" << err;
+ logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
+ }
+ }
+ reply->deleteLater();
+ });
+#endif
+}
+
+#if defined(Q_OS_WINDOWS)
+int UpdateController::runWindowsInstaller(const QString &installerPath)
+{
+ qint64 pid;
+ bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid);
+
+ if (success) {
+ logger.info() << "Installation process started with PID:" << pid;
+ } else {
+ logger.error() << "Failed to start installation process";
+ return -1;
+ }
+
+ return 0;
+}
+#endif
+
+#if defined(Q_OS_MACOS)
+int UpdateController::runMacInstaller(const QString &installerPath)
+{
+ // Create temporary directory for extraction
+ QTemporaryDir extractDir;
+ extractDir.setAutoRemove(false);
+ if (!extractDir.isValid()) {
+ logger.error() << "Failed to create temporary directory";
+ return -1;
+ }
+ logger.info() << "Temporary directory created:" << extractDir.path();
+
+ // Create script file in the temporary directory
+ QString scriptPath = extractDir.path() + "/mac_installer.sh";
+ QFile scriptFile(scriptPath);
+ if (!scriptFile.open(QIODevice::WriteOnly)) {
+ logger.error() << "Failed to create script file";
+ return -1;
+ }
+
+ // Get script content from registry
+ QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::mac_installer);
+ if (scriptContent.isEmpty()) {
+ logger.error() << "macOS installer script content is empty";
+ scriptFile.close();
+ return -1;
+ }
+
+ scriptFile.write(scriptContent.toUtf8());
+ scriptFile.close();
+ logger.info() << "Script file created:" << scriptPath;
+
+ // Make script executable
+ QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser);
+
+ // Start detached process
+ qint64 pid;
+ bool success =
+ QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid);
+
+ if (success) {
+ logger.info() << "Installation process started with PID:" << pid;
+ } else {
+ logger.error() << "Failed to start installation process";
+ return -1;
+ }
+
+ return 0;
+}
+#endif
+
+#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
+int UpdateController::runLinuxInstaller(const QString &installerPath)
+{
+ // Create temporary directory for extraction
+ QTemporaryDir extractDir;
+ extractDir.setAutoRemove(false);
+ if (!extractDir.isValid()) {
+ logger.error() << "Failed to create temporary directory";
+ return -1;
+ }
+ logger.info() << "Temporary directory created:" << extractDir.path();
+
+ // Create script file in the temporary directory
+ QString scriptPath = extractDir.path() + "/installer.sh";
+ QFile scriptFile(scriptPath);
+ if (!scriptFile.open(QIODevice::WriteOnly)) {
+ logger.error() << "Failed to create script file";
+ return -1;
+ }
+
+ // Get script content from registry
+ QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer);
+ scriptFile.write(scriptContent.toUtf8());
+ scriptFile.close();
+ logger.info() << "Script file created:" << scriptPath;
+
+ // Make script executable
+ QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser);
+
+ // Start detached process
+ qint64 pid;
+ bool success =
+ QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid);
+
+ if (success) {
+ logger.info() << "Installation process started with PID:" << pid;
+ } else {
+ logger.error() << "Failed to start installation process";
+ return -1;
+ }
+
+ return 0;
+}
+#endif
+
+
diff --git a/client/core/controllers/updateController.h b/client/core/controllers/updateController.h
new file mode 100644
index 000000000..97ad361df
--- /dev/null
+++ b/client/core/controllers/updateController.h
@@ -0,0 +1,57 @@
+#ifndef UPDATECONTROLLER_H
+#define UPDATECONTROLLER_H
+
+#include
+#include
+#include
+
+#include "core/repositories/secureAppSettingsRepository.h"
+
+class UpdateController : public QObject
+{
+ Q_OBJECT
+public:
+ explicit UpdateController(SecureAppSettingsRepository* appSettingsRepository, QObject *parent = nullptr);
+
+ QString getRawChangelogText() const;
+ QString getReleaseDate() const;
+ QString getVersion() const;
+
+public slots:
+ void checkForUpdates();
+ void runInstaller();
+
+signals:
+ void updateFound();
+
+private:
+ void finishUpdateCheck();
+ void fetchGatewayUrl();
+ void fetchVersionInfo();
+ void fetchChangelog();
+ void fetchReleaseDate();
+ void doGetAsync(const QString &endpoint, std::function onDone);
+ bool isNewVersionAvailable() const;
+ void setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation);
+ void handleNetworkError(QNetworkReply* reply, const QString& operation);
+ QString composeDownloadUrl() const;
+
+ SecureAppSettingsRepository* m_appSettingsRepository;
+
+ QString m_baseUrl;
+ QString m_changelogText;
+ QString m_version;
+ QString m_releaseDate;
+ QString m_downloadUrl;
+ bool m_updateCheckRunning = false;
+
+#if defined(Q_OS_WINDOWS)
+ int runWindowsInstaller(const QString &installerPath);
+#elif defined(Q_OS_MACOS)
+ int runMacInstaller(const QString &installerPath);
+#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
+ int runLinuxInstaller(const QString &installerPath);
+#endif
+};
+
+#endif // UPDATECONTROLLER_H
diff --git a/client/core/models/api/apiConfig.h b/client/core/models/api/apiConfig.h
index a2cff58d0..05ecda478 100644
--- a/client/core/models/api/apiConfig.h
+++ b/client/core/models/api/apiConfig.h
@@ -58,7 +58,7 @@ struct ApiConfig
QString stackType;
QString cliVersion;
- bool isTestPurchase;
+ bool isTestPurchase = false;
bool isInAppPurchase = false;
bool subscriptionExpiredByServer = false;
diff --git a/client/core/protocols/xrayProtocol.cpp b/client/core/protocols/xrayProtocol.cpp
index d3d483334..893b8367e 100755
--- a/client/core/protocols/xrayProtocol.cpp
+++ b/client/core/protocols/xrayProtocol.cpp
@@ -1,16 +1,16 @@
#include "xrayProtocol.h"
+#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/ipcClient.h"
#include "core/utils/networkUtilities.h"
-#include "core/protocols/protocolUtils.h"
#include "core/utils/serialization/serialization.h"
#include "ipc.h"
#include
+#include
#include
#include
-#include
#include
#include
#include
@@ -43,7 +43,17 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) :
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolUtils::key_proto_config_data(Proto::SSXray)).toObject();
}
- m_xrayConfig = xrayConfiguration;
+
+ if (xrayConfiguration.isEmpty()) {
+ qWarning() << "Xray config wrapper is empty";
+ m_xrayConfig = {};
+ }
+
+ m_xrayConfig = QJsonDocument::fromJson(xrayConfiguration.value(amnezia::configKey::config).toString().toUtf8()).object();
+ if (m_xrayConfig.isEmpty()) {
+ qWarning() << "Xray config string is not a valid JSON object";
+ m_xrayConfig = {};
+ }
}
XrayProtocol::~XrayProtocol()
@@ -65,9 +75,9 @@ ErrorCode XrayProtocol::start()
qCritical() << "EnsureInboundAuth failed:" << e.what();
return ErrorCode::InternalError;
}
- m_socksUser = creds.username;
+ m_socksUser = creds.username;
m_socksPassword = creds.password;
- m_socksPort = creds.port;
+ m_socksPort = creds.port;
const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact);
if (xrayConfigStr.isEmpty()) {
@@ -75,16 +85,16 @@ ErrorCode XrayProtocol::start()
return ErrorCode::XrayExecutableCrashed;
}
- return IpcClient::withInterface([&](QSharedPointer iface) {
- auto xrayStart = iface->xrayStart(xrayConfigStr);
- if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
- qCritical() << "Failed to start xray";
- return ErrorCode::XrayExecutableCrashed;
- }
- return startTun2Socks();
- }, [] () {
- return ErrorCode::AmneziaServiceConnectionFailed;
- });
+ return IpcClient::withInterface(
+ [&](QSharedPointer iface) {
+ auto xrayStart = iface->xrayStart(xrayConfigStr);
+ if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
+ qCritical() << "Failed to start xray";
+ return ErrorCode::XrayExecutableCrashed;
+ }
+ return startTun2Socks();
+ },
+ []() { return ErrorCode::AmneziaServiceConnectionFailed; });
}
void XrayProtocol::stop()
@@ -143,129 +153,135 @@ ErrorCode XrayProtocol::startTun2Socks()
return ErrorCode::AmneziaServiceConnectionFailed;
}
- const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3")
- .arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
+ const QString proxyUrl = QString("socks5://%1:%2@127.0.0.1:%3").arg(m_socksUser, m_socksPassword, QString::number(m_socksPort));
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
- m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl});
+ m_tun2socksProcess->setArguments({ "-device", QString("tun://%1").arg(tunName), "-proxy", proxyUrl });
- connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() {
- auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
- if (!readAllStandardOutput.waitForFinished()) {
- qWarning() << "Failed to read output from tun2socks";
- return;
- }
+ connect(
+ m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this,
+ [this]() {
+ auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
+ if (!readAllStandardOutput.waitForFinished()) {
+ qWarning() << "Failed to read output from tun2socks";
+ return;
+ }
- const QString line = readAllStandardOutput.returnValue();
+ const QString line = readAllStandardOutput.returnValue();
- if (!line.contains("[TCP]") && !line.contains("[UDP]"))
- qDebug() << "[tun2socks]:" << line;
-
- if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
- disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
+ if (!line.contains("[TCP]") && !line.contains("[UDP]"))
+ qDebug() << "[tun2socks]:" << line;
- if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
+ if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) {
+ disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
+
+ if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
+ stop();
+ setLastError(res);
+ } else {
+ setConnectionState(Vpn::ConnectionState::Connected);
+ }
+ }
+ },
+ Qt::QueuedConnection);
+
+ connect(
+ m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
+ [this](int exitCode, QProcess::ExitStatus exitStatus) {
+ if (exitStatus == QProcess::ExitStatus::CrashExit) {
+ qCritical() << "Tun2socks process crashed!";
+ } else {
+ qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
+ }
stop();
- setLastError(res);
- } else {
- setConnectionState(Vpn::ConnectionState::Connected);
- }
- }
- }, Qt::QueuedConnection);
-
- connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
- if (exitStatus == QProcess::ExitStatus::CrashExit) {
- qCritical() << "Tun2socks process crashed!";
- } else {
- qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
- }
- stop();
- setLastError(ErrorCode::Tun2SockExecutableCrashed);
- }, Qt::QueuedConnection);
+ setLastError(ErrorCode::Tun2SockExecutableCrashed);
+ },
+ Qt::QueuedConnection);
m_tun2socksProcess->start();
return ErrorCode::NoError;
}
-ErrorCode XrayProtocol::setupRouting() {
- return IpcClient::withInterface([this](QSharedPointer iface) -> ErrorCode {
+ErrorCode XrayProtocol::setupRouting()
+{
+ return IpcClient::withInterface(
+ [this](QSharedPointer iface) -> ErrorCode {
#ifdef Q_OS_WIN
- const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
+ const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
#endif
- auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
- if (!createTun.waitForFinished() || !createTun.returnValue()) {
- qCritical() << "Failed to assign IP address for TUN";
- return ErrorCode::InternalError;
- }
-
- auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
- if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
- qCritical() << "Failed to set DNS resolvers for TUN";
- return ErrorCode::InternalError;
- }
-
-#ifdef Q_OS_WIN
- int vpnAdapterIndex = -1;
- QList netInterfaces = QNetworkInterface::allInterfaces();
- for (auto& netInterface : netInterfaces) {
- for (auto& address : netInterface.addressEntries()) {
- if (m_vpnLocalAddress == address.ip().toString())
- vpnAdapterIndex = netInterface.index();
- }
- }
-#else
- static const int vpnAdapterIndex = 0;
-#endif
- const bool killSwitchEnabled = QVariant(m_rawConfig.value(configKey::killSwitchOption).toString()).toBool();
- if (killSwitchEnabled) {
- if (vpnAdapterIndex != -1) {
- QJsonObject config = m_rawConfig;
- config.insert("vpnServer", m_remoteAddress);
-
- auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
- if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
- qCritical() << "Failed to enable killswitch";
+ auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
+ if (!createTun.waitForFinished() || !createTun.returnValue()) {
+ qCritical() << "Failed to assign IP address for TUN";
return ErrorCode::InternalError;
}
- } else
- qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
- }
- if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
- static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
-
- auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
- if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
- qCritical() << "Failed to set routes for TUN";
- return ErrorCode::InternalError;
- }
- }
-
- auto StopRoutingIpv6 = iface->StopRoutingIpv6();
- if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
- qCritical() << "Failed to disable IPv6 routing";
- return ErrorCode::InternalError;
- }
+ auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
+ if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
+ qCritical() << "Failed to set DNS resolvers for TUN";
+ return ErrorCode::InternalError;
+ }
#ifdef Q_OS_WIN
- if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
- QJsonObject config = m_rawConfig;
- config.insert("inetAdapterIndex", inetAdapterIndex);
- config.insert("vpnAdapterIndex", vpnAdapterIndex);
- config.insert("vpnGateway", m_vpnGateway);
- config.insert("vpnServer", m_remoteAddress);
-
- auto enablePeerTraffic = iface->enablePeerTraffic(config);
- if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
- qCritical() << "Failed to enable peer traffic";
- return ErrorCode::InternalError;
- }
- } else
- qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
+ int vpnAdapterIndex = -1;
+ QList netInterfaces = QNetworkInterface::allInterfaces();
+ for (auto &netInterface : netInterfaces) {
+ for (auto &address : netInterface.addressEntries()) {
+ if (m_vpnLocalAddress == address.ip().toString())
+ vpnAdapterIndex = netInterface.index();
+ }
+ }
+#else
+ static const int vpnAdapterIndex = 0;
#endif
- return ErrorCode::NoError;
- },
- [] () {
- return ErrorCode::AmneziaServiceConnectionFailed;
- });
+ const bool killSwitchEnabled = QVariant(m_rawConfig.value(configKey::killSwitchOption).toString()).toBool();
+ if (killSwitchEnabled) {
+ if (vpnAdapterIndex != -1) {
+ QJsonObject config = m_rawConfig;
+ config.insert("vpnServer", m_remoteAddress);
+
+ auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
+ if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
+ qCritical() << "Failed to enable killswitch";
+ return ErrorCode::InternalError;
+ }
+ } else
+ qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
+ }
+
+ if (m_routeMode == amnezia::RouteMode::VpnAllSites) {
+ static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
+ "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
+
+ auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
+ if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
+ qCritical() << "Failed to set routes for TUN";
+ return ErrorCode::InternalError;
+ }
+ }
+
+ auto StopRoutingIpv6 = iface->StopRoutingIpv6();
+ if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
+ qCritical() << "Failed to disable IPv6 routing";
+ return ErrorCode::InternalError;
+ }
+
+#ifdef Q_OS_WIN
+ if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
+ QJsonObject config = m_rawConfig;
+ config.insert("inetAdapterIndex", inetAdapterIndex);
+ config.insert("vpnAdapterIndex", vpnAdapterIndex);
+ config.insert("vpnGateway", m_vpnGateway);
+ config.insert("vpnServer", m_remoteAddress);
+
+ auto enablePeerTraffic = iface->enablePeerTraffic(config);
+ if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
+ qCritical() << "Failed to enable peer traffic";
+ return ErrorCode::InternalError;
+ }
+ } else
+ qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
+#endif
+ return ErrorCode::NoError;
+ },
+ []() { return ErrorCode::AmneziaServiceConnectionFailed; });
}
diff --git a/client/core/utils/selfhosted/scriptsRegistry.cpp b/client/core/utils/selfhosted/scriptsRegistry.cpp
index 03d329ee4..3ff409499 100644
--- a/client/core/utils/selfhosted/scriptsRegistry.cpp
+++ b/client/core/utils/selfhosted/scriptsRegistry.cpp
@@ -73,6 +73,15 @@ QString amnezia::scriptName(ProtocolScriptType type)
}
}
+QString amnezia::scriptName(ClientScriptType type)
+{
+ switch (type) {
+ case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh");
+ case ClientScriptType::mac_installer: return QLatin1String("mac_installer.sh");
+ default: return QString();
+ }
+}
+
QString amnezia::scriptData(amnezia::SharedScriptType type)
{
QString fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type));
@@ -101,6 +110,22 @@ QString amnezia::scriptData(amnezia::ProtocolScriptType type, DockerContainer co
return data;
}
+QString amnezia::scriptData(ClientScriptType type)
+{
+ QString fileName = QString(":/client_scripts/%1").arg(amnezia::scriptName(type));
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ qDebug() << "Warning: script missing" << fileName;
+ return "";
+ }
+ QByteArray data = file.readAll();
+ if (data.isEmpty()) {
+ qDebug() << "Warning: script is empty" << fileName;
+ }
+ data.replace("\r", "");
+ return data;
+}
+
amnezia::ScriptVars amnezia::genBaseVars(const ServerCredentials &credentials,
DockerContainer container,
const QString &primaryDns,
diff --git a/client/core/utils/selfhosted/scriptsRegistry.h b/client/core/utils/selfhosted/scriptsRegistry.h
index e10862777..26bb2f0e9 100644
--- a/client/core/utils/selfhosted/scriptsRegistry.h
+++ b/client/core/utils/selfhosted/scriptsRegistry.h
@@ -41,14 +41,21 @@ enum ProtocolScriptType {
xray_template
};
+enum ClientScriptType {
+ // Client-side scripts
+ linux_installer,
+ mac_installer
+};
QString scriptFolder(DockerContainer container);
QString scriptName(SharedScriptType type);
QString scriptName(ProtocolScriptType type);
+QString scriptName(ClientScriptType type);
QString scriptData(SharedScriptType type);
QString scriptData(ProtocolScriptType type, DockerContainer container);
+QString scriptData(ClientScriptType type);
ScriptVars genBaseVars(const ServerCredentials &credentials,
DockerContainer container,
diff --git a/client/ui/controllers/api/apiNewsUiController.cpp b/client/ui/controllers/api/apiNewsUiController.cpp
index 899032e33..2b288c6b0 100644
--- a/client/ui/controllers/api/apiNewsUiController.cpp
+++ b/client/ui/controllers/api/apiNewsUiController.cpp
@@ -22,7 +22,7 @@ void ApiNewsUiController::fetchNews(bool showError)
return;
}
- m_newsModel->updateModel(newsArray);
+ m_newsModel->setNewsList(newsArray);
emit fetchNewsFinished();
});
}
diff --git a/client/ui/controllers/api/servicesCatalogUiController.cpp b/client/ui/controllers/api/servicesCatalogUiController.cpp
index 734bdd9ac..bf5fed650 100644
--- a/client/ui/controllers/api/servicesCatalogUiController.cpp
+++ b/client/ui/controllers/api/servicesCatalogUiController.cpp
@@ -23,6 +23,9 @@ bool ServicesCatalogUiController::fillAvailableServices()
}
m_apiServicesModel->updateModel(servicesData);
+ if (m_apiServicesModel->rowCount() > 0) {
+ m_apiServicesModel->setServiceIndex(0);
+ }
return true;
}
diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp
index aa6934ddb..955301163 100644
--- a/client/ui/controllers/api/subscriptionUiController.cpp
+++ b/client/ui/controllers/api/subscriptionUiController.cpp
@@ -390,7 +390,8 @@ bool SubscriptionUiController::updateServiceFromGateway(const int serverIndex, c
if (oldServerConfig.isApiV2()) {
const ApiV2ServerConfig *oldApiV2 = oldServerConfig.as();
if (oldApiV2) {
- wasSubscriptionExpired = oldApiV2->apiConfig.isSubscriptionExpired();
+ wasSubscriptionExpired = oldApiV2->apiConfig.subscriptionExpiredByServer
+ || oldApiV2->apiConfig.isSubscriptionExpired();
}
}
@@ -411,8 +412,9 @@ bool SubscriptionUiController::updateServiceFromGateway(const int serverIndex, c
} else {
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
emit subscriptionExpiredOnServer();
+ } else {
+ emit errorOccurred(errorCode);
}
- emit errorOccurred(errorCode);
return false;
}
}
@@ -435,14 +437,10 @@ bool SubscriptionUiController::updateServiceFromTelegram(const int serverIndex)
}
}
-bool SubscriptionUiController::deactivateDevice(int serverIndex, const bool isRemoveEvent)
+bool SubscriptionUiController::deactivateDevice(int serverIndex)
{
-
- ErrorCode errorCode = m_subscriptionController->deactivateDevice(serverIndex, isRemoveEvent);
+ ErrorCode errorCode = m_subscriptionController->deactivateDevice(serverIndex);
if (errorCode != ErrorCode::NoError) {
- if (errorCode == ErrorCode::ApiSubscriptionExpiredError && isRemoveEvent) {
- return true;
- }
emit errorOccurred(errorCode);
return false;
}
@@ -469,7 +467,11 @@ void SubscriptionUiController::validateConfig()
ErrorCode errorCode = m_subscriptionController->validateAndUpdateConfig(serverIndex, hasInstalledContainers);
if (errorCode != ErrorCode::NoError) {
- emit errorOccurred(errorCode);
+ if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
+ emit subscriptionExpiredOnServer();
+ } else {
+ emit errorOccurred(errorCode);
+ }
emit configValidated(false);
return;
}
@@ -559,6 +561,9 @@ void SubscriptionUiController::getRenewalLink(int serverIndex)
emit errorOccurred(errorCode);
return;
}
+ if (url.isEmpty()) {
+ return;
+ }
emit renewalLinkReceived(url);
});
watcher->setFuture(m_subscriptionController->getRenewalLink(serverIndex));
diff --git a/client/ui/controllers/api/subscriptionUiController.h b/client/ui/controllers/api/subscriptionUiController.h
index 142335991..d4ed056d8 100644
--- a/client/ui/controllers/api/subscriptionUiController.h
+++ b/client/ui/controllers/api/subscriptionUiController.h
@@ -48,7 +48,7 @@ public slots:
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex);
- bool deactivateDevice(int serverIndex, const bool isRemoveEvent);
+ bool deactivateDevice(int serverIndex);
bool deactivateExternalDevice(int serverIndex, const QString &uuid, const QString &serverCountryCode);
void validateConfig();
diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h
index df95fd7af..603e8a8f4 100644
--- a/client/ui/controllers/qml/pageController.h
+++ b/client/ui/controllers/qml/pageController.h
@@ -167,6 +167,7 @@ signals:
void escapePressed();
void closeTopDrawer();
+ void showChangelogDrawer();
void imeHeightChanged(int height);
void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged();
diff --git a/client/ui/controllers/updateUiController.cpp b/client/ui/controllers/updateUiController.cpp
new file mode 100644
index 000000000..1aaf6cd8a
--- /dev/null
+++ b/client/ui/controllers/updateUiController.cpp
@@ -0,0 +1,84 @@
+#include "updateUiController.h"
+
+UpdateUiController::UpdateUiController(UpdateController* updateController, QObject *parent)
+ : QObject(parent), m_updateController(updateController)
+{
+ if (m_updateController) {
+ connect(m_updateController, &UpdateController::updateFound, this, &UpdateUiController::updateFound);
+ }
+}
+
+QString UpdateUiController::getHeaderText() const
+{
+ if (!m_updateController) {
+ return QString();
+ }
+
+ const QString version = m_updateController->getVersion();
+ const QString releaseDate = m_updateController->getReleaseDate();
+ if (releaseDate.trimmed().isEmpty()) {
+ return tr("New version released: %1").arg(version);
+ }
+
+ return tr("New version released: %1 (%2)").arg(version, releaseDate);
+}
+
+QString UpdateUiController::getChangelogText() const
+{
+ if (!m_updateController) {
+ return QString();
+ }
+
+ const QString rawChangelog = m_updateController->getRawChangelogText();
+ if (rawChangelog.isEmpty()) {
+ return tr("Failed to load changelog text");
+ }
+
+ QStringList lines = rawChangelog.split("\n");
+ QStringList filteredChangeLogText;
+ bool add = false;
+ QString osSection;
+
+#ifdef Q_OS_WINDOWS
+ osSection = "### Windows";
+#elif defined(Q_OS_MACOS)
+ osSection = "### macOS";
+#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
+ osSection = "### Linux";
+#endif
+
+ for (const QString &line : lines) {
+ if (line.startsWith("### General")) {
+ add = true;
+ } else if (line.startsWith("### ") && line != osSection) {
+ add = false;
+ } else if (line == osSection) {
+ add = true;
+ }
+
+ if (add) {
+ filteredChangeLogText.append(line);
+ }
+ }
+
+ return filteredChangeLogText.join("\n");
+}
+
+QString UpdateUiController::getVersion() const
+{
+ return m_updateController ? m_updateController->getVersion() : QString();
+}
+
+void UpdateUiController::checkForUpdates()
+{
+ if (m_updateController) {
+ m_updateController->checkForUpdates();
+ }
+}
+
+void UpdateUiController::runInstaller()
+{
+ if (m_updateController) {
+ m_updateController->runInstaller();
+ }
+}
diff --git a/client/ui/controllers/updateUiController.h b/client/ui/controllers/updateUiController.h
new file mode 100644
index 000000000..d877443e3
--- /dev/null
+++ b/client/ui/controllers/updateUiController.h
@@ -0,0 +1,33 @@
+#ifndef UPDATEUICONTROLLER_H
+#define UPDATEUICONTROLLER_H
+
+#include
+
+#include "core/controllers/updateController.h"
+
+class UpdateUiController : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString changelogText READ getChangelogText NOTIFY updateFound)
+ Q_PROPERTY(QString headerText READ getHeaderText NOTIFY updateFound)
+
+public:
+ explicit UpdateUiController(UpdateController* updateController, QObject *parent = nullptr);
+
+ QString getHeaderText() const;
+ QString getChangelogText() const;
+ QString getVersion() const;
+
+public slots:
+ void checkForUpdates();
+ void runInstaller();
+
+signals:
+ void updateFound();
+
+private:
+ UpdateController* m_updateController;
+};
+
+#endif // UPDATEUICONTROLLER_H
diff --git a/client/ui/models/newsModel.cpp b/client/ui/models/newsModel.cpp
index 13bdb4ed0..ba9e209a4 100644
--- a/client/ui/models/newsModel.cpp
+++ b/client/ui/models/newsModel.cpp
@@ -33,8 +33,9 @@ QVariant NewsModel::data(const QModelIndex &index, int role) const
case TitleRole: return item.title;
case ContentRole: return item.content;
case TimestampRole: return item.timestamp.toLocalTime().toString(Qt::ISODate);
- case IsReadRole: return item.read;
+ case IsReadRole: return m_readIds.contains(item.id);
case IsProcessedRole: return index.row() == m_processedIndex;
+ case IsUpdateRole: return item.isUpdate;
default: return QVariant();
}
}
@@ -48,6 +49,7 @@ QHash NewsModel::roleNames() const
roles[TimestampRole] = "timestamp";
roles[IsReadRole] = "read";
roles[IsProcessedRole] = "isProcessed";
+ roles[IsUpdateRole] = "isUpdate";
return roles;
}
@@ -55,13 +57,33 @@ void NewsModel::markAsRead(int index)
{
if (index < 0 || index >= m_items.size())
return;
- if (!m_items[index].read) {
- m_items[index].read = true;
- m_readIds.insert(m_items[index].id);
- saveReadIds();
- QModelIndex idx = createIndex(index, 0);
- emit dataChanged(idx, idx, { IsReadRole });
- emit hasUnreadChanged();
+
+ const QString &itemId = m_items.at(index).id;
+ if (itemId.isEmpty() || m_readIds.contains(itemId))
+ return;
+
+ m_readIds.insert(itemId);
+ saveReadIds();
+
+ QModelIndex idx = createIndex(index, 0);
+ emit dataChanged(idx, idx, { IsReadRole });
+ emit hasUnreadChanged();
+}
+
+void NewsModel::markUpdateAsSkipped()
+{
+ if (!m_updateItem.has_value())
+ return;
+
+ const QString updateId = m_updateItem->id;
+ if (updateId.isEmpty())
+ return;
+
+ for (int i = 0; i < m_items.size(); ++i) {
+ if (m_items.at(i).id == updateId) {
+ markAsRead(i);
+ break;
+ }
}
}
@@ -78,37 +100,65 @@ void NewsModel::setProcessedIndex(int index)
emit processedIndexChanged(index);
}
-void NewsModel::updateModel(const QJsonArray &serverItems)
+void NewsModel::setNewsList(const QJsonArray &serverItems)
{
- QList updatedItems;
+ QVector updatedItems;
+ updatedItems.reserve(serverItems.size());
for (const QJsonValue &value : serverItems) {
if (!value.isObject())
continue;
- QJsonObject object = value.toObject();
-
+ const QJsonObject object = value.toObject();
+
NewsItem item;
item.id = object.value("id").toString();
+ if (item.id.isEmpty())
+ continue;
item.title = object.value("title").toString();
item.content = object.value("content").toString();
item.timestamp = QDateTime::fromString(object.value("timestamp").toString(), Qt::ISODate);
- item.read = m_readIds.contains(object.value("id").toString());
+ item.isUpdate = false;
+
updatedItems.append(item);
}
+ m_apiItems = updatedItems;
+ updateModel();
+}
+
+void NewsModel::setUpdateNotification(const QString &id, const QString &title, const QString &content)
+{
+ if (id.isEmpty())
+ return;
+
+ NewsItem updateItem;
+ updateItem.id = id;
+ updateItem.title = title;
+ updateItem.content = content;
+ updateItem.timestamp = QDateTime::currentDateTimeUtc();
+ updateItem.isUpdate = true;
+
+ m_updateItem = updateItem;
+ updateModel();
+}
+
+void NewsModel::updateModel()
+{
beginResetModel();
- m_items = updatedItems;
+ m_items = m_apiItems;
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
+ if (m_updateItem.has_value()) {
+ m_items.prepend(*m_updateItem);
+ }
endResetModel();
- loadReadIds();
emit hasUnreadChanged();
}
bool NewsModel::hasUnread() const
{
for (const NewsItem &item : m_items) {
- if (!item.read)
+ if (!m_readIds.contains(item.id))
return true;
}
return false;
diff --git a/client/ui/models/newsModel.h b/client/ui/models/newsModel.h
index c2d4e488c..fa9ba28b1 100644
--- a/client/ui/models/newsModel.h
+++ b/client/ui/models/newsModel.h
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
struct NewsItem
{
@@ -14,7 +15,7 @@ struct NewsItem
QString title;
QString content;
QDateTime timestamp;
- bool read;
+ bool isUpdate = false;
};
class NewsModel : public QAbstractListModel
@@ -27,17 +28,20 @@ public:
ContentRole,
TimestampRole,
IsReadRole,
- IsProcessedRole
+ IsProcessedRole,
+ IsUpdateRole
};
explicit NewsModel(class SecureAppSettingsRepository* appSettingsRepository, QObject *parent = nullptr);
Q_INVOKABLE void markAsRead(int index);
+ Q_INVOKABLE void markUpdateAsSkipped();
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
int processedIndex() const;
void setProcessedIndex(int index);
- void updateModel(const QJsonArray &items);
+ void setNewsList(const QJsonArray &items);
+ void setUpdateNotification(const QString &id, const QString &title, const QString &content);
bool hasUnread() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
@@ -50,11 +54,14 @@ signals:
private:
QVector m_items;
+ QVector m_apiItems;
+ std::optional m_updateItem;
int m_processedIndex = -1;
class SecureAppSettingsRepository* m_appSettingsRepository;
QSet m_readIds;
void loadReadIds();
void saveReadIds() const;
+ void updateModel();
};
#endif // NEWSMODEL_H
diff --git a/client/ui/qml/Components/ChangelogDrawer.qml b/client/ui/qml/Components/ChangelogDrawer.qml
new file mode 100644
index 000000000..1bb767be4
--- /dev/null
+++ b/client/ui/qml/Components/ChangelogDrawer.qml
@@ -0,0 +1,103 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import "../Controls2"
+import "../Controls2/TextTypes"
+
+import "../Config"
+
+DrawerType2 {
+ id: root
+
+ anchors.fill: parent
+ expandedHeight: parent.height * 0.9
+
+ expandedStateContent: Item {
+ implicitHeight: root.expandedHeight
+
+ Header2TextType {
+ id: header
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 16
+ anchors.rightMargin: 16
+ anchors.leftMargin: 16
+ anchors.bottomMargin: 16
+
+ text: UpdateController.headerText
+ }
+
+ FlickableType {
+ anchors.top: header.bottom
+ anchors.bottom: updateButton.top
+ contentHeight: changelog.height + 32
+
+ ParagraphTextType {
+ id: changelog
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 16
+ anchors.rightMargin: 16
+ anchors.leftMargin: 16
+ anchors.bottomMargin: 16
+
+ HoverHandler {
+ enabled: parent.hoveredLink
+ cursorShape: Qt.PointingHandCursor
+ }
+
+ onLinkActivated: function(link) {
+ Qt.openUrlExternally(link)
+ }
+
+ text: UpdateController.changelogText
+ textFormat: Text.MarkdownText
+ }
+ }
+
+ BasicButtonType {
+ id: updateButton
+ anchors.bottom: skipButton.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottomMargin: 8
+ anchors.rightMargin: 16
+ anchors.leftMargin: 16
+
+ text: qsTr("Update")
+
+ clickedFunc: function() {
+ PageController.showBusyIndicator(true)
+ UpdateController.runInstaller()
+ PageController.showBusyIndicator(false)
+ root.closeTriggered()
+ }
+ }
+
+ BasicButtonType {
+ id: skipButton
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottomMargin: 16
+ anchors.rightMargin: 16
+ anchors.leftMargin: 16
+
+ defaultColor: "transparent"
+ hoveredColor: Qt.rgba(1, 1, 1, 0.08)
+ pressedColor: Qt.rgba(1, 1, 1, 0.12)
+ disabledColor: "#878B91"
+ textColor: "#D7D8DB"
+ borderWidth: 1
+
+ text: qsTr("Skip")
+
+ clickedFunc: function() {
+ root.closeTriggered()
+ }
+ }
+ }
+}
diff --git a/client/ui/qml/Components/SubscriptionExpiredDrawer.qml b/client/ui/qml/Components/SubscriptionExpiredDrawer.qml
index d9b6b29c8..24fe66087 100644
--- a/client/ui/qml/Components/SubscriptionExpiredDrawer.qml
+++ b/client/ui/qml/Components/SubscriptionExpiredDrawer.qml
@@ -15,7 +15,7 @@ DrawerType2 {
property bool isRenewalAvailable: false
onOpened: {
- isRenewalAvailable = ServersModel.getProcessedServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
+ isRenewalAvailable = ServersModel.getDefaultServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
}
expandedStateContent: ColumnLayout {
@@ -28,7 +28,7 @@ DrawerType2 {
spacing: 0
onImplicitHeightChanged: {
- root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
+ root.expandedHeight = content.implicitHeight + 32 + PageController.safeAreaBottomMargin
}
Item {
@@ -43,7 +43,7 @@ DrawerType2 {
anchors.left: parent.left
anchors.right: parent.right
- text: ServersModel.getProcessedServerData("name") + qsTr(" subscription has expired")
+ text: ServersModel.getDefaultServerData("name") + qsTr(" subscription has expired")
horizontalAlignment: Text.AlignLeft
}
}
@@ -76,7 +76,7 @@ DrawerType2 {
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
- SubscriptionUiController.getRenewalLink(ServersUiController.getProcessedServerIndex())
+ SubscriptionUiController.getRenewalLink(ServersUiController.defaultIndex)
}
}
@@ -96,7 +96,7 @@ DrawerType2 {
clickedFunc: function() {
PageController.showBusyIndicator(true)
- let result = ApiSettingsController.getAccountInfo(false)
+ let result = SubscriptionUiController.getAccountInfo(ServersUiController.defaultIndex, false)
PageController.showBusyIndicator(false)
if (result) {
root.closeTriggered()
diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
index 143352fb3..7eb119244 100644
--- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
+++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
@@ -469,7 +469,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
} else {
PageController.showBusyIndicator(true)
- if (SubscriptionUiController.deactivateDevice(ServersUiController.getProcessedServerIndex(), false)) {
+ if (SubscriptionUiController.deactivateDevice(ServersUiController.getProcessedServerIndex())) {
SubscriptionUiController.getAccountInfo(ServersUiController.getProcessedServerIndex(), true)
}
PageController.showBusyIndicator(false)
diff --git a/client/ui/qml/Pages2/PageSettingsNewsDetail.qml b/client/ui/qml/Pages2/PageSettingsNewsDetail.qml
index c67b4303f..b304ab5a2 100644
--- a/client/ui/qml/Pages2/PageSettingsNewsDetail.qml
+++ b/client/ui/qml/Pages2/PageSettingsNewsDetail.qml
@@ -14,6 +14,7 @@ import SortFilterProxyModel 0.2
PageType {
id: root
property var newsItem
+ property bool isUpdateItem: newsItem && (newsItem.isUpdate !== undefined ? newsItem.isUpdate : false)
SortFilterProxyModel {
id: proxyNews
@@ -54,7 +55,7 @@ PageType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
- headerText: newsItem.title
+ headerText: newsItem ? newsItem.title : ""
}
ParagraphTextType {
@@ -62,9 +63,9 @@ PageType {
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
- text: newsItem.content
+ text: newsItem ? newsItem.content : ""
- textFormat: Text.RichText
+ textFormat: root.isUpdateItem ? Text.MarkdownText : Text.RichText
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
@@ -76,6 +77,47 @@ PageType {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
+
+ BasicButtonType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ visible: root.isUpdateItem
+ text: qsTr("Update")
+
+ clickedFunc: function() {
+ if (!root.isUpdateItem)
+ return
+ PageController.showBusyIndicator(true)
+ UpdateController.runInstaller()
+ PageController.showBusyIndicator(false)
+ PageController.closePage()
+ }
+ }
+
+ BasicButtonType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ Layout.bottomMargin: 16
+ visible: root.isUpdateItem
+ defaultColor: "transparent"
+ hoveredColor: Qt.rgba(1, 1, 1, 0.08)
+ pressedColor: Qt.rgba(1, 1, 1, 0.12)
+ disabledColor: "#878B91"
+ textColor: "#D7D8DB"
+ borderWidth: 1
+ text: qsTr("Skip")
+
+ clickedFunc: function() {
+ if (!root.isUpdateItem)
+ return
+ NewsModel.markUpdateAsSkipped()
+ PageController.closePage()
+ }
+ }
}
}
}
diff --git a/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml b/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml
index 2aa4ba008..7041d4089 100644
--- a/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml
+++ b/client/ui/qml/Pages2/PageSettingsNewsNotifications.qml
@@ -69,8 +69,10 @@ PageType {
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
+ if (!isUpdate) {
NewsModel.markAsRead(index)
- NewsModel.processedIndex = index
+ }
+ NewsModel.processedIndex = index
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
}
}
diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml
index 0ad23f929..2a7730482 100644
--- a/client/ui/qml/main2.qml
+++ b/client/ui/qml/main2.qml
@@ -144,6 +144,10 @@ Window {
busyIndicator.visible = visible
PageController.disableControls(visible)
}
+
+ function onShowChangelogDrawer() {
+ changelogDrawer.openTriggered()
+ }
}
Connections {
@@ -386,4 +390,14 @@ Window {
onAccepted: SystemController.fileDialogClosed(true)
onRejected: SystemController.fileDialogClosed(false)
}
+
+ Item {
+ anchors.fill: parent
+
+ ChangelogDrawer {
+ id: changelogDrawer
+
+ anchors.fill: parent
+ }
+ }
}
diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc
index 18957481b..6a004038a 100644
--- a/client/ui/qml/qml.qrc
+++ b/client/ui/qml/qml.qrc
@@ -7,6 +7,7 @@
Components/HomeContainersListView.qml
Components/HomeSplitTunnelingDrawer.qml
Components/InstalledAppsDrawer.qml
+ Components/ChangelogDrawer.qml
Components/QuestionDrawer.qml
Components/SelectLanguageDrawer.qml
Components/ServersListView.qml
diff --git a/deploy/deploy_s3.sh b/deploy/deploy_s3.sh
index 193d26079..e2df5f3f1 100755
--- a/deploy/deploy_s3.sh
+++ b/deploy/deploy_s3.sh
@@ -12,7 +12,8 @@ mkdir -p dist
cd dist
-echo $VERSION >> VERSION
+echo $VERSION > VERSION
+curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE