chore: is-test-flight processing (#2093)

* fix: context menu fixes for qt6.9

* chore: is-test-flight porcessing

* chore: bump version and minor build fixes

* refactor: moved test purchase processing on client side

* fix: fixed free import on ios

* chore: bump qt version in deploy.yml

* fix: minor fixes
This commit is contained in:
vkamn
2025-12-29 19:18:03 +08:00
committed by GitHub
parent 6bac948633
commit d78202c612
19 changed files with 272 additions and 341 deletions

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.6.2
QT_VERSION: 6.9.2
QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -91,7 +91,7 @@ jobs:
runs-on: windows-latest
env:
QT_VERSION: 6.6.2
QT_VERSION: 6.9.2
QIF_VERSION: 4.7
BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
@@ -374,7 +374,7 @@ jobs:
runs-on: macos-latest
env:
QT_VERSION: 6.8.3
QT_VERSION: 6.9.2
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.12.0)
set(AMNEZIAVPN_VERSION 4.8.12.5)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"

View File

@@ -59,11 +59,13 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
AmneziaApplication::~AmneziaApplication()
{
#ifdef AMNEZIA_DESKTOP
if (m_vpnConnection) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
QThread::msleep(2000);
}
#endif
m_vpnConnectionThread.requestInterruption();
m_vpnConnectionThread.quit();

View File

@@ -68,6 +68,7 @@ namespace apiDefs
constexpr QLatin1String migrationCode("migration_code");
constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String isTestPurchase("is_test_purchase");
constexpr QLatin1String userCountryCode("user_country_code");

View File

@@ -1,6 +1,7 @@
#include "apiUtils.h"
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
namespace
@@ -88,6 +89,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
{
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
@@ -106,10 +108,20 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << replyError;
qDebug() << replyErrorString;
qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeConflict) {
int httpStatusFromBody = -1;
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
httpStatusFromBody = jsonObj.value("http_status").toInt(-1);
}
if (httpStatusFromBody == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError;
} else if (httpStatusCode == httpStatusCodeNotFound) {
} else if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}

View File

@@ -41,6 +41,11 @@ namespace
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -130,6 +135,26 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
return encRequestData;
}
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
DecryptionResult result;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
try {
QSimpleCrypto::QBlockCipher blockCipher;
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
result.isDecryptionSuccessful = true;
} catch (...) {
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
}
return result;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
@@ -153,21 +178,27 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater();
if (sslErrors.isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
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,
&encRequestData, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
sslErrors = nestedSslErrors;
return false;
}
@@ -179,21 +210,19 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
if (errorCode) {
return errorCode;
}
try {
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();
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
@@ -222,32 +251,33 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
reply->deleteLater();
auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList<QSslError> &sslErrors,
QNetworkReply::NetworkError replyError, const QString &replyErrorString,
int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, ecryptedResponseBody);
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish();
return;
}
QSimpleCrypto::QBlockCipher blockCipher;
try {
QByteArray responseBody = blockCipher.decryptAesBlockCipher(ecryptedResponseBody, encRequestData.key, encRequestData.iv, "",
encRequestData.salt);
promise->addResult(qMakePair(ErrorCode::NoError, responseBody));
promise->finish();
} catch (...) {
if (!decryptionResult.isDecryptionSuccessful) {
Utils::logException();
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
promise->finish();
};
if (sslErrors->isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
@@ -270,13 +300,21 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrls) {
bypassProxyAsync(endpoint, proxyUrls, encRequestData, processResponse);
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
});
});
});
} else {
processResponse(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
@@ -369,9 +407,23 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
return {};
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt)
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
{
const QByteArray &responseBody = decryptedResponseBody;
int httpStatus = -1;
if (isDecryptionSuccessful) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
httpStatus = jsonObj.value("http_status").toInt(-1);
}
} else {
qDebug() << "failed to decrypt the data";
return true;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << replyError;
@@ -379,7 +431,7 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
} else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
} else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
} else if (httpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
@@ -387,24 +439,18 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << replyError;
return true;
}
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
} else if (httpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << replyError;
return true;
}
} else if (httpStatus == httpStatusCodeConflict) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
} catch (...) {
qDebug() << "failed to decrypt the data";
return true;
}
}
return false;
}
@@ -552,7 +598,8 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete)
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
@@ -586,11 +633,11 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
}
@@ -601,7 +648,7 @@ void GatewayController::bypassProxyAsync(
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, reply]() {
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
@@ -609,6 +656,10 @@ void GatewayController::bypassProxyAsync(
reply->deleteLater();
onComplete(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode);
});
}

