diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 262ba6d20..f50b9921f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e1ed9283..4f3ecaac6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -set(AMNEZIAVPN_VERSION 4.8.12.0) +set(AMNEZIAVPN_VERSION 4.8.12.5) project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} DESCRIPTION "AmneziaVPN" diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 5b90947ca..45d53c631 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -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(); diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h index 8e5428558..8ec919b8c 100644 --- a/client/core/api/apiDefs.h +++ b/client/core/api/apiDefs.h @@ -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"); diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp index 2c0c86358..b2bee8be5 100644 --- a/client/core/api/apiUtils.cpp +++ b/client/core/api/apiUtils.cpp @@ -1,6 +1,7 @@ #include "apiUtils.h" #include +#include #include namespace @@ -88,6 +89,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &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 &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; } diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 9288a1753..132af459f 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -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 &nestedSslErrors) { + auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData, + &decryptionResult, this](QNetworkReply *reply, const QList &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> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) @@ -222,32 +251,33 @@ QFuture> GatewayController::postAsync(const QString reply->deleteLater(); - auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList &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 &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> 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 &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(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 onComplete) +void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, + std::function 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 &, QNetworkReply::NetworkError, const QString &, int)> onComplete) + std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete) { auto sslErrors = QSharedPointer>::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 &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); }); } diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h index f6b053366..96e842535 100644 --- a/client/core/controllers/gatewayController.h +++ b/client/core/controllers/gatewayController.h @@ -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 requestFunction, std::function &sslErrors)> replyProcessingFunction); @@ -50,7 +57,7 @@ private: void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function onComplete); void bypassProxyAsync( const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, - std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete); + std::function &, QNetworkReply::NetworkError, const QString &, int)> onComplete); int m_requestTimeoutMsecs; QString m_gatewayEndpoint; diff --git a/client/platforms/ios/ios_controller.h b/client/platforms/ios/ios_controller.h index 718e245fe..b4600655b 100644 --- a/client/platforms/ios/ios_controller.h +++ b/client/platforms/ios/ios_controller.h @@ -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); diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index af20a537b..8427fba70 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -1060,3 +1060,8 @@ void IosController::requestInetAccess() { }]; [task resume]; } + +bool IosController::isTestFlight() { + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"]; +} diff --git a/client/settings.cpp b/client/settings.cpp index a4e479d7f..c11295ef6 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -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) diff --git a/client/settings.h b/client/settings.h index 249506277..bbe6f939c 100644 --- a/client/settings.h +++ b/client/settings.h @@ -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(); diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index d4a2c105a..21ba5f8fd 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -1,9 +1,5 @@ #include "apiConfigsController.h" -#include -#include -#include -#include #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 +#include +#include +#include #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; @@ -176,7 +182,7 @@ namespace auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); - //TODO looks like this block can be removed after v1 configs EOL + // TODO looks like this block can be removed after v1 configs EOL serverProtocolConfig[config_key::junkPacketCount] = clientProtocolConfig.value(config_key::junkPacketCount); serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize); @@ -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, @@ -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,48 +389,38 @@ 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 products; - QString fetchError; - QEventLoop waitFetch; - IosController::Instance()->fetchProducts(productIds, - [&](const QList &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, - [&](bool success, const QString &txId, const QString &purchasedProductId, - const QString &originalTxId, const QString &errorString) { + IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"), + [&](bool success, const QString &txId, const QString &purchasedProductId, + const QString &originalTxId, const QString &errorString) { purchaseOk = success; originalTransactionId = originalTxId; storeTransactionId = txId; @@ -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"; + if (!fillAvailableServices()) { + qWarning().noquote() << "[IAP] Unable to fetch services list before restore"; emit errorOccurred(ErrorCode::ApiServicesMissingError); return false; } + + if (m_apiServicesModel->rowCount() <= 0) { + 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 &transactions, - const QString &errorString) { + IosController::Instance()->restorePurchases([&](bool success, const QList &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(errorCode); continue; } - ErrorCode installError = ErrorCode::NoError; - if (!installServerFromSubscriptionResponse(responseBody, &installError)) { - if (installError == ErrorCode::ApiConfigAlreadyAdded) { - duplicateConfigAlreadyPresent = true; - qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId - << "because subscription config with the same vpn_key already exists"; - } else { - qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" - << originalTransactionId; - } - continue; + 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 { + hasInstalledConfig = true; } - - 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); } diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index 24d4f203f..dc6546426 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -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 m_qrCodes; QString m_vpnKey; diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index c6b69647d..59a68fd88 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -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(); diff --git a/client/ui/qml/Controls2/ContextMenuType.qml b/client/ui/qml/Controls2/ContextMenuType.qml index 3842af2a9..cb32e3118 100644 --- a/client/ui/qml/Controls2/ContextMenuType.qml +++ b/client/ui/qml/Controls2/ContextMenuType.qml @@ -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() } diff --git a/client/ui/qml/Controls2/TextAreaType.qml b/client/ui/qml/Controls2/TextAreaType.qml index 7b6721ddc..3200e1f35 100644 --- a/client/ui/qml/Controls2/TextAreaType.qml +++ b/client/ui/qml/Controls2/TextAreaType.qml @@ -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 - } } } diff --git a/client/ui/qml/Controls2/TextAreaWithFooterType.qml b/client/ui/qml/Controls2/TextAreaWithFooterType.qml index 2cb6a69c9..f54a0688e 100644 --- a/client/ui/qml/Controls2/TextAreaWithFooterType.qml +++ b/client/ui/qml/Controls2/TextAreaWithFooterType.qml @@ -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 { diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index f8c74a59c..b7de64c63 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -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 } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml index 63edc53c8..16ac2f204 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -106,22 +106,18 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("Connect") - - clickedFunc: function() { - 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) + text: qsTr("Connect") + + clickedFunc: function() { + PageController.showBusyIndicator(true) + var result = ApiConfigsController.importService() + PageController.showBusyIndicator(false) + + if (!result) { + var endpoint = ApiServicesModel.getStoreEndpoint() + Qt.openUrlExternally(endpoint) + PageController.closePage() + PageController.closePage() } } }