2025-02-12 12:43:11 +07:00
|
|
|
#include "apiConfigsController.h"
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
#include "amnezia_application.h"
|
2025-02-07 22:22:14 +07:00
|
|
|
#include "configurators/wireguard_configurator.h"
|
|
|
|
|
#include "core/api/apiDefs.h"
|
2025-02-15 11:50:42 +07:00
|
|
|
#include "core/api/apiUtils.h"
|
2025-02-07 22:22:14 +07:00
|
|
|
#include "core/controllers/gatewayController.h"
|
2025-02-12 12:43:11 +07:00
|
|
|
#include "core/qrCodeUtils.h"
|
2025-02-07 22:22:14 +07:00
|
|
|
#include "ui/controllers/systemController.h"
|
2025-02-12 12:43:11 +07:00
|
|
|
#include "version.h"
|
2025-12-29 19:18:03 +08:00
|
|
|
#include <QClipboard>
|
2026-04-08 11:21:12 +07:00
|
|
|
#include <QCoreApplication>
|
2025-12-29 19:18:03 +08:00
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QEventLoop>
|
2026-04-08 11:21:12 +07:00
|
|
|
#include <QHash>
|
|
|
|
|
#include <QJsonArray>
|
2025-12-29 19:18:03 +08:00
|
|
|
#include <QSet>
|
2026-04-08 11:21:12 +07:00
|
|
|
#include <QVariantMap>
|
|
|
|
|
#include <limits>
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-12-18 16:36:12 +02:00
|
|
|
#include "platforms/ios/ios_controller.h"
|
2025-02-07 22:22:14 +07:00
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
namespace configKey
|
|
|
|
|
{
|
|
|
|
|
constexpr char cloak[] = "cloak";
|
|
|
|
|
constexpr char awg[] = "awg";
|
2025-07-03 09:58:23 +08:00
|
|
|
constexpr char vless[] = "vless";
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-03-21 10:25:44 +07:00
|
|
|
constexpr char apiEndpoint[] = "api_endpoint";
|
2025-02-07 22:22:14 +07:00
|
|
|
constexpr char accessToken[] = "api_key";
|
|
|
|
|
constexpr char certificate[] = "certificate";
|
|
|
|
|
constexpr char publicKey[] = "public_key";
|
2025-02-15 11:50:42 +07:00
|
|
|
constexpr char protocol[] = "protocol";
|
2025-02-07 22:22:14 +07:00
|
|
|
|
|
|
|
|
constexpr char uuid[] = "installation_uuid";
|
|
|
|
|
constexpr char osVersion[] = "os_version";
|
|
|
|
|
constexpr char appVersion[] = "app_version";
|
|
|
|
|
|
|
|
|
|
constexpr char userCountryCode[] = "user_country_code";
|
|
|
|
|
constexpr char serverCountryCode[] = "server_country_code";
|
|
|
|
|
constexpr char serviceType[] = "service_type";
|
|
|
|
|
constexpr char serviceInfo[] = "service_info";
|
|
|
|
|
constexpr char serviceProtocol[] = "service_protocol";
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
constexpr char services[] = "services";
|
|
|
|
|
constexpr char serviceDescription[] = "service_description";
|
|
|
|
|
constexpr char subscriptionPlans[] = "subscription_plans";
|
|
|
|
|
constexpr char storeProductId[] = "store_product_id";
|
|
|
|
|
constexpr char priceLabel[] = "price_label";
|
|
|
|
|
constexpr char subtitle[] = "subtitle";
|
|
|
|
|
constexpr char isTrial[] = "is_trial";
|
|
|
|
|
constexpr char minPriceLabel[] = "min_price_label";
|
|
|
|
|
|
2025-02-07 22:22:14 +07:00
|
|
|
constexpr char apiPayload[] = "api_payload";
|
|
|
|
|
constexpr char keyPayload[] = "key_payload";
|
|
|
|
|
|
|
|
|
|
constexpr char apiConfig[] = "api_config";
|
|
|
|
|
constexpr char authData[] = "auth_data";
|
|
|
|
|
|
|
|
|
|
constexpr char config[] = "config";
|
2025-08-26 20:31:41 +08:00
|
|
|
|
2025-09-30 12:10:27 +08:00
|
|
|
constexpr char isConnectEvent[] = "is_connect_event";
|
2025-02-07 22:22:14 +07:00
|
|
|
}
|
2025-07-03 09:58:23 +08:00
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
namespace serviceType
|
|
|
|
|
{
|
|
|
|
|
constexpr char amneziaFree[] = "amnezia-free";
|
|
|
|
|
constexpr char amneziaPremium[] = "amnezia-premium";
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
struct ProtocolData
|
|
|
|
|
{
|
|
|
|
|
OpenVpnConfigurator::ConnectionData certRequest;
|
|
|
|
|
|
|
|
|
|
QString wireGuardClientPrivKey;
|
|
|
|
|
QString wireGuardClientPubKey;
|
|
|
|
|
|
|
|
|
|
QString xrayUuid;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct GatewayRequestData
|
|
|
|
|
{
|
|
|
|
|
QString osVersion;
|
|
|
|
|
QString appVersion;
|
2025-11-03 10:26:22 +08:00
|
|
|
QString appLanguage;
|
2025-07-03 09:58:23 +08:00
|
|
|
|
|
|
|
|
QString installationUuid;
|
|
|
|
|
|
|
|
|
|
QString userCountryCode;
|
|
|
|
|
QString serverCountryCode;
|
|
|
|
|
QString serviceType;
|
|
|
|
|
QString serviceProtocol;
|
|
|
|
|
|
|
|
|
|
QJsonObject authData;
|
|
|
|
|
|
2025-07-08 15:14:00 +08:00
|
|
|
QString appName;
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject toJsonObject() const
|
|
|
|
|
{
|
|
|
|
|
QJsonObject obj;
|
|
|
|
|
if (!osVersion.isEmpty()) {
|
|
|
|
|
obj[configKey::osVersion] = osVersion;
|
|
|
|
|
}
|
|
|
|
|
if (!appVersion.isEmpty()) {
|
|
|
|
|
obj[configKey::appVersion] = appVersion;
|
|
|
|
|
}
|
2025-11-03 10:26:22 +08:00
|
|
|
if (!appLanguage.isEmpty()) {
|
|
|
|
|
obj[apiDefs::key::appLanguage] = appLanguage;
|
|
|
|
|
}
|
2025-07-03 09:58:23 +08:00
|
|
|
if (!installationUuid.isEmpty()) {
|
|
|
|
|
obj[configKey::uuid] = installationUuid;
|
|
|
|
|
}
|
|
|
|
|
if (!userCountryCode.isEmpty()) {
|
|
|
|
|
obj[configKey::userCountryCode] = userCountryCode;
|
|
|
|
|
}
|
|
|
|
|
if (!serverCountryCode.isEmpty()) {
|
|
|
|
|
obj[configKey::serverCountryCode] = serverCountryCode;
|
|
|
|
|
}
|
|
|
|
|
if (!serviceType.isEmpty()) {
|
|
|
|
|
obj[configKey::serviceType] = serviceType;
|
|
|
|
|
}
|
|
|
|
|
if (!serviceProtocol.isEmpty()) {
|
|
|
|
|
obj[configKey::serviceProtocol] = serviceProtocol;
|
|
|
|
|
}
|
|
|
|
|
if (!authData.isEmpty()) {
|
|
|
|
|
obj[configKey::authData] = authData;
|
|
|
|
|
}
|
2025-07-04 11:39:10 +08:00
|
|
|
if (!appName.isEmpty()) {
|
2025-07-08 15:14:00 +08:00
|
|
|
obj[apiDefs::key::cliName] = appName;
|
2025-07-04 11:39:10 +08:00
|
|
|
}
|
2025-07-03 09:58:23 +08:00
|
|
|
return obj;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ProtocolData generateProtocolData(const QString &protocol)
|
|
|
|
|
{
|
|
|
|
|
ProtocolData protocolData;
|
|
|
|
|
if (protocol == configKey::cloak) {
|
|
|
|
|
protocolData.certRequest = OpenVpnConfigurator::createCertRequest();
|
|
|
|
|
} else if (protocol == configKey::awg) {
|
|
|
|
|
auto connData = WireguardConfigurator::genClientKeys();
|
|
|
|
|
protocolData.wireGuardClientPubKey = connData.clientPubKey;
|
|
|
|
|
protocolData.wireGuardClientPrivKey = connData.clientPrivKey;
|
|
|
|
|
} else if (protocol == configKey::vless) {
|
|
|
|
|
protocolData.xrayUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return protocolData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload)
|
|
|
|
|
{
|
|
|
|
|
if (protocol == configKey::cloak) {
|
|
|
|
|
apiPayload[configKey::certificate] = protocolData.certRequest.request;
|
|
|
|
|
} else if (protocol == configKey::awg) {
|
|
|
|
|
apiPayload[configKey::publicKey] = protocolData.wireGuardClientPubKey;
|
|
|
|
|
} else if (protocol == configKey::vless) {
|
|
|
|
|
apiPayload[configKey::publicKey] = protocolData.xrayUuid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ErrorCode fillServerConfig(const QString &protocol, const ProtocolData &apiPayloadData, const QByteArray &apiResponseBody,
|
|
|
|
|
QJsonObject &serverConfig)
|
|
|
|
|
{
|
|
|
|
|
QString data = QJsonDocument::fromJson(apiResponseBody).object().value(config_key::config).toString();
|
|
|
|
|
|
|
|
|
|
data.replace("vpn://", "");
|
|
|
|
|
QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
|
|
|
|
|
|
|
|
|
if (ba.isEmpty()) {
|
|
|
|
|
qDebug() << "empty vpn key";
|
|
|
|
|
return ErrorCode::ApiConfigEmptyError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray ba_uncompressed = qUncompress(ba);
|
|
|
|
|
if (!ba_uncompressed.isEmpty()) {
|
|
|
|
|
ba = ba_uncompressed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString configStr = ba;
|
|
|
|
|
if (protocol == configKey::cloak) {
|
|
|
|
|
configStr.replace("<key>", "<key>\n");
|
|
|
|
|
configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey);
|
|
|
|
|
} else if (protocol == configKey::awg) {
|
|
|
|
|
configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey);
|
|
|
|
|
auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
|
|
|
|
|
auto containers = newServerConfig.value(config_key::containers).toArray();
|
|
|
|
|
if (containers.isEmpty()) {
|
|
|
|
|
qDebug() << "missing containers field";
|
|
|
|
|
return ErrorCode::ApiConfigEmptyError;
|
|
|
|
|
}
|
2025-12-11 15:18:36 +08:00
|
|
|
auto containerObject = containers.at(0).toObject();
|
|
|
|
|
auto containerType = ContainerProps::containerFromString(containerObject.value(config_key::container).toString());
|
|
|
|
|
QString containerName = ContainerProps::containerTypeToString(containerType);
|
|
|
|
|
auto serverProtocolConfig = containerObject.value(containerName).toObject();
|
2025-07-03 09:58:23 +08:00
|
|
|
auto clientProtocolConfig =
|
|
|
|
|
QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object();
|
2025-07-07 12:03:25 +08:00
|
|
|
|
2025-08-26 20:31:41 +08:00
|
|
|
// TODO looks like this block can be removed after v1 configs EOL
|
2025-07-07 12:03:25 +08:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
serverProtocolConfig[config_key::junkPacketCount] = clientProtocolConfig.value(config_key::junkPacketCount);
|
|
|
|
|
serverProtocolConfig[config_key::junkPacketMinSize] = clientProtocolConfig.value(config_key::junkPacketMinSize);
|
|
|
|
|
serverProtocolConfig[config_key::junkPacketMaxSize] = clientProtocolConfig.value(config_key::junkPacketMaxSize);
|
|
|
|
|
serverProtocolConfig[config_key::initPacketJunkSize] = clientProtocolConfig.value(config_key::initPacketJunkSize);
|
|
|
|
|
serverProtocolConfig[config_key::responsePacketJunkSize] = clientProtocolConfig.value(config_key::responsePacketJunkSize);
|
|
|
|
|
serverProtocolConfig[config_key::initPacketMagicHeader] = clientProtocolConfig.value(config_key::initPacketMagicHeader);
|
|
|
|
|
serverProtocolConfig[config_key::responsePacketMagicHeader] = clientProtocolConfig.value(config_key::responsePacketMagicHeader);
|
|
|
|
|
serverProtocolConfig[config_key::underloadPacketMagicHeader] = clientProtocolConfig.value(config_key::underloadPacketMagicHeader);
|
|
|
|
|
serverProtocolConfig[config_key::transportPacketMagicHeader] = clientProtocolConfig.value(config_key::transportPacketMagicHeader);
|
2025-07-07 12:03:25 +08:00
|
|
|
|
|
|
|
|
serverProtocolConfig[config_key::cookieReplyPacketJunkSize] = clientProtocolConfig.value(config_key::cookieReplyPacketJunkSize);
|
|
|
|
|
serverProtocolConfig[config_key::transportPacketJunkSize] = clientProtocolConfig.value(config_key::transportPacketJunkSize);
|
|
|
|
|
serverProtocolConfig[config_key::specialJunk1] = clientProtocolConfig.value(config_key::specialJunk1);
|
|
|
|
|
serverProtocolConfig[config_key::specialJunk2] = clientProtocolConfig.value(config_key::specialJunk2);
|
|
|
|
|
serverProtocolConfig[config_key::specialJunk3] = clientProtocolConfig.value(config_key::specialJunk3);
|
|
|
|
|
serverProtocolConfig[config_key::specialJunk4] = clientProtocolConfig.value(config_key::specialJunk4);
|
|
|
|
|
serverProtocolConfig[config_key::specialJunk5] = clientProtocolConfig.value(config_key::specialJunk5);
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
2025-12-11 15:18:36 +08:00
|
|
|
containerObject[containerName] = serverProtocolConfig;
|
|
|
|
|
containers.replace(0, containerObject);
|
2025-07-03 09:58:23 +08:00
|
|
|
newServerConfig[config_key::containers] = containers;
|
|
|
|
|
configStr = QString(QJsonDocument(newServerConfig).toJson());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
|
|
|
|
|
serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1);
|
|
|
|
|
serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2);
|
|
|
|
|
serverConfig[config_key::containers] = newServerConfig.value(config_key::containers);
|
|
|
|
|
serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName);
|
|
|
|
|
|
|
|
|
|
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
|
|
|
|
|
serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion);
|
|
|
|
|
serverConfig[config_key::description] = newServerConfig.value(config_key::description);
|
|
|
|
|
serverConfig[config_key::name] = newServerConfig.value(config_key::name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString();
|
|
|
|
|
serverConfig[config_key::defaultContainer] = defaultContainer;
|
|
|
|
|
|
|
|
|
|
QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap();
|
|
|
|
|
map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap());
|
|
|
|
|
auto apiConfig = QJsonObject::fromVariantMap(map);
|
|
|
|
|
|
|
|
|
|
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
|
|
|
|
|
apiConfig.insert(apiDefs::key::supportedProtocols,
|
|
|
|
|
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::supportedProtocols).toArray());
|
2025-11-03 10:26:22 +08:00
|
|
|
|
|
|
|
|
apiConfig.insert(apiDefs::key::serviceInfo,
|
|
|
|
|
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::serviceInfo).toObject());
|
2025-07-03 09:58:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverConfig[configKey::apiConfig] = apiConfig;
|
|
|
|
|
|
|
|
|
|
return ErrorCode::NoError;
|
|
|
|
|
}
|
2026-04-08 11:21:12 +07:00
|
|
|
|
|
|
|
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
|
|
|
|
struct StoreKitPlanQuote {
|
|
|
|
|
QString displayPrice;
|
|
|
|
|
double priceAmount = 0.0;
|
|
|
|
|
double subscriptionBillingMonths = 0.0;
|
|
|
|
|
QString displayPricePerMonth;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
constexpr double kOneMonthThreshold = 1.0 + 1e-6;
|
|
|
|
|
constexpr double kMonthsFallbackThreshold = 1e-6;
|
|
|
|
|
constexpr double kMonthlyPriceEpsilon = 1e-9;
|
|
|
|
|
|
|
|
|
|
QStringList collectPremiumStoreProductIds(const QJsonArray &services)
|
|
|
|
|
{
|
|
|
|
|
QStringList productIds;
|
|
|
|
|
QSet<QString> seenProductIds;
|
|
|
|
|
for (const QJsonValue &serviceValue : services) {
|
|
|
|
|
const QJsonObject serviceObject = serviceValue.toObject();
|
|
|
|
|
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const QJsonArray subscriptionPlans =
|
|
|
|
|
serviceObject.value(configKey::serviceDescription).toObject().value(configKey::subscriptionPlans).toArray();
|
|
|
|
|
for (const QJsonValue &planValue : subscriptionPlans) {
|
|
|
|
|
if (!planValue.isObject()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const QString storeProductId = planValue.toObject().value(configKey::storeProductId).toString();
|
|
|
|
|
if (storeProductId.isEmpty() || seenProductIds.contains(storeProductId)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
seenProductIds.insert(storeProductId);
|
|
|
|
|
productIds.append(storeProductId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return productIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QHash<QString, StoreKitPlanQuote> buildStoreKitQuoteMap(const QList<QVariantMap> &fetchedProducts)
|
|
|
|
|
{
|
|
|
|
|
QHash<QString, StoreKitPlanQuote> quotesByProductId;
|
|
|
|
|
quotesByProductId.reserve(fetchedProducts.size());
|
|
|
|
|
|
|
|
|
|
for (const QVariantMap &productInfo : fetchedProducts) {
|
|
|
|
|
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
|
|
|
|
if (productId.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
|
|
|
|
if (displayPrice.isEmpty()) {
|
|
|
|
|
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
|
|
|
|
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
|
|
|
|
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StoreKitPlanQuote quote;
|
|
|
|
|
quote.displayPrice = displayPrice;
|
|
|
|
|
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
|
|
|
|
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
|
|
|
|
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
|
|
|
|
quotesByProductId.insert(productId, quote);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return quotesByProductId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
|
|
|
|
{
|
|
|
|
|
QJsonArray services = data.value(configKey::services).toArray();
|
|
|
|
|
if (services.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QStringList productIds = collectPremiumStoreProductIds(services);
|
|
|
|
|
if (productIds.isEmpty()) {
|
|
|
|
|
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<QVariantMap> fetchedProducts;
|
|
|
|
|
QEventLoop loop;
|
|
|
|
|
IosController::Instance()->fetchProducts(productIds,
|
|
|
|
|
[&](const QList<QVariantMap> &products, const QStringList &invalidIds,
|
|
|
|
|
const QString &errorString) {
|
|
|
|
|
if (!errorString.isEmpty()) {
|
|
|
|
|
qWarning().noquote() << "[IAP] StoreKit merge fetch:" << errorString;
|
|
|
|
|
}
|
|
|
|
|
if (!invalidIds.isEmpty()) {
|
|
|
|
|
qWarning().noquote() << "[IAP] Unknown App Store product ids:" << invalidIds;
|
|
|
|
|
}
|
|
|
|
|
fetchedProducts = products;
|
|
|
|
|
loop.quit();
|
|
|
|
|
});
|
|
|
|
|
loop.exec();
|
|
|
|
|
|
|
|
|
|
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
|
|
|
|
|
|
|
|
|
|
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
|
|
|
|
|
QJsonObject serviceObject = services.at(serviceIndex).toObject();
|
|
|
|
|
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject descriptionObject = serviceObject.value(configKey::serviceDescription).toObject();
|
|
|
|
|
const QJsonArray sourcePlans = descriptionObject.value(configKey::subscriptionPlans).toArray();
|
|
|
|
|
|
|
|
|
|
QJsonArray mergedPlans;
|
|
|
|
|
double minMonthlyAmount = std::numeric_limits<double>::infinity();
|
|
|
|
|
QString minMonthlyDisplay;
|
|
|
|
|
|
|
|
|
|
for (const QJsonValue &planValue : sourcePlans) {
|
|
|
|
|
if (!planValue.isObject()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject planObject = planValue.toObject();
|
|
|
|
|
const QString storeProductId = planObject.value(configKey::storeProductId).toString();
|
|
|
|
|
if (storeProductId.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto quoteIterator = quotesByProductId.constFind(storeProductId);
|
|
|
|
|
if (quoteIterator == quotesByProductId.cend()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
|
|
|
|
|
const StoreKitPlanQuote "e = *quoteIterator;
|
|
|
|
|
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
|
|
|
|
|
|
|
|
|
const double months = quote.subscriptionBillingMonths;
|
|
|
|
|
if (!isTrialPlan && months > kOneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
|
|
|
|
|
planObject.insert(
|
|
|
|
|
configKey::subtitle,
|
|
|
|
|
QCoreApplication::translate("ApiConfigsController", "%1/mo", "IAP: price per month in plan subtitle")
|
|
|
|
|
.arg(quote.displayPricePerMonth));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isTrialPlan && quote.priceAmount > 0.0) {
|
|
|
|
|
const double monthsForMin = months > kMonthsFallbackThreshold ? months : 1.0;
|
|
|
|
|
const double monthly = quote.priceAmount / monthsForMin;
|
|
|
|
|
if (monthly < minMonthlyAmount - kMonthlyPriceEpsilon) {
|
|
|
|
|
minMonthlyAmount = monthly;
|
|
|
|
|
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mergedPlans.append(planObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
descriptionObject.insert(configKey::subscriptionPlans, mergedPlans);
|
|
|
|
|
if (minMonthlyAmount < std::numeric_limits<double>::infinity() && !minMonthlyDisplay.isEmpty()) {
|
|
|
|
|
descriptionObject.insert(configKey::minPriceLabel,
|
|
|
|
|
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
|
|
|
|
|
"IAP: card footer minimum monthly price from StoreKit")
|
|
|
|
|
.arg(minMonthlyDisplay));
|
|
|
|
|
}
|
|
|
|
|
serviceObject.insert(configKey::serviceDescription, descriptionObject);
|
|
|
|
|
services.replace(serviceIndex, serviceObject);
|
|
|
|
|
}
|
|
|
|
|
data.insert(configKey::services, services);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2025-02-07 22:22:14 +07:00
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
|
|
|
|
|
const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
2026-04-08 11:21:12 +07:00
|
|
|
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
|
|
|
|
|
const QSharedPointer<ApiBenefitsModel> &benefitsModel,
|
2025-02-15 11:50:42 +07:00
|
|
|
const std::shared_ptr<Settings> &settings, QObject *parent)
|
2026-04-08 11:21:12 +07:00
|
|
|
: QObject(parent)
|
|
|
|
|
, m_serversModel(serversModel)
|
|
|
|
|
, m_apiServicesModel(apiServicesModel)
|
|
|
|
|
, m_subscriptionPlansModel(subscriptionPlansModel)
|
|
|
|
|
, m_benefitsModel(benefitsModel)
|
|
|
|
|
, m_settings(settings)
|
2025-02-07 22:22:14 +07:00
|
|
|
{
|
2026-04-08 11:21:12 +07:00
|
|
|
connect(m_apiServicesModel.data(), &ApiServicesModel::serviceSelectionChanged, this, [this]() {
|
|
|
|
|
const ApiServicesModel::ApiServicesData serviceData = m_apiServicesModel->selectedServiceData();
|
|
|
|
|
m_subscriptionPlansModel->updateModel(serviceData.subscriptionPlansJson);
|
|
|
|
|
m_benefitsModel->updateModel(serviceData.benefits);
|
|
|
|
|
});
|
2025-02-07 22:22:14 +07:00
|
|
|
}
|
|
|
|
|
|
2025-10-07 19:16:28 +04:00
|
|
|
bool ApiConfigsController::exportVpnKey(const QString &fileName)
|
|
|
|
|
{
|
|
|
|
|
if (fileName.isEmpty()) {
|
|
|
|
|
emit errorOccurred(ErrorCode::PermissionsError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prepareVpnKeyExport();
|
|
|
|
|
if (m_vpnKey.isEmpty()) {
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SystemController::saveFile(fileName, m_vpnKey);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 15:10:59 +07:00
|
|
|
bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName)
|
2025-02-07 22:22:14 +07:00
|
|
|
{
|
2025-02-10 15:10:59 +07:00
|
|
|
if (fileName.isEmpty()) {
|
|
|
|
|
emit errorOccurred(ErrorCode::PermissionsError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 22:22:14 +07:00
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
|
|
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-11-03 10:26:22 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-07-03 09:58:23 +08:00
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
apiConfigObject.value(configKey::userCountryCode).toString(),
|
|
|
|
|
serverCountryCode,
|
|
|
|
|
apiConfigObject.value(configKey::serviceType).toString(),
|
2025-07-16 13:26:19 +08:00
|
|
|
configKey::awg, // apiConfigObject.value(configKey::serviceProtocol).toString(),
|
2025-07-03 09:58:23 +08:00
|
|
|
serverConfigObject.value(configKey::authData).toObject() };
|
|
|
|
|
|
2025-07-16 13:26:19 +08:00
|
|
|
QString protocol = gatewayRequestData.serviceProtocol;
|
2025-07-03 09:58:23 +08:00
|
|
|
ProtocolData protocolData = generateProtocolData(protocol);
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
|
|
|
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
|
2025-12-29 19:18:03 +08:00
|
|
|
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
2025-02-07 22:22:14 +07:00
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase);
|
2025-02-10 15:10:59 +07:00
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-02-07 22:22:14 +07:00
|
|
|
|
|
|
|
|
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
|
|
|
|
|
QString nativeConfig = jsonConfig.value(configKey::config).toString();
|
2025-07-03 09:58:23 +08:00
|
|
|
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
|
2025-02-07 22:22:14 +07:00
|
|
|
|
|
|
|
|
SystemController::saveFile(fileName, nativeConfig);
|
2025-02-10 15:10:59 +07:00
|
|
|
return true;
|
2025-02-07 22:22:14 +07:00
|
|
|
}
|
|
|
|
|
|
2025-02-20 13:44:19 +07:00
|
|
|
bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
|
|
|
|
|
{
|
|
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
|
|
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-11-03 10:26:22 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-07-03 09:58:23 +08:00
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
apiConfigObject.value(configKey::userCountryCode).toString(),
|
|
|
|
|
serverCountryCode,
|
|
|
|
|
apiConfigObject.value(configKey::serviceType).toString(),
|
2025-07-16 13:26:19 +08:00
|
|
|
configKey::awg, // apiConfigObject.value(configKey::serviceProtocol).toString(),
|
2025-07-03 09:58:23 +08:00
|
|
|
serverConfigObject.value(configKey::authData).toObject() };
|
2025-02-20 13:44:19 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
2025-12-29 19:18:03 +08:00
|
|
|
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
2025-02-20 13:44:19 +07:00
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase);
|
2025-02-23 14:26:04 +07:00
|
|
|
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
|
2025-02-20 13:44:19 +07:00
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 12:43:11 +07:00
|
|
|
void ApiConfigsController::prepareVpnKeyExport()
|
|
|
|
|
{
|
|
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex());
|
|
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
|
|
|
|
|
|
|
|
|
auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString();
|
2025-10-07 19:16:28 +04:00
|
|
|
if (vpnKey.isEmpty()) {
|
|
|
|
|
vpnKey = apiUtils::getPremiumV2VpnKey(serverConfigObject);
|
|
|
|
|
apiConfigObject.insert(apiDefs::key::vpnKey, vpnKey);
|
|
|
|
|
serverConfigObject.insert(configKey::apiConfig, apiConfigObject);
|
|
|
|
|
m_serversModel->editServer(serverConfigObject, m_serversModel->getProcessedServerIndex());
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-22 14:42:09 +07:00
|
|
|
m_vpnKey = vpnKey;
|
2025-02-12 12:43:11 +07:00
|
|
|
|
2025-02-21 14:15:23 +07:00
|
|
|
vpnKey.replace("vpn://", "");
|
|
|
|
|
|
2025-02-19 14:56:53 +07:00
|
|
|
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(vpnKey.toUtf8());
|
2025-02-12 12:43:11 +07:00
|
|
|
|
|
|
|
|
emit vpnKeyExportReady();
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-22 14:42:09 +07:00
|
|
|
void ApiConfigsController::copyVpnKeyToClipboard()
|
|
|
|
|
{
|
|
|
|
|
auto clipboard = amnApp->getClipboard();
|
|
|
|
|
clipboard->setText(m_vpnKey);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
bool ApiConfigsController::fillAvailableServices()
|
|
|
|
|
{
|
|
|
|
|
QJsonObject apiPayload;
|
|
|
|
|
apiPayload[configKey::osVersion] = QSysInfo::productType();
|
2026-03-24 19:25:04 +07:00
|
|
|
apiPayload[configKey::appVersion] = QString(APP_VERSION);
|
|
|
|
|
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
|
2025-11-03 10:26:22 +08:00
|
|
|
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
QByteArray responseBody;
|
2025-07-03 09:58:23 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
|
2025-02-15 11:50:42 +07:00
|
|
|
if (errorCode == ErrorCode::NoError) {
|
|
|
|
|
if (!responseBody.contains("services")) {
|
|
|
|
|
errorCode = ErrorCode::ApiServicesMissingError;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
|
2026-04-08 11:21:12 +07:00
|
|
|
|
2026-02-05 15:23:06 +03:00
|
|
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
2026-04-08 11:21:12 +07:00
|
|
|
mergeStoreKitPricesIntoPremiumPlans(data);
|
2026-02-05 15:23:06 +03:00
|
|
|
#endif
|
2026-04-08 11:21:12 +07:00
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
m_apiServicesModel->updateModel(data);
|
2025-12-18 16:36:12 +02:00
|
|
|
if (m_apiServicesModel->rowCount() > 0) {
|
|
|
|
|
m_apiServicesModel->setServiceIndex(0);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
bool ApiConfigsController::importService()
|
2025-12-18 16:36:12 +02:00
|
|
|
{
|
|
|
|
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
2026-04-08 11:21:12 +07:00
|
|
|
const bool isIosOrMacOsNe = true;
|
2025-12-29 19:18:03 +08:00
|
|
|
#else
|
2026-04-08 11:21:12 +07:00
|
|
|
const bool isIosOrMacOsNe = false;
|
2025-12-29 19:18:03 +08:00
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
|
|
|
|
if (isIosOrMacOsNe) {
|
2026-04-08 11:21:12 +07:00
|
|
|
return importPremiumFromAppStore(QString());
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
2026-03-24 09:29:51 +07:00
|
|
|
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
2026-04-08 11:21:12 +07:00
|
|
|
return importFreeFromGateway();
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
2025-12-29 19:18:03 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-18 16:36:12 +02:00
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProductId)
|
2025-12-29 19:18:03 +08:00
|
|
|
{
|
|
|
|
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
2026-04-08 11:21:12 +07:00
|
|
|
QString productId = storeProductId.trimmed();
|
|
|
|
|
if (productId.isEmpty()) {
|
|
|
|
|
productId = QStringLiteral("amnezia_premium_6_month");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 16:36:12 +02:00
|
|
|
bool purchaseOk = false;
|
|
|
|
|
QString originalTransactionId;
|
|
|
|
|
QString storeTransactionId;
|
2026-04-08 11:21:12 +07:00
|
|
|
QString purchasedStoreProductId;
|
2025-12-18 16:36:12 +02:00
|
|
|
QString purchaseError;
|
|
|
|
|
QEventLoop waitPurchase;
|
2026-04-08 11:21:12 +07:00
|
|
|
IosController::Instance()->purchaseProduct(productId,
|
|
|
|
|
[&](bool success, const QString &transactionId, const QString &purchasedProductId,
|
|
|
|
|
const QString &originalTransactionIdResponse, const QString &errorString) {
|
2025-12-18 16:36:12 +02:00
|
|
|
purchaseOk = success;
|
2026-04-08 11:21:12 +07:00
|
|
|
originalTransactionId = originalTransactionIdResponse;
|
|
|
|
|
storeTransactionId = transactionId;
|
|
|
|
|
purchasedStoreProductId = purchasedProductId;
|
2025-12-18 16:36:12 +02:00
|
|
|
purchaseError = errorString;
|
|
|
|
|
waitPurchase.quit();
|
|
|
|
|
});
|
|
|
|
|
waitPurchase.exec();
|
|
|
|
|
|
|
|
|
|
if (!purchaseOk || originalTransactionId.isEmpty()) {
|
|
|
|
|
qDebug() << "IAP purchase failed:" << purchaseError;
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
|
2026-04-08 11:21:12 +07:00
|
|
|
<< "originalTransactionId =" << originalTransactionId << "productId =" << purchasedStoreProductId;
|
2025-12-18 16:36:12 +02:00
|
|
|
|
|
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-12-29 19:18:03 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-12-18 16:36:12 +02:00
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
m_apiServicesModel->getCountryCode(),
|
|
|
|
|
"",
|
|
|
|
|
m_apiServicesModel->getSelectedServiceType(),
|
|
|
|
|
m_apiServicesModel->getSelectedServiceProtocol(),
|
|
|
|
|
QJsonObject() };
|
|
|
|
|
|
|
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
|
|
|
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
2025-12-29 19:18:03 +08:00
|
|
|
auto isTestPurchase = IosController::Instance()->isTestFlight();
|
2025-12-18 16:36:12 +02:00
|
|
|
|
|
|
|
|
ErrorCode errorCode;
|
|
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
2025-12-18 16:36:12 +02:00
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
int duplicateServerIndex = -1;
|
|
|
|
|
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
|
|
|
|
|
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
2026-04-10 21:24:00 +07:00
|
|
|
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
|
2026-04-08 11:21:12 +07:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-29 19:18:03 +08:00
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
2025-12-18 16:36:12 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-08 11:21:12 +07:00
|
|
|
emit installServerFromApiFinished(
|
2026-04-10 21:24:00 +07:00
|
|
|
tr("%1 has been added to the app").arg(m_apiServicesModel->getSelectedServiceName()));
|
2025-12-18 16:36:12 +02:00
|
|
|
return true;
|
2026-04-08 11:21:12 +07:00
|
|
|
#else
|
|
|
|
|
Q_UNUSED(storeProductId);
|
|
|
|
|
return false;
|
|
|
|
|
#endif
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
bool ApiConfigsController::restoreServiceFromAppStore()
|
2025-12-18 16:36:12 +02:00
|
|
|
{
|
|
|
|
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
|
|
|
|
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
|
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
if (!fillAvailableServices()) {
|
|
|
|
|
qWarning().noquote() << "[IAP] Unable to fetch services list before restore";
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
|
|
|
|
return false;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_apiServicesModel->rowCount() <= 0) {
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-29 19:18:03 +08:00
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
const int premiumServiceIndex = m_apiServicesModel->serviceIndexForType(premiumServiceType);
|
|
|
|
|
if (premiumServiceIndex < 0) {
|
2025-12-29 19:18:03 +08:00
|
|
|
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
|
|
|
|
return false;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
2026-04-08 11:21:12 +07:00
|
|
|
m_apiServicesModel->setServiceIndex(premiumServiceIndex);
|
2025-12-18 16:36:12 +02:00
|
|
|
|
|
|
|
|
bool restoreSuccess = false;
|
|
|
|
|
QList<QVariantMap> restoredTransactions;
|
|
|
|
|
QString restoreError;
|
|
|
|
|
QEventLoop waitRestore;
|
|
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
|
2025-12-18 16:36:12 +02:00
|
|
|
restoreSuccess = success;
|
|
|
|
|
restoredTransactions = transactions;
|
|
|
|
|
restoreError = errorString;
|
|
|
|
|
waitRestore.quit();
|
|
|
|
|
});
|
|
|
|
|
waitRestore.exec();
|
|
|
|
|
|
|
|
|
|
if (!restoreSuccess) {
|
|
|
|
|
qWarning().noquote() << "[IAP] Restore failed:" << restoreError;
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (restoredTransactions.isEmpty()) {
|
2026-04-08 11:21:12 +07:00
|
|
|
qInfo().noquote() << "[IAP] Restore completed, but no active entitlements found";
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiNoPurchasedSubscriptionsError);
|
2025-12-18 16:36:12 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
const bool isTestPurchase = IosController::Instance()->isTestFlight();
|
|
|
|
|
const QString serviceType = m_apiServicesModel->getSelectedServiceType();
|
|
|
|
|
const QString serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol();
|
|
|
|
|
const QString countryCode = m_apiServicesModel->getCountryCode();
|
|
|
|
|
const QString appLanguage = m_settings->getAppLanguage().name().split("_").first();
|
|
|
|
|
const QString installationUuid = m_settings->getInstallationUuid(true);
|
|
|
|
|
|
2025-12-18 16:36:12 +02:00
|
|
|
bool hasInstalledConfig = false;
|
|
|
|
|
bool duplicateConfigAlreadyPresent = false;
|
2026-04-08 11:21:12 +07:00
|
|
|
int duplicateServerIndex = -1;
|
|
|
|
|
QSet<QString> processedOriginalTransactionIds;
|
|
|
|
|
|
2025-12-18 16:36:12 +02:00
|
|
|
for (const QVariantMap &transaction : restoredTransactions) {
|
|
|
|
|
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
|
|
|
|
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
|
|
|
|
const QString productId = transaction.value(QStringLiteral("productId")).toString();
|
|
|
|
|
|
|
|
|
|
if (originalTransactionId.isEmpty()) {
|
2025-12-29 19:18:03 +08:00
|
|
|
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
|
2025-12-18 16:36:12 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
if (processedOriginalTransactionIds.contains(originalTransactionId)) {
|
|
|
|
|
qInfo().noquote() << "[IAP] Skipping duplicate restored transaction" << originalTransactionId;
|
2025-12-18 16:36:12 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-08 11:21:12 +07:00
|
|
|
processedOriginalTransactionIds.insert(originalTransactionId);
|
2025-12-18 16:36:12 +02:00
|
|
|
|
|
|
|
|
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
2025-12-29 19:18:03 +08:00
|
|
|
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
|
2025-12-18 16:36:12 +02:00
|
|
|
|
|
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2026-04-08 11:21:12 +07:00
|
|
|
appLanguage,
|
|
|
|
|
installationUuid,
|
|
|
|
|
countryCode,
|
2025-12-18 16:36:12 +02:00
|
|
|
"",
|
2026-04-08 11:21:12 +07:00
|
|
|
serviceType,
|
|
|
|
|
serviceProtocol,
|
2025-12-18 16:36:12 +02:00
|
|
|
QJsonObject() };
|
|
|
|
|
|
|
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
|
|
|
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
2026-04-08 11:21:12 +07:00
|
|
|
|
2025-12-18 16:36:12 +02:00
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
2025-12-18 16:36:12 +02:00
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId
|
|
|
|
|
<< "errorCode =" << static_cast<int>(errorCode);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
int currentDuplicateServerIndex = -1;
|
|
|
|
|
errorCode = importServiceFromBilling(responseBody, isTestPurchase, currentDuplicateServerIndex);
|
2025-12-29 19:18:03 +08:00
|
|
|
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
|
|
|
|
duplicateConfigAlreadyPresent = true;
|
2026-04-08 11:21:12 +07:00
|
|
|
if (duplicateServerIndex < 0) {
|
|
|
|
|
duplicateServerIndex = currentDuplicateServerIndex;
|
|
|
|
|
}
|
|
|
|
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists" << originalTransactionId;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
|
|
|
|
|
<< "errorCode =" << static_cast<int>(errorCode);
|
|
|
|
|
continue;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
2026-04-08 11:21:12 +07:00
|
|
|
|
|
|
|
|
hasInstalledConfig = true;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasInstalledConfig) {
|
2026-04-08 11:21:12 +07:00
|
|
|
if (duplicateConfigAlreadyPresent) {
|
2026-04-10 21:24:00 +07:00
|
|
|
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
|
2026-04-08 11:21:12 +07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
2025-12-18 16:36:12 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
|
|
|
|
#endif
|
2025-02-15 11:50:42 +07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
bool ApiConfigsController::importFreeFromGateway()
|
2025-02-15 11:50:42 +07:00
|
|
|
{
|
2025-07-03 09:58:23 +08:00
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-11-03 10:26:22 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-07-03 09:58:23 +08:00
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
m_apiServicesModel->getCountryCode(),
|
|
|
|
|
"",
|
|
|
|
|
m_apiServicesModel->getSelectedServiceType(),
|
|
|
|
|
m_apiServicesModel->getSelectedServiceProtocol(),
|
2025-07-08 15:14:00 +08:00
|
|
|
QJsonObject(),
|
|
|
|
|
QString(APPLICATION_NAME) };
|
2025-07-03 09:58:23 +08:00
|
|
|
|
|
|
|
|
if (m_serversModel->isServerFromApiAlreadyExists(gatewayRequestData.userCountryCode, gatewayRequestData.serviceType,
|
|
|
|
|
gatewayRequestData.serviceProtocol)) {
|
2025-02-15 11:50:42 +07:00
|
|
|
emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
|
|
|
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-12-18 16:36:12 +02:00
|
|
|
ErrorCode errorCode;
|
2025-02-15 11:50:42 +07:00
|
|
|
QByteArray responseBody;
|
2025-12-18 16:36:12 +02:00
|
|
|
|
|
|
|
|
errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
QJsonObject serverConfig;
|
|
|
|
|
if (errorCode == ErrorCode::NoError) {
|
2025-07-03 09:58:23 +08:00
|
|
|
errorCode = fillServerConfig(gatewayRequestData.serviceProtocol, protocolData, responseBody, serverConfig);
|
|
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
QJsonObject apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
|
|
|
|
apiConfig.insert(configKey::userCountryCode, m_apiServicesModel->getCountryCode());
|
|
|
|
|
apiConfig.insert(configKey::serviceType, m_apiServicesModel->getSelectedServiceType());
|
|
|
|
|
apiConfig.insert(configKey::serviceProtocol, m_apiServicesModel->getSelectedServiceProtocol());
|
|
|
|
|
|
|
|
|
|
serverConfig.insert(configKey::apiConfig, apiConfig);
|
|
|
|
|
|
|
|
|
|
m_serversModel->addServer(serverConfig);
|
|
|
|
|
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
bool ApiConfigsController::importTrialFromGateway(const QString &email)
|
|
|
|
|
{
|
|
|
|
|
emit trialEmailError(QString());
|
|
|
|
|
|
|
|
|
|
const QString trimmedEmail = email.trimmed();
|
|
|
|
|
if (trimmedEmail.isEmpty()) {
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
|
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
|
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
m_apiServicesModel->getCountryCode(),
|
|
|
|
|
"",
|
|
|
|
|
m_apiServicesModel->getSelectedServiceType(),
|
|
|
|
|
m_apiServicesModel->getSelectedServiceProtocol(),
|
|
|
|
|
QJsonObject() };
|
|
|
|
|
|
|
|
|
|
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
|
|
|
|
|
|
|
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
|
|
|
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
|
|
|
|
|
apiPayload.insert(apiDefs::key::email, trimmedEmail);
|
|
|
|
|
|
|
|
|
|
QByteArray responseBody;
|
|
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
|
|
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
if (errorCode == ErrorCode::ApiTrialAlreadyUsedError) {
|
2026-04-10 21:24:00 +07:00
|
|
|
emit trialEmailError(tr("This email address has already been used to activate a trial. If you like the service, you can upgrade to Premium"));
|
2026-04-08 11:21:12 +07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
|
|
|
|
QString key = responseObject.value(apiDefs::key::config).toString();
|
|
|
|
|
if (key.isEmpty()) {
|
|
|
|
|
qWarning().noquote() << "[Trial] trial response does not contain config field";
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key.replace(QStringLiteral("vpn://"), QString());
|
|
|
|
|
QByteArray configBytes = QByteArray::fromBase64(key.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
|
|
|
|
QByteArray uncompressed = qUncompress(configBytes);
|
|
|
|
|
if (!uncompressed.isEmpty()) {
|
|
|
|
|
configBytes = uncompressed;
|
|
|
|
|
}
|
|
|
|
|
if (configBytes.isEmpty()) {
|
|
|
|
|
qWarning().noquote() << "[Trial] trial response config payload is empty";
|
|
|
|
|
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
|
|
|
|
|
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
|
|
|
|
configObject.insert(config_key::crc, crc);
|
|
|
|
|
m_serversModel->addServer(configObject);
|
|
|
|
|
|
|
|
|
|
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
|
|
|
|
bool reloadServiceConfig)
|
|
|
|
|
{
|
|
|
|
|
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
|
|
|
|
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-11-03 10:26:22 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-07-03 09:58:23 +08:00
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
apiConfig.value(configKey::userCountryCode).toString(),
|
|
|
|
|
newCountryCode,
|
|
|
|
|
apiConfig.value(configKey::serviceType).toString(),
|
|
|
|
|
apiConfig.value(configKey::serviceProtocol).toString(),
|
2025-07-08 15:14:00 +08:00
|
|
|
serverConfig.value(configKey::authData).toObject(),
|
|
|
|
|
QString(APPLICATION_NAME) };
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
|
|
|
|
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-09-30 12:10:27 +08:00
|
|
|
if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) {
|
|
|
|
|
apiPayload.insert(configKey::isConnectEvent, true);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
2026-03-25 14:48:32 +03:00
|
|
|
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
|
2025-02-15 11:50:42 +07:00
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
QJsonObject newServerConfig;
|
|
|
|
|
if (errorCode == ErrorCode::NoError) {
|
2025-07-03 09:58:23 +08:00
|
|
|
errorCode = fillServerConfig(gatewayRequestData.serviceProtocol, protocolData, responseBody, newServerConfig);
|
|
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject();
|
|
|
|
|
newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode));
|
|
|
|
|
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
|
|
|
|
|
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
|
2025-02-19 14:56:53 +07:00
|
|
|
newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey));
|
2026-04-08 11:21:12 +07:00
|
|
|
if (apiConfig.contains(apiDefs::key::isInAppPurchase)) {
|
|
|
|
|
newApiConfig.insert(apiDefs::key::isInAppPurchase, apiConfig.value(apiDefs::key::isInAppPurchase));
|
|
|
|
|
}
|
|
|
|
|
if (apiConfig.contains(apiDefs::key::isTestPurchase)) {
|
|
|
|
|
newApiConfig.insert(apiDefs::key::isTestPurchase, apiConfig.value(apiDefs::key::isTestPurchase));
|
|
|
|
|
}
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
2025-07-03 09:58:23 +08:00
|
|
|
newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
|
2025-09-03 06:54:11 +03:00
|
|
|
newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc));
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-06-16 18:01:46 +04:00
|
|
|
if (serverConfig.value(config_key::nameOverriddenByUser).toBool()) {
|
|
|
|
|
newServerConfig.insert(config_key::name, serverConfig.value(config_key::name));
|
|
|
|
|
newServerConfig.insert(config_key::nameOverriddenByUser, true);
|
|
|
|
|
}
|
2025-02-15 11:50:42 +07:00
|
|
|
m_serversModel->editServer(newServerConfig, serverIndex);
|
2026-03-25 14:48:32 +03:00
|
|
|
|
|
|
|
|
if (wasSubscriptionExpired) {
|
|
|
|
|
emit subscriptionRefreshNeeded();
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
if (reloadServiceConfig) {
|
|
|
|
|
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
|
|
|
|
} else if (newCountryName.isEmpty()) {
|
|
|
|
|
emit updateServerFromApiFinished();
|
|
|
|
|
} else {
|
|
|
|
|
emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName));
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-03-24 17:45:02 +03:00
|
|
|
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
2026-04-08 11:21:12 +07:00
|
|
|
if (!apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false)) {
|
|
|
|
|
apiConfig.insert(apiDefs::key::subscriptionExpiredByServer, true);
|
|
|
|
|
serverConfig.insert(configKey::apiConfig, apiConfig);
|
|
|
|
|
m_serversModel->editServer(serverConfig, serverIndex);
|
|
|
|
|
emit subscriptionExpiredOnServer();
|
|
|
|
|
} else {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
}
|
2026-03-24 17:45:02 +03:00
|
|
|
} else {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
}
|
2025-02-15 11:50:42 +07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
|
|
|
|
{
|
|
|
|
|
#ifdef Q_OS_IOS
|
|
|
|
|
IosController::Instance()->requestInetAccess();
|
|
|
|
|
QThread::msleep(10);
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-05-15 21:34:48 +08:00
|
|
|
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
|
|
|
|
m_settings->isStrictKillSwitchEnabled());
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-03-21 10:25:44 +07:00
|
|
|
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
|
|
|
|
auto installationUuid = m_settings->getInstallationUuid(true);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-03-21 10:25:44 +07:00
|
|
|
QString serviceProtocol = serverConfig.value(configKey::protocol).toString();
|
2025-07-03 09:58:23 +08:00
|
|
|
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload;
|
|
|
|
|
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
2025-03-21 10:25:44 +07:00
|
|
|
apiPayload[configKey::uuid] = installationUuid;
|
2025-07-03 09:58:23 +08:00
|
|
|
apiPayload[configKey::osVersion] = QSysInfo::productType();
|
|
|
|
|
apiPayload[configKey::appVersion] = QString(APP_VERSION);
|
2025-03-21 10:25:44 +07:00
|
|
|
apiPayload[configKey::accessToken] = serverConfig.value(configKey::accessToken).toString();
|
|
|
|
|
apiPayload[configKey::apiEndpoint] = serverConfig.value(configKey::apiEndpoint).toString();
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-03-21 10:25:44 +07:00
|
|
|
QByteArray responseBody;
|
|
|
|
|
ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody);
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-03-21 10:25:44 +07:00
|
|
|
if (errorCode == ErrorCode::NoError) {
|
2025-07-03 09:58:23 +08:00
|
|
|
errorCode = fillServerConfig(serviceProtocol, protocolData, responseBody, serverConfig);
|
|
|
|
|
if (errorCode != ErrorCode::NoError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-02-15 11:50:42 +07:00
|
|
|
|
|
|
|
|
m_serversModel->editServer(serverConfig, serverIndex);
|
|
|
|
|
emit updateServerFromApiFinished();
|
2025-03-21 10:25:44 +07:00
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
2025-02-15 11:50:42 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 14:45:12 +08:00
|
|
|
bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
|
2025-02-20 13:44:19 +07:00
|
|
|
{
|
2025-02-23 14:26:04 +07:00
|
|
|
auto serverIndex = m_serversModel->getProcessedServerIndex();
|
|
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
|
2025-02-20 13:44:19 +07:00
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
|
|
|
|
|
2025-04-16 09:58:44 +07:00
|
|
|
if (!apiUtils::isPremiumServer(serverConfigObject)) {
|
2025-02-23 14:26:04 +07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-11-03 10:26:22 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-07-03 09:58:23 +08:00
|
|
|
m_settings->getInstallationUuid(true),
|
|
|
|
|
apiConfigObject.value(configKey::userCountryCode).toString(),
|
|
|
|
|
apiConfigObject.value(configKey::serverCountryCode).toString(),
|
|
|
|
|
apiConfigObject.value(configKey::serviceType).toString(),
|
|
|
|
|
"",
|
2025-07-08 15:14:00 +08:00
|
|
|
serverConfigObject.value(configKey::authData).toObject(),
|
|
|
|
|
QString(APPLICATION_NAME) };
|
2025-02-20 13:44:19 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
2025-12-29 19:18:03 +08:00
|
|
|
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
2025-02-20 13:44:19 +07:00
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
|
|
|
|
|
|
2025-02-23 14:26:04 +07:00
|
|
|
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
|
2025-02-20 13:44:19 +07:00
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-02-23 14:26:04 +07:00
|
|
|
|
|
|
|
|
serverConfigObject.remove(config_key::containers);
|
|
|
|
|
m_serversModel->editServer(serverConfigObject, serverIndex);
|
|
|
|
|
|
2025-02-20 13:44:19 +07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-28 18:17:43 +03:00
|
|
|
bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode)
|
|
|
|
|
{
|
|
|
|
|
auto serverIndex = m_serversModel->getProcessedServerIndex();
|
|
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
|
|
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
|
|
|
|
|
2025-04-16 09:58:44 +07:00
|
|
|
if (!apiUtils::isPremiumServer(serverConfigObject)) {
|
2025-02-28 18:17:43 +03:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
|
|
|
|
QString(APP_VERSION),
|
2025-11-03 10:26:22 +08:00
|
|
|
m_settings->getAppLanguage().name().split("_").first(),
|
2025-07-03 09:58:23 +08:00
|
|
|
uuid,
|
|
|
|
|
apiConfigObject.value(configKey::userCountryCode).toString(),
|
|
|
|
|
serverCountryCode,
|
|
|
|
|
apiConfigObject.value(configKey::serviceType).toString(),
|
|
|
|
|
"",
|
2025-07-04 11:39:10 +08:00
|
|
|
serverConfigObject.value(configKey::authData).toObject(),
|
|
|
|
|
QString(APPLICATION_NAME) };
|
2025-02-28 18:17:43 +03:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
2025-12-29 19:18:03 +08:00
|
|
|
bool isTestPurchase = apiConfigObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
2025-02-28 18:17:43 +03:00
|
|
|
QByteArray responseBody;
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_config"), apiPayload, responseBody, isTestPurchase);
|
2025-02-28 18:17:43 +03:00
|
|
|
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
|
|
|
|
|
emit errorOccurred(errorCode);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (uuid == m_settings->getInstallationUuid(true)) {
|
|
|
|
|
serverConfigObject.remove(config_key::containers);
|
|
|
|
|
m_serversModel->editServer(serverConfigObject, serverIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:50:42 +07:00
|
|
|
bool ApiConfigsController::isConfigValid()
|
|
|
|
|
{
|
|
|
|
|
int serverIndex = m_serversModel->getDefaultServerIndex();
|
|
|
|
|
QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex);
|
|
|
|
|
auto configSource = apiUtils::getConfigSource(serverConfigObject);
|
|
|
|
|
|
|
|
|
|
if (configSource == apiDefs::ConfigSource::Telegram
|
|
|
|
|
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
|
|
|
|
m_serversModel->removeApiConfig(serverIndex);
|
|
|
|
|
return updateServiceFromTelegram(serverIndex);
|
|
|
|
|
} else if (configSource == apiDefs::ConfigSource::AmneziaGateway
|
|
|
|
|
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
|
|
|
|
return updateServiceFromGateway(serverIndex, "", "");
|
|
|
|
|
} else if (configSource && m_serversModel->isApiKeyExpired(serverIndex)) {
|
|
|
|
|
qDebug() << "attempt to update api config by expires_at event";
|
2025-03-04 13:33:35 +07:00
|
|
|
if (configSource == apiDefs::ConfigSource::AmneziaGateway) {
|
2025-02-15 11:50:42 +07:00
|
|
|
return updateServiceFromGateway(serverIndex, "", "");
|
|
|
|
|
} else {
|
|
|
|
|
m_serversModel->removeApiConfig(serverIndex);
|
|
|
|
|
return updateServiceFromTelegram(serverIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
void ApiConfigsController::setCurrentProtocol(const QString &protocolName)
|
2025-02-07 22:22:14 +07:00
|
|
|
{
|
2025-07-03 09:58:23 +08:00
|
|
|
auto serverIndex = m_serversModel->getProcessedServerIndex();
|
|
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
|
|
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
apiConfigObject[configKey::serviceProtocol] = protocolName;
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
serverConfigObject.insert(configKey::apiConfig, apiConfigObject);
|
2025-02-07 22:22:14 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
m_serversModel->editServer(serverConfigObject, serverIndex);
|
2025-02-07 22:22:14 +07:00
|
|
|
}
|
2025-02-12 12:43:11 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
bool ApiConfigsController::isVlessProtocol()
|
2025-02-15 11:50:42 +07:00
|
|
|
{
|
2025-07-03 09:58:23 +08:00
|
|
|
auto serverIndex = m_serversModel->getProcessedServerIndex();
|
|
|
|
|
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
|
|
|
|
|
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
2025-02-15 11:50:42 +07:00
|
|
|
|
2025-07-03 09:58:23 +08:00
|
|
|
if (apiConfigObject[configKey::serviceProtocol].toString() == "vless") {
|
|
|
|
|
return true;
|
2025-02-15 11:50:42 +07:00
|
|
|
}
|
2025-07-03 09:58:23 +08:00
|
|
|
return false;
|
2025-02-15 11:50:42 +07:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 12:43:11 +07:00
|
|
|
QList<QString> ApiConfigsController::getQrCodes()
|
|
|
|
|
{
|
|
|
|
|
return m_qrCodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ApiConfigsController::getQrCodesCount()
|
|
|
|
|
{
|
2025-12-18 16:36:12 +02:00
|
|
|
return static_cast<int>(m_qrCodes.size());
|
2025-02-12 12:43:11 +07:00
|
|
|
}
|
2025-02-22 14:42:09 +07:00
|
|
|
|
|
|
|
|
QString ApiConfigsController::getVpnKey()
|
|
|
|
|
{
|
|
|
|
|
return m_vpnKey;
|
|
|
|
|
}
|
2025-07-03 09:58:23 +08:00
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase,
|
|
|
|
|
int &duplicateServerIndex)
|
2025-07-03 09:58:23 +08:00
|
|
|
{
|
2026-04-08 11:21:12 +07:00
|
|
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
|
|
|
|
duplicateServerIndex = -1;
|
2025-12-29 19:18:03 +08:00
|
|
|
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
2026-04-08 11:21:12 +07:00
|
|
|
const QString rawVpnKey = responseObject.value(QStringLiteral("key")).toString();
|
|
|
|
|
if (rawVpnKey.isEmpty()) {
|
2025-12-18 16:36:12 +02:00
|
|
|
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
2025-12-29 19:18:03 +08:00
|
|
|
return ErrorCode::ApiPurchaseError;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
QString normalizedVpnKey = rawVpnKey;
|
|
|
|
|
normalizedVpnKey.replace(QStringLiteral("vpn://"), QString());
|
|
|
|
|
|
|
|
|
|
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
|
|
|
|
if (duplicateServerIndex >= 0) {
|
2025-12-18 16:36:12 +02:00
|
|
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
2025-12-29 19:18:03 +08:00
|
|
|
return ErrorCode::ApiConfigAlreadyAdded;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
QByteArray configPayload =
|
|
|
|
|
QByteArray::fromBase64(normalizedVpnKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
|
|
|
|
QByteArray configUncompressed = qUncompress(configPayload);
|
|
|
|
|
const bool payloadWasCompressed = !configUncompressed.isEmpty();
|
|
|
|
|
if (payloadWasCompressed) {
|
|
|
|
|
configPayload = configUncompressed;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
if (configPayload.isEmpty()) {
|
2025-12-29 19:18:03 +08:00
|
|
|
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
|
|
|
|
return ErrorCode::ApiPurchaseError;
|
2025-12-18 16:36:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
QJsonObject configObject = QJsonDocument::fromJson(configPayload).object();
|
2025-12-18 16:36:12 +02:00
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
2026-04-08 11:21:12 +07:00
|
|
|
apiConfig.insert(apiDefs::key::isTestPurchase, isTestPurchase);
|
|
|
|
|
apiConfig.insert(apiDefs::key::isInAppPurchase, true);
|
|
|
|
|
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
|
|
|
|
|
|
|
|
|
configPayload = QJsonDocument(configObject).toJson();
|
|
|
|
|
if (payloadWasCompressed) {
|
|
|
|
|
configPayload = qCompress(configPayload, 8);
|
|
|
|
|
}
|
|
|
|
|
normalizedVpnKey = QString(configPayload.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
2025-12-29 19:18:03 +08:00
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
|
|
|
|
if (duplicateServerIndex >= 0) {
|
|
|
|
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
|
|
|
|
return ErrorCode::ApiConfigAlreadyAdded;
|
|
|
|
|
}
|
2025-12-29 19:18:03 +08:00
|
|
|
|
2026-04-08 11:21:12 +07:00
|
|
|
apiConfig.insert(apiDefs::key::vpnKey, normalizedVpnKey);
|
2025-12-29 19:18:03 +08:00
|
|
|
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
2026-04-08 11:21:12 +07:00
|
|
|
|
|
|
|
|
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
2025-12-29 19:18:03 +08:00
|
|
|
configObject.insert(config_key::crc, crc);
|
|
|
|
|
m_serversModel->addServer(configObject);
|
|
|
|
|
|
|
|
|
|
return ErrorCode::NoError;
|
2025-12-18 16:36:12 +02:00
|
|
|
#else
|
|
|
|
|
Q_UNUSED(responseBody)
|
2025-12-29 19:18:03 +08:00
|
|
|
Q_UNUSED(isTestPurchase)
|
2026-04-08 11:21:12 +07:00
|
|
|
duplicateServerIndex = -1;
|
2025-12-29 19:18:03 +08:00
|
|
|
return ErrorCode::NoError;
|
2025-12-18 16:36:12 +02:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 19:18:03 +08:00
|
|
|
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
|
|
|
|
|
bool isTestPurchase)
|
2025-07-03 09:58:23 +08:00
|
|
|
{
|
2025-12-29 19:18:03 +08:00
|
|
|
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
|
|
|
|
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
2025-07-03 09:58:23 +08:00
|
|
|
return gatewayController.post(endpoint, apiPayload, responseBody);
|
|
|
|
|
}
|