View File

@@ -36,11 +36,18 @@ private:
amnezia::ErrorCode errorCode;
};
struct DecryptionResult
{
QByteArray decryptedBody;
bool isDecryptionSuccessful;
};
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
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 = "");
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
@@ -50,7 +57,7 @@ private:
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;

View File

@@ -77,6 +77,7 @@ public:
const QString &errorString)> &&callback);
void requestInetAccess();
bool isTestFlight();
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);

View File

@@ -1060,3 +1060,8 @@ void IosController::requestInetAccess() {
}];
[task resume];
}
bool IosController::isTestFlight() {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
}

View File

@@ -534,14 +534,14 @@ void Settings::setDevGatewayEndpoint()
m_gatewayEndpoint = DEV_AGW_ENDPOINT;
}
QString Settings::getGatewayEndpoint()
QString Settings::getGatewayEndpoint(bool isTestPurchase)
{
return m_gatewayEndpoint;
return isTestPurchase ? DEV_AGW_ENDPOINT : m_gatewayEndpoint;
}
bool Settings::isDevGatewayEnv()
bool Settings::isDevGatewayEnv(bool isTestPurchase)
{
return value("Conf/devGatewayEnv", false).toBool();
return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();
}
void Settings::toggleDevGatewayEnv(bool enabled)

View File

@@ -223,8 +223,8 @@ public:
void resetGatewayEndpoint();
void setGatewayEndpoint(const QString &endpoint);
void setDevGatewayEndpoint();
QString getGatewayEndpoint();
bool isDevGatewayEnv();
QString getGatewayEndpoint(bool isTestPurchase = false);
bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled);
bool isHomeAdLabelVisible();

View File

@@ -1,9 +1,5 @@
#include "apiConfigsController.h"
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
#include "core/api/apiDefs.h"
@@ -12,6 +8,10 @@
#include "core/qrCodeUtils.h"
#include "ui/controllers/systemController.h"
#include "version.h"
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include "platforms/ios/ios_controller.h"
@@ -53,6 +53,12 @@ namespace
constexpr char isConnectEvent[] = "is_connect_event";
}
namespace serviceType
{
constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium";
}
struct ProtocolData
{
OpenVpnConfigurator::ConnectionData certRequest;
@@ -235,19 +241,6 @@ namespace
return ErrorCode::NoError;
}
bool isSubscriptionExpired(const QJsonObject &apiConfig)
{
auto subscription = apiConfig.value(configKey::subscription).toObject();
if (subscription.isEmpty()) {
return false;
}
auto subscriptionEndDate = subscription.value(configKey::endDate).toString();
if (apiUtils::isSubscriptionExpired(subscriptionEndDate)) {
return true;
}
return false;
}
}
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
@@ -284,11 +277,6 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -304,9 +292,9 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
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) {
emit errorOccurred(errorCode);
return false;
@@ -325,11 +313,6 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -341,9 +324,9 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
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) {
emit errorOccurred(errorCode);
return false;
@@ -406,46 +389,36 @@ bool ApiConfigsController::fillAvailableServices()
return true;
}
bool ApiConfigsController::importService()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
bool isIosOrMacOsNe = true;
#else
bool isIosOrMacOsNe = false;
#endif
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
if (isIosOrMacOsNe) {
importSerivceFromAppStore();
return true;
}
} else {
importServiceFromGateway();
return true;
}
return false;
}
bool ApiConfigsController::importSerivceFromAppStore()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
QString chosenProductId;
{
const QStringList productIds = { QStringLiteral("com.amnezia.amneziavpn.1_month"), QStringLiteral("com.amnezia.AmneziaVPN.6_month") };
qInfo().noquote() << "[IAP] Fetching products" << productIds;
QList<QVariantMap> products;
QString fetchError;
QEventLoop waitFetch;
IosController::Instance()->fetchProducts(productIds,
[&](const QList<QVariantMap> &prods, const QStringList &invalid, const QString &err) {
products = prods;
fetchError = err;
qInfo().noquote() << "[IAP] Fetch callback" << "invalid=" << invalid
<< "error=" << err;
waitFetch.quit();
});
waitFetch.exec();
qInfo().noquote() << "[IAP] Product fetch completed; success =" << fetchError.isEmpty()
<< "returned =" << products.size() << "invalid =" << !fetchError.isEmpty();
if (fetchError.isEmpty() && !products.isEmpty()) {
chosenProductId = products.first().value("productId").toString();
}
if (chosenProductId.isEmpty() && !productIds.isEmpty()) {
chosenProductId = productIds.first();
}
qInfo().noquote() << "[IAP] Chosen product =" << chosenProductId;
}
bool purchaseOk = false;
QString originalTransactionId;
QString storeTransactionId;
QString storeProductId;
QString purchaseError;
QEventLoop waitPurchase;
IosController::Instance()->purchaseProduct(chosenProductId,
IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"),
[&](bool success, const QString &txId, const QString &purchasedProductId,
const QString &originalTxId, const QString &errorString) {
purchaseOk = success;
@@ -463,11 +436,11 @@ bool ApiConfigsController::importSerivceFromAppStore()
return false;
}
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
<< "originalTransactionId =" << originalTransactionId
<< "productId =" << storeProductId;
<< "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
@@ -477,25 +450,22 @@ bool ApiConfigsController::importSerivceFromAppStore()
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
qInfo().noquote() << "[IAP] Sending subscription request. Payload:"
<< QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
auto isTestPurchase = IosController::Instance()->isTestFlight();
ErrorCode errorCode;
QByteArray responseBody;
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody);
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
ErrorCode installError = ErrorCode::NoError;
if (!installServerFromSubscriptionResponse(responseBody, &installError)) {
const ErrorCode errorToEmit = installError == ErrorCode::NoError ? ErrorCode::ApiPurchaseError : installError;
emit errorOccurred(errorToEmit);
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
qInfo().noquote() << "[IAP] Subscription config installed after purchase";
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#endif
return true;
@@ -505,22 +475,18 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
const QString premiumServiceType = QStringLiteral("amnezia-premium");
const QString originalServiceType = m_apiServicesModel->rowCount() > 0 ? m_apiServicesModel->getSelectedServiceType() : QString();
if (m_apiServicesModel->rowCount() <= 0) {
qInfo().noquote() << "[IAP] Services model is empty before restore, requesting available services";
if (!fillAvailableServices()) {
qWarning().noquote() << "[IAP] Unable to fetch services list before restore";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
}
if (m_apiServicesModel->rowCount() <= 0) {
qWarning().noquote() << "[IAP] Restore aborted: services list is still empty";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
// Ensure we have a valid premium selection for gateway requests
bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
@@ -530,8 +496,10 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
break;
}
}
if (!premiumSelected) {
m_apiServicesModel->setServiceIndex(0);
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
bool restoreSuccess = false;
@@ -539,9 +507,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
QString restoreError;
QEventLoop waitRestore;
IosController::Instance()->restorePurchases([&](bool success,
const QList<QVariantMap> &transactions,
const QString &errorString) {
IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
restoreSuccess = success;
restoredTransactions = transactions;
restoreError = errorString;
@@ -571,8 +537,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
const QString productId = transaction.value(QStringLiteral("productId")).toString();
if (originalTransactionId.isEmpty()) {
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId"
<< transactionId;
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
continue;
}
@@ -583,11 +548,11 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
processedTransactions.insert(originalTransactionId);
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
<< "originalTransactionId =" << originalTransactionId
<< "productId =" << productId;
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
@@ -597,43 +562,30 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
auto isTestPurchase = IosController::Instance()->isTestFlight();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
ErrorCode installError = ErrorCode::NoError;
if (!installServerFromSubscriptionResponse(responseBody, &installError)) {
if (installError == ErrorCode::ApiConfigAlreadyAdded) {
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true;
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
<< "because subscription config with the same vpn_key already exists";
} else if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
} else {
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction"
<< originalTransactionId;
}
continue;
}
hasInstalledConfig = true;
}
}
if (!hasInstalledConfig) {
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
emit errorOccurred(restoreError);
// Restore previous selection so that start page state is unchanged.
if (!originalServiceType.isEmpty()) {
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == originalServiceType) {
break;
}
}
}
return false;
}
@@ -642,16 +594,6 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
<< "duplicate restored transactions for original transaction IDs already processed";
}
// Restore previous selection if it differs from premium
if (!originalServiceType.isEmpty() && originalServiceType != premiumServiceType) {
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == originalServiceType) {
break;
}
}
}
#endif
return true;
}
@@ -714,11 +656,6 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
if (isSubscriptionExpired(apiConfig)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -738,8 +675,9 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
apiPayload.insert(configKey::isConnectEvent, true);
}
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
QJsonObject newServerConfig;
if (errorCode == ErrorCode::NoError) {
@@ -831,15 +769,6 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
return true;
}
if (isSubscriptionExpired(apiConfigObject)) {
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -851,9 +780,10 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
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) {
emit errorOccurred(errorCode);
return false;
@@ -875,11 +805,6 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
return true;
}
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
@@ -891,9 +816,9 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
serverConfigObject.value(configKey::authData).toObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
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) {
emit errorOccurred(errorCode);
return false;
@@ -972,95 +897,58 @@ QString ApiConfigsController::getVpnKey()
return m_vpnKey;
}
bool ApiConfigsController::installServerFromSubscriptionResponse(const QByteArray &responseBody, ErrorCode *errorOut)
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
{
#ifdef Q_OS_IOS
if (errorOut) {
*errorOut = ErrorCode::NoError;
}
QJsonParseError parseError {};
QJsonDocument responseDoc = QJsonDocument::fromJson(responseBody, &parseError);
if (parseError.error == QJsonParseError::NoError) {
qInfo().noquote() << "[IAP] Subscription raw response" << responseDoc.toJson(QJsonDocument::Compact);
} else {
qWarning().noquote() << "[IAP] Subscription raw response parse error:" << parseError.errorString()
<< "raw=" << QString::fromUtf8(responseBody);
}
const QJsonObject responseObject = responseDoc.object();
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
QString key = responseObject.value(QStringLiteral("key")).toString();
if (key.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
return ErrorCode::ApiPurchaseError;
}
if (m_serversModel->hasServerWithVpnKey(key)) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
if (errorOut) {
*errorOut = ErrorCode::ApiConfigAlreadyAdded;
}
return false;
return ErrorCode::ApiConfigAlreadyAdded;
}
QString normalizedKey = key;
normalizedKey.replace(QStringLiteral("vpn://"), QString());
QByteArray config = QByteArray::fromBase64(normalizedKey.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(config);
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(configString);
if (!configUncompressed.isEmpty()) {
config = configUncompressed;
configString = configUncompressed;
}
if (config.isEmpty()) {
if (configString.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
return ErrorCode::ApiPurchaseError;
}
QJsonParseError configParseError {};
QJsonDocument configDoc = QJsonDocument::fromJson(config, &configParseError);
if (configParseError.error != QJsonParseError::NoError) {
qWarning().noquote() << "[IAP] Failed to parse subscription config:" << configParseError.errorString();
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
}
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
QJsonObject configJson = configDoc.object();
quint16 crc = qChecksum(QJsonDocument(configJson).toJson());
auto apiConfig = configJson.value(apiDefs::key::apiConfig).toObject();
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = normalizedKey;
auto subscriptionObject = apiConfig.value(configKey::subscription).toObject();
qInfo().noquote() << "[IAP] Subscription payload details" << "serviceType="
<< apiConfig.value(configKey::serviceType).toString()
<< "serviceProtocol=" << apiConfig.value(configKey::serviceProtocol).toString()
<< "subscriptionEnd=" << subscriptionObject.value(apiDefs::key::subscriptionEndDate).toString()
<< "subscriptionType=" << subscriptionObject.value(QStringLiteral("type")).toString();
configJson.insert(apiDefs::key::apiConfig, apiConfig);
configJson.insert(config_key::crc, crc);
m_serversModel->addServer(configJson);
apiConfig[apiDefs::key::isTestPurchase] = isTestPurchase;
qDebug() << configJson;
return true;
configObject.insert(apiDefs::key::apiConfig, apiConfig);
configObject.insert(config_key::crc, crc);
m_serversModel->addServer(configObject);
return ErrorCode::NoError;
#else
Q_UNUSED(responseBody)
if (errorOut) {
*errorOut = ErrorCode::ApiPurchaseError;
}
return false;
Q_UNUSED(isTestPurchase)
return ErrorCode::NoError;
#endif
}
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
}

View File

@@ -26,6 +26,7 @@ public slots:
void copyVpnKeyToClipboard();
bool fillAvailableServices();
bool importService();
bool importSerivceFromAppStore();
bool restoreSerivceFromAppStore();
bool importServiceFromGateway();
@@ -55,8 +56,8 @@ private:
int getQrCodesCount();
QString getVpnKey();
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody);
bool installServerFromSubscriptionResponse(const QByteArray &responseBody, ErrorCode *errorOut = nullptr);
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
QList<QString> m_qrCodes;
QString m_vpnKey;

View File

@@ -5,6 +5,7 @@
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "platforms/ios/ios_controller.h"
#include "version.h"
namespace
@@ -49,14 +50,15 @@ bool ApiSettingsController::getAccountInfo(bool reload)
wait.exec(QEventLoop::ExcludeUserInputEvents);
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();

View File

@@ -1,33 +1,30 @@
import QtQuick
import QtQuick.Controls
import Qt.labs.platform
Menu {
property var textObj
popupType: Popup.Native
MenuItem {
text: qsTr("C&ut")
shortcut: StandardKey.Cut
enabled: textObj.selectedText
onTriggered: textObj.cut()
}
MenuItem {
text: qsTr("&Copy")
shortcut: StandardKey.Copy
enabled: textObj.selectedText
onTriggered: textObj.copy()
}
MenuItem {
text: qsTr("&Paste")
shortcut: StandardKey.Paste
// Fix calling paste from clipboard when launching app on android/ios
enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste
// Fix calling paste from clipboard when launching app on android
enabled: Qt.platform.os === "android" ? true : textObj.canPaste
onTriggered: textObj.paste()
}
MenuItem {
text: qsTr("&SelectAll")
shortcut: StandardKey.SelectAll
enabled: textObj.length > 0
onTriggered: textObj.selectAll()
}

View File

@@ -93,25 +93,13 @@ Rectangle {
wrapMode: Text.Wrap
MouseArea {
id: textAreaMouse
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
ContextMenu.menu: ContextMenuType {
textObj: textArea
}
onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor)
}
ContextMenuType {
id: contextMenu
textObj: textArea
}
}
}

View File

@@ -79,25 +79,13 @@ Rectangle {
wrapMode: Text.Wrap
MouseArea {
id: textAreaMouse
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
ContextMenu.menu: ContextMenuType {
textObj: textArea
}
onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor)
}
ContextMenuType {
id: contextMenu
textObj: textArea
}
}
RowLayout {

View File

@@ -137,15 +137,7 @@ Item {
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
enabled: true
}
ContextMenuType {
id: contextMenu
ContextMenu.menu: ContextMenuType {
textObj: textField
}

View File

@@ -109,19 +109,15 @@ PageType {
text: qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
if (endpoint !== undefined && endpoint !== "" && Qt.platform.os !== "ios" && !IsMacOsNeBuild) {
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
} else if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
PageController.showBusyIndicator(true)
ApiConfigsController.importSerivceFromAppStore()
PageController.showBusyIndicator(false)
} else {
PageController.showBusyIndicator(true)
ApiConfigsController.importServiceFromGateway()
PageController.showBusyIndicator(false)
}
}
}