mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
feat: new services description (#2412)
* feat: iap for apple now use storekit2 * fix: fixed error 101 on connection event * feat: enhance StoreKit2Helper to handle entitlements and improve restore service from App Store functionality * chore: add isInAppPurchase and isTestPurchase in primary config * refactor: use end_date from primary config for renew ui * fix: hide renew button for free * fix: hide renew button for appstore purchases * feat: add new premium info page * feat: add new free info page * chore: minor fixes * refactor: move plan and benefits into separate models * fix: fixed expired status when configs without an end date * feat: add trial api support * chore: add api message parsing for 422 error * feat: move privacy policy and term of use to gateway * feat: add iap support for new premium info page * chore: minor fixes * chore: minor fix * chore: minor fixes * feat: additional parsing for storekit subscription plans * chore: minor codestyle fixes * chore: simplify benefits * chore: hide extend buttons on external premium * feat: add trial error processing * fix: remove wrong check from tiral handler * chore: cleanup --------- Co-authored-by: spectrum <yyy@amnezia.org>
This commit is contained in:
@@ -9,9 +9,14 @@
|
||||
#include "ui/controllers/systemController.h"
|
||||
#include "version.h"
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QSet>
|
||||
#include <QVariantMap>
|
||||
#include <limits>
|
||||
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
|
||||
@@ -39,6 +44,15 @@ namespace
|
||||
constexpr char serviceInfo[] = "service_info";
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
|
||||
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";
|
||||
|
||||
constexpr char apiPayload[] = "api_payload";
|
||||
constexpr char keyPayload[] = "key_payload";
|
||||
|
||||
@@ -47,9 +61,6 @@ namespace
|
||||
|
||||
constexpr char config[] = "config";
|
||||
|
||||
constexpr char subscription[] = "subscription";
|
||||
constexpr char endDate[] = "end_date";
|
||||
|
||||
constexpr char isConnectEvent[] = "is_connect_event";
|
||||
}
|
||||
|
||||
@@ -241,13 +252,190 @@ namespace
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
|
||||
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
|
||||
const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
||||
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
|
||||
const QSharedPointer<ApiBenefitsModel> &benefitsModel,
|
||||
const std::shared_ptr<Settings> &settings, QObject *parent)
|
||||
: QObject(parent), m_serversModel(serversModel), m_apiServicesModel(apiServicesModel), m_settings(settings)
|
||||
: QObject(parent)
|
||||
, m_serversModel(serversModel)
|
||||
, m_apiServicesModel(apiServicesModel)
|
||||
, m_subscriptionPlansModel(subscriptionPlansModel)
|
||||
, m_benefitsModel(benefitsModel)
|
||||
, m_settings(settings)
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
bool ApiConfigsController::exportVpnKey(const QString &fileName)
|
||||
@@ -384,51 +572,11 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
}
|
||||
|
||||
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
|
||||
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
QEventLoop waitProducts;
|
||||
bool productsFetched = false;
|
||||
QString productPrice;
|
||||
QString productCurrency;
|
||||
|
||||
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
|
||||
[&](const QList<QVariantMap> &products,
|
||||
const QStringList &invalidIds,
|
||||
const QString &errorString) {
|
||||
if (!errorString.isEmpty() || products.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
|
||||
} else {
|
||||
const auto &product = products.first();
|
||||
productPrice = product.value("price").toString();
|
||||
productCurrency = product.value("currencyCode").toString();
|
||||
productsFetched = true;
|
||||
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
|
||||
}
|
||||
waitProducts.quit();
|
||||
});
|
||||
waitProducts.exec();
|
||||
|
||||
if (productsFetched && !productPrice.isEmpty()) {
|
||||
QJsonArray services = data.value("services").toArray();
|
||||
for (int i = 0; i < services.size(); ++i) {
|
||||
QJsonObject service = services[i].toObject();
|
||||
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
|
||||
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
||||
QString formattedPrice = productPrice;
|
||||
if (!productCurrency.isEmpty()) {
|
||||
formattedPrice += " " + productCurrency;
|
||||
}
|
||||
serviceInfo["price"] = formattedPrice;
|
||||
service[configKey::serviceInfo] = serviceInfo;
|
||||
services[i] = service;
|
||||
data["services"] = services;
|
||||
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
mergeStoreKitPricesIntoPremiumPlans(data);
|
||||
#endif
|
||||
|
||||
|
||||
m_apiServicesModel->updateModel(data);
|
||||
if (m_apiServicesModel->rowCount() > 0) {
|
||||
m_apiServicesModel->setServiceIndex(0);
|
||||
@@ -439,39 +587,42 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
bool ApiConfigsController::importService()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
bool isIosOrMacOsNe = true;
|
||||
const bool isIosOrMacOsNe = true;
|
||||
#else
|
||||
bool isIosOrMacOsNe = false;
|
||||
const bool isIosOrMacOsNe = false;
|
||||
#endif
|
||||
|
||||
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
||||
if (isIosOrMacOsNe) {
|
||||
importSerivceFromAppStore();
|
||||
return true;
|
||||
return importPremiumFromAppStore(QString());
|
||||
}
|
||||
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
||||
importServiceFromGateway();
|
||||
return true;
|
||||
return importFreeFromGateway();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::importSerivceFromAppStore()
|
||||
bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProductId)
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
QString productId = storeProductId.trimmed();
|
||||
if (productId.isEmpty()) {
|
||||
productId = QStringLiteral("amnezia_premium_6_month");
|
||||
}
|
||||
|
||||
bool purchaseOk = false;
|
||||
QString originalTransactionId;
|
||||
QString storeTransactionId;
|
||||
QString storeProductId;
|
||||
QString purchasedStoreProductId;
|
||||
QString purchaseError;
|
||||
QEventLoop waitPurchase;
|
||||
IosController::Instance()->purchaseProduct(QStringLiteral("amnezia_premium_6_month"),
|
||||
[&](bool success, const QString &txId, const QString &purchasedProductId,
|
||||
const QString &originalTxId, const QString &errorString) {
|
||||
IosController::Instance()->purchaseProduct(productId,
|
||||
[&](bool success, const QString &transactionId, const QString &purchasedProductId,
|
||||
const QString &originalTransactionIdResponse, const QString &errorString) {
|
||||
purchaseOk = success;
|
||||
originalTransactionId = originalTxId;
|
||||
storeTransactionId = txId;
|
||||
storeProductId = purchasedProductId;
|
||||
originalTransactionId = originalTransactionIdResponse;
|
||||
storeTransactionId = transactionId;
|
||||
purchasedStoreProductId = purchasedProductId;
|
||||
purchaseError = errorString;
|
||||
waitPurchase.quit();
|
||||
});
|
||||
@@ -483,7 +634,7 @@ bool ApiConfigsController::importSerivceFromAppStore()
|
||||
return false;
|
||||
}
|
||||
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << purchasedStoreProductId;
|
||||
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -507,18 +658,26 @@ bool ApiConfigsController::importSerivceFromAppStore()
|
||||
return false;
|
||||
}
|
||||
|
||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
|
||||
int duplicateServerIndex = -1;
|
||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
|
||||
return true;
|
||||
}
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
#endif
|
||||
emit installServerFromApiFinished(
|
||||
tr("%1 was added to the app.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return true;
|
||||
#else
|
||||
Q_UNUSED(storeProductId);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
bool ApiConfigsController::restoreServiceFromAppStore()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
||||
@@ -534,20 +693,12 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we have a valid premium selection for gateway requests
|
||||
bool premiumSelected = false;
|
||||
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
|
||||
m_apiServicesModel->setServiceIndex(i);
|
||||
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
|
||||
premiumSelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!premiumSelected) {
|
||||
const int premiumServiceIndex = m_apiServicesModel->serviceIndexForType(premiumServiceType);
|
||||
if (premiumServiceIndex < 0) {
|
||||
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
||||
return false;
|
||||
}
|
||||
m_apiServicesModel->setServiceIndex(premiumServiceIndex);
|
||||
|
||||
bool restoreSuccess = false;
|
||||
QList<QVariantMap> restoredTransactions;
|
||||
@@ -569,15 +720,23 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
}
|
||||
|
||||
if (restoredTransactions.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] Restore completed, but no transactions were returned";
|
||||
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
||||
qInfo().noquote() << "[IAP] Restore completed, but no active entitlements found";
|
||||
emit errorOccurred(ErrorCode::ApiNoPurchasedSubscriptionsError);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
bool hasInstalledConfig = false;
|
||||
bool duplicateConfigAlreadyPresent = false;
|
||||
int duplicateCount = 0;
|
||||
QSet<QString> processedTransactions;
|
||||
int duplicateServerIndex = -1;
|
||||
QSet<QString> processedOriginalTransactionIds;
|
||||
|
||||
for (const QVariantMap &transaction : restoredTransactions) {
|
||||
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
||||
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
||||
@@ -588,28 +747,28 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedTransactions.contains(originalTransactionId)) {
|
||||
duplicateCount++;
|
||||
if (processedOriginalTransactionIds.contains(originalTransactionId)) {
|
||||
qInfo().noquote() << "[IAP] Skipping duplicate restored transaction" << originalTransactionId;
|
||||
continue;
|
||||
}
|
||||
processedTransactions.insert(originalTransactionId);
|
||||
processedOriginalTransactionIds.insert(originalTransactionId);
|
||||
|
||||
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
|
||||
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_settings->getAppLanguage().name().split("_").first(),
|
||||
m_settings->getInstallationUuid(true),
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
appLanguage,
|
||||
installationUuid,
|
||||
countryCode,
|
||||
"",
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject() };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
||||
auto isTestPurchase = IosController::Instance()->isTestFlight();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
@@ -618,34 +777,42 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
continue;
|
||||
}
|
||||
|
||||
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
|
||||
int currentDuplicateServerIndex = -1;
|
||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase, currentDuplicateServerIndex);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
hasInstalledConfig = true;
|
||||
}
|
||||
|
||||
if (!hasInstalledConfig) {
|
||||
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
||||
emit errorOccurred(restoreError);
|
||||
if (duplicateConfigAlreadyPresent) {
|
||||
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
||||
if (duplicateCount > 0) {
|
||||
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
|
||||
<< "duplicate restored transactions for original transaction IDs already processed";
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::importServiceFromGateway()
|
||||
bool ApiConfigsController::importFreeFromGateway()
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -697,6 +864,72 @@ bool ApiConfigsController::importServiceFromGateway()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
emit trialEmailError(tr("This email has already been used for trial activation. If you like the service, you can buy Premium."));
|
||||
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;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig)
|
||||
{
|
||||
@@ -740,6 +973,12 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
||||
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
|
||||
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
|
||||
newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey));
|
||||
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));
|
||||
}
|
||||
|
||||
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
||||
newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
|
||||
@@ -765,7 +1004,14 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
||||
return true;
|
||||
} else {
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
@@ -954,43 +1200,63 @@ QString ApiConfigsController::getVpnKey()
|
||||
return m_vpnKey;
|
||||
}
|
||||
|
||||
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
|
||||
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase,
|
||||
int &duplicateServerIndex)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
duplicateServerIndex = -1;
|
||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
||||
QString key = responseObject.value(QStringLiteral("key")).toString();
|
||||
if (key.isEmpty()) {
|
||||
const QString rawVpnKey = responseObject.value(QStringLiteral("key")).toString();
|
||||
if (rawVpnKey.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
if (m_serversModel->hasServerWithVpnKey(key)) {
|
||||
QString normalizedVpnKey = rawVpnKey;
|
||||
normalizedVpnKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
||||
if (duplicateServerIndex >= 0) {
|
||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||
return ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
|
||||
QString normalizedKey = key;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray configUncompressed = qUncompress(configString);
|
||||
if (!configUncompressed.isEmpty()) {
|
||||
configString = configUncompressed;
|
||||
QByteArray configPayload =
|
||||
QByteArray::fromBase64(normalizedVpnKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray configUncompressed = qUncompress(configPayload);
|
||||
const bool payloadWasCompressed = !configUncompressed.isEmpty();
|
||||
if (payloadWasCompressed) {
|
||||
configPayload = configUncompressed;
|
||||
}
|
||||
|
||||
if (configString.isEmpty()) {
|
||||
if (configPayload.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configPayload).object();
|
||||
|
||||
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
||||
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));
|
||||
|
||||
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
||||
if (duplicateServerIndex >= 0) {
|
||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||
return ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
|
||||
apiConfig.insert(apiDefs::key::vpnKey, normalizedVpnKey);
|
||||
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
||||
|
||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
||||
apiConfig[apiDefs::key::vpnKey] = normalizedKey;
|
||||
apiConfig[apiDefs::key::isTestPurchase] = isTestPurchase;
|
||||
|
||||
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
||||
configObject.insert(config_key::crc, crc);
|
||||
m_serversModel->addServer(configObject);
|
||||
|
||||
@@ -998,6 +1264,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
||||
#else
|
||||
Q_UNUSED(responseBody)
|
||||
Q_UNUSED(isTestPurchase)
|
||||
duplicateServerIndex = -1;
|
||||
return ErrorCode::NoError;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#ifndef APICONFIGSCONTROLLER_H
|
||||
#define APICONFIGSCONTROLLER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
#include "configurators/openvpn_configurator.h"
|
||||
#include "ui/models/api/apiBenefitsModel.h"
|
||||
#include "ui/models/api/apiServicesModel.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
#include "ui/models/servers_model.h"
|
||||
|
||||
class ApiConfigsController : public QObject
|
||||
@@ -12,7 +15,9 @@ class ApiConfigsController : public QObject
|
||||
Q_OBJECT
|
||||
public:
|
||||
ApiConfigsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiServicesModel> &apiServicesModel,
|
||||
const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
|
||||
const QSharedPointer<ApiSubscriptionPlansModel> &subscriptionPlansModel,
|
||||
const QSharedPointer<ApiBenefitsModel> &benefitsModel, const std::shared_ptr<Settings> &settings,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
|
||||
Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady)
|
||||
@@ -27,9 +32,10 @@ public slots:
|
||||
|
||||
bool fillAvailableServices();
|
||||
bool importService();
|
||||
bool importSerivceFromAppStore();
|
||||
bool restoreSerivceFromAppStore();
|
||||
bool importServiceFromGateway();
|
||||
bool importPremiumFromAppStore(const QString &storeProductId);
|
||||
bool restoreServiceFromAppStore();
|
||||
bool importFreeFromGateway();
|
||||
bool importTrialFromGateway(const QString &email);
|
||||
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig = false);
|
||||
bool updateServiceFromTelegram(const int serverIndex);
|
||||
@@ -43,10 +49,11 @@ public slots:
|
||||
|
||||
signals:
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
void trialEmailError(const QString &message);
|
||||
void subscriptionExpiredOnServer();
|
||||
void subscriptionRefreshNeeded();
|
||||
|
||||
void installServerFromApiFinished(const QString &message);
|
||||
void installServerFromApiFinished(const QString &message, int preferredDefaultServerIndex = -1);
|
||||
void changeApiCountryFinished(const QString &message);
|
||||
void reloadServerFromApiFinished(const QString &message);
|
||||
void updateServerFromApiFinished();
|
||||
@@ -59,7 +66,7 @@ private:
|
||||
QString getVpnKey();
|
||||
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
|
||||
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase, int &duplicateServerIndex);
|
||||
|
||||
QList<QString> m_qrCodes;
|
||||
QString m_vpnKey;
|
||||
@@ -67,6 +74,9 @@ private:
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
|
||||
QSharedPointer<ApiSubscriptionPlansModel> m_subscriptionPlansModel;
|
||||
QSharedPointer<ApiBenefitsModel> m_benefitsModel;
|
||||
};
|
||||
|
||||
#endif // APICONFIGSCONTROLLER_H
|
||||
#endif
|
||||
|
||||
@@ -78,13 +78,6 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object();
|
||||
m_apiAccountInfoModel->updateModel(accountInfo, serverConfig);
|
||||
|
||||
QString subscriptionEndDate = accountInfo.value(apiDefs::key::subscriptionEndDate).toString();
|
||||
if (!subscriptionEndDate.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::subscriptionEndDate, subscriptionEndDate);
|
||||
serverConfig.insert(configKey::apiConfig, apiConfig);
|
||||
m_serversModel->editServer(serverConfig, processedIndex);
|
||||
}
|
||||
|
||||
if (reload) {
|
||||
updateApiCountryModel();
|
||||
updateApiDevicesModel();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
#include "amnezia_application.h"
|
||||
#include "utilities.h"
|
||||
#include "core/controllers/vpnConfigurationController.h"
|
||||
#include "version.h"
|
||||
@@ -81,6 +82,8 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
m_connectionStateText = tr("Connecting...");
|
||||
switch (state) {
|
||||
case Vpn::ConnectionState::Connected: {
|
||||
amnApp->networkManager()->clearConnectionCache();
|
||||
|
||||
m_isConnectionInProgress = false;
|
||||
m_isConnected = true;
|
||||
m_connectionStateText = tr("Connected");
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace PageLoader
|
||||
PageSetupWizardViewConfig,
|
||||
PageSetupWizardQrReader,
|
||||
PageSetupWizardApiServicesList,
|
||||
PageSetupWizardApiServiceInfo,
|
||||
PageSetupWizardApiFreeInfo,
|
||||
|
||||
PageProtocolOpenVpnSettings,
|
||||
PageProtocolShadowSocksSettings,
|
||||
@@ -76,6 +76,9 @@ namespace PageLoader
|
||||
PageShareFullAccess,
|
||||
PageShareConnection,
|
||||
|
||||
PageSetupWizardApiPremiumInfo,
|
||||
PageSetupWizardApiTrialEmail,
|
||||
|
||||
PageDevMenu
|
||||
};
|
||||
Q_ENUM_NS(PageEnum)
|
||||
|
||||
@@ -57,6 +57,11 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
case IsSubscriptionRenewalAvailableRole: {
|
||||
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
case HasExpiredWorkerRole: {
|
||||
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
|
||||
QJsonObject issuedConfigObject = m_issuedConfigsInfo.at(i).toObject();
|
||||
@@ -77,16 +82,31 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
return false;
|
||||
}
|
||||
case IsSubscriptionExpiredRole: {
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
|
||||
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
|
||||
return false;
|
||||
}
|
||||
if (m_accountInfoData.isInAppPurchase) {
|
||||
return false;
|
||||
}
|
||||
if (m_accountInfoData.subscriptionEndDate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate);
|
||||
}
|
||||
case IsSubscriptionExpiringSoonRole: {
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
|
||||
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
|
||||
if (apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate)) return false;
|
||||
QDateTime endDate = QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODateWithMs);
|
||||
return endDate <= QDateTime::currentDateTimeUtc().addDays(10);
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
|
||||
return false;
|
||||
}
|
||||
if (m_accountInfoData.isInAppPurchase) {
|
||||
return false;
|
||||
}
|
||||
if (m_accountInfoData.subscriptionEndDate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return apiUtils::isSubscriptionExpiringSoon(m_accountInfoData.subscriptionEndDate);
|
||||
}
|
||||
case IsInAppPurchaseRole: {
|
||||
return m_accountInfoData.isInAppPurchase;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +128,9 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
|
||||
|
||||
accountInfoData.configType = apiUtils::getConfigType(serverConfig);
|
||||
|
||||
const QJsonObject apiConfig = serverConfig.value(apiDefs::key::apiConfig).toObject();
|
||||
accountInfoData.isInAppPurchase = apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false);
|
||||
|
||||
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
|
||||
|
||||
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
|
||||
@@ -177,10 +200,12 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
|
||||
roles[ConnectedDevicesRole] = "connectedDevices";
|
||||
roles[ServiceDescriptionRole] = "serviceDescription";
|
||||
roles[IsComponentVisibleRole] = "isComponentVisible";
|
||||
roles[IsSubscriptionRenewalAvailableRole] = "isSubscriptionRenewalAvailable";
|
||||
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
|
||||
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
roles[IsInAppPurchaseRole] = "isInAppPurchase";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ public:
|
||||
ServiceDescriptionRole,
|
||||
EndDateRole,
|
||||
IsComponentVisibleRole,
|
||||
IsSubscriptionRenewalAvailableRole,
|
||||
HasExpiredWorkerRole,
|
||||
IsProtocolSelectionSupportedRole,
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
IsInAppPurchaseRole
|
||||
};
|
||||
|
||||
explicit ApiAccountInfoModel(QObject *parent = nullptr);
|
||||
@@ -57,6 +59,8 @@ private:
|
||||
QStringList supportedProtocols;
|
||||
|
||||
QString subscriptionDescription;
|
||||
|
||||
bool isInAppPurchase = false;
|
||||
};
|
||||
|
||||
AccountInfoData m_accountInfoData;
|
||||
|
||||
112
client/ui/models/api/apiBenefitsModel.cpp
Normal file
112
client/ui/models/api/apiBenefitsModel.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "apiBenefitsModel.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <utility>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace configKey
|
||||
{
|
||||
constexpr char title[] = "title";
|
||||
constexpr char body[] = "body";
|
||||
constexpr char icon[] = "icon";
|
||||
constexpr char accent[] = "accent";
|
||||
}
|
||||
|
||||
QString gatewayIconKeyToUrl(const QString &iconKey)
|
||||
{
|
||||
if (iconKey.startsWith(QLatin1String("qrc:"))) {
|
||||
return iconKey;
|
||||
}
|
||||
static const QHash<QString, QString> map = {
|
||||
{ QStringLiteral("globe-2"), QStringLiteral("qrc:/images/controls/globe-2.svg") },
|
||||
{ QStringLiteral("smartphone"), QStringLiteral("qrc:/images/controls/smartphone.svg") },
|
||||
{ QStringLiteral("gauge"), QStringLiteral("qrc:/images/controls/gauge.svg") },
|
||||
{ QStringLiteral("infinity"), QStringLiteral("qrc:/images/controls/infinity.svg") },
|
||||
{ QStringLiteral("tag"), QStringLiteral("qrc:/images/controls/tag.svg") },
|
||||
{ QStringLiteral("history"), QStringLiteral("qrc:/images/controls/history.svg") },
|
||||
{ QStringLiteral("info"), QStringLiteral("qrc:/images/controls/info.svg") },
|
||||
{ QStringLiteral("app"), QStringLiteral("qrc:/images/controls/app.svg") },
|
||||
{ QStringLiteral("download"), QStringLiteral("qrc:/images/controls/download.svg") },
|
||||
{ QStringLiteral("help-circle"), QStringLiteral("qrc:/images/controls/help-circle.svg") },
|
||||
};
|
||||
return map.value(iconKey, QStringLiteral("qrc:/images/controls/info.svg"));
|
||||
}
|
||||
}
|
||||
|
||||
ApiBenefitsModel::ApiBenefitsModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int ApiBenefitsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return m_serviceBenefits.size();
|
||||
}
|
||||
|
||||
QVariant ApiBenefitsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_serviceBenefits.size()) {
|
||||
return {};
|
||||
}
|
||||
const ServiceBenefitItem &item = m_serviceBenefits.at(index.row());
|
||||
switch (role) {
|
||||
case IconRole:
|
||||
return item.icon;
|
||||
case TitleRole:
|
||||
return item.title;
|
||||
case BodyRole:
|
||||
return item.body;
|
||||
case AccentRole:
|
||||
return item.accent;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ApiBenefitsModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ IconRole, "icon" },
|
||||
{ TitleRole, "title" },
|
||||
{ BodyRole, "body" },
|
||||
{ AccentRole, "accent" },
|
||||
};
|
||||
}
|
||||
|
||||
void ApiBenefitsModel::updateModel(const QJsonArray &benefits)
|
||||
{
|
||||
beginResetModel();
|
||||
m_serviceBenefits.clear();
|
||||
for (const QJsonValue &benefitValue : benefits) {
|
||||
if (!benefitValue.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject benefitObject = benefitValue.toObject();
|
||||
QString title = benefitObject.value(configKey::title).toString();
|
||||
QString body = benefitObject.value(configKey::body).toString();
|
||||
const QString iconKey = benefitObject.value(configKey::icon).toString();
|
||||
if (title.isEmpty() && body.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ServiceBenefitItem item;
|
||||
item.icon = gatewayIconKeyToUrl(iconKey);
|
||||
item.title = std::move(title);
|
||||
item.body = std::move(body);
|
||||
item.accent = benefitObject.value(configKey::accent).toBool();
|
||||
m_serviceBenefits.append(std::move(item));
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void ApiBenefitsModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_serviceBenefits.clear();
|
||||
endResetModel();
|
||||
}
|
||||
43
client/ui/models/api/apiBenefitsModel.h
Normal file
43
client/ui/models/api/apiBenefitsModel.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef APIBENEFITSMODEL_H
|
||||
#define APIBENEFITSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class ApiBenefitsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
IconRole = Qt::UserRole + 1,
|
||||
TitleRole,
|
||||
BodyRole,
|
||||
AccentRole
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit ApiBenefitsModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void updateModel(const QJsonArray &benefits);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
struct ServiceBenefitItem
|
||||
{
|
||||
QString icon;
|
||||
QString title;
|
||||
QString body;
|
||||
bool accent = false;
|
||||
};
|
||||
|
||||
QVector<ServiceBenefitItem> m_serviceBenefits;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,7 +1,11 @@
|
||||
#include "apiServicesModel.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/api/apiDefs.h"
|
||||
#include "logger.h"
|
||||
|
||||
namespace
|
||||
@@ -17,15 +21,9 @@ namespace
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
constexpr char serviceDescription[] = "service_description";
|
||||
|
||||
constexpr char name[] = "name";
|
||||
constexpr char price[] = "price";
|
||||
constexpr char speed[] = "speed";
|
||||
constexpr char timelimit[] = "timelimit";
|
||||
constexpr char region[] = "region";
|
||||
|
||||
constexpr char description[] = "description";
|
||||
constexpr char cardDescription[] = "card_description";
|
||||
constexpr char features[] = "features";
|
||||
constexpr char serviceName[] = "service_name";
|
||||
|
||||
constexpr char availableCountries[] = "available_countries";
|
||||
|
||||
@@ -33,19 +31,21 @@ namespace
|
||||
|
||||
constexpr char isAvailable[] = "is_available";
|
||||
|
||||
constexpr char subscription[] = "subscription";
|
||||
constexpr char endDate[] = "end_date";
|
||||
constexpr char subscriptionPlans[] = "subscription_plans";
|
||||
constexpr char minPriceLabel[] = "min_price_label";
|
||||
constexpr char benefits[] = "benefits";
|
||||
}
|
||||
|
||||
namespace serviceType
|
||||
{
|
||||
constexpr char amneziaFree[] = "amnezia-free";
|
||||
constexpr char amneziaPremium[] = "amnezia-premium";
|
||||
constexpr char amneziaTrial[] = "amnezia-trial";
|
||||
}
|
||||
}
|
||||
|
||||
ApiServicesModel::ApiServicesModel(QObject *parent) : QAbstractListModel(parent)
|
||||
ApiServicesModel::ApiServicesModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_selectedServiceIndex(0)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -69,9 +69,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
return apiServiceData.serviceInfo.name;
|
||||
}
|
||||
case CardDescriptionRole: {
|
||||
auto speed = apiServiceData.serviceInfo.speed;
|
||||
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
|
||||
return apiServiceData.serviceInfo.cardDescription.arg(speed);
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
return apiServiceData.serviceInfo.cardDescription;
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
QString description = apiServiceData.serviceInfo.cardDescription;
|
||||
if (!isServiceAvailable) {
|
||||
@@ -92,44 +91,29 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case SpeedRole: {
|
||||
return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
|
||||
}
|
||||
case TimeLimitRole: {
|
||||
auto timeLimit = apiServiceData.serviceInfo.timeLimit;
|
||||
if (timeLimit == "0") {
|
||||
return "";
|
||||
}
|
||||
return tr("%1 days").arg(timeLimit);
|
||||
}
|
||||
case RegionRole: {
|
||||
return apiServiceData.serviceInfo.region;
|
||||
}
|
||||
case FeaturesRole: {
|
||||
return apiServiceData.serviceInfo.features;
|
||||
}
|
||||
case PriceRole: {
|
||||
auto price = apiServiceData.serviceInfo.price;
|
||||
if (price == "free") {
|
||||
return tr("Free");
|
||||
}
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
return tr("%1 $").arg(price);
|
||||
#else
|
||||
return tr("%1 $/month").arg(price);
|
||||
#endif
|
||||
return apiServiceData.minPriceLabel;
|
||||
}
|
||||
case EndDateRole: {
|
||||
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
|
||||
}
|
||||
case TermsOfUseUrlRole: {
|
||||
return apiServiceData.serviceInfo.termsOfUseUrl;
|
||||
}
|
||||
case PrivacyPolicyUrlRole: {
|
||||
return apiServiceData.serviceInfo.privacyPolicyUrl;
|
||||
}
|
||||
case ShowRecommendedRole: {
|
||||
return serviceType == serviceType::amneziaPremium;
|
||||
}
|
||||
case OrderRole: {
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
return 0;
|
||||
} else if (serviceType == serviceType::amneziaTrial) {
|
||||
return 1;
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
return 2;
|
||||
}
|
||||
if (serviceType == serviceType::amneziaFree) {
|
||||
return 1;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,12 +139,27 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_services.isEmpty() && m_selectedServiceIndex >= m_services.size()) {
|
||||
m_selectedServiceIndex = 0;
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
|
||||
emit serviceSelectionChanged();
|
||||
}
|
||||
|
||||
void ApiServicesModel::setServiceIndex(const int index)
|
||||
{
|
||||
m_selectedServiceIndex = index;
|
||||
emit serviceSelectionChanged();
|
||||
}
|
||||
|
||||
ApiServicesModel::ApiServicesData ApiServicesModel::selectedServiceData() const
|
||||
{
|
||||
if (m_services.isEmpty() || m_selectedServiceIndex < 0 || m_selectedServiceIndex >= m_services.size()) {
|
||||
return {};
|
||||
}
|
||||
return m_services.at(m_selectedServiceIndex);
|
||||
}
|
||||
|
||||
QJsonObject ApiServicesModel::getSelectedServiceInfo()
|
||||
@@ -217,6 +216,16 @@ QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
|
||||
return {};
|
||||
}
|
||||
|
||||
int ApiServicesModel::serviceIndexForType(const QString &type) const
|
||||
{
|
||||
for (int serviceIndex = 0; serviceIndex < m_services.size(); ++serviceIndex) {
|
||||
if (m_services.at(serviceIndex).type == type) {
|
||||
return serviceIndex;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ApiServicesModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
@@ -224,12 +233,11 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
|
||||
roles[CardDescriptionRole] = "cardDescription";
|
||||
roles[ServiceDescriptionRole] = "serviceDescription";
|
||||
roles[IsServiceAvailableRole] = "isServiceAvailable";
|
||||
roles[SpeedRole] = "speed";
|
||||
roles[TimeLimitRole] = "timeLimit";
|
||||
roles[RegionRole] = "region";
|
||||
roles[FeaturesRole] = "features";
|
||||
roles[PriceRole] = "price";
|
||||
roles[EndDateRole] = "endDate";
|
||||
roles[TermsOfUseUrlRole] = "termsOfUseUrl";
|
||||
roles[PrivacyPolicyUrlRole] = "privacyPolicyUrl";
|
||||
roles[ShowRecommendedRole] = "showRecommended";
|
||||
roles[OrderRole] = "order";
|
||||
|
||||
return roles;
|
||||
@@ -243,18 +251,22 @@ ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJs
|
||||
auto availableCountries = data.value(configKey::availableCountries).toArray();
|
||||
auto serviceDescription = data.value(configKey::serviceDescription).toObject();
|
||||
|
||||
auto subscriptionObject = data.value(configKey::subscription).toObject();
|
||||
auto subscriptionObject = data.value(apiDefs::key::subscription).toObject();
|
||||
|
||||
ApiServicesData serviceData;
|
||||
serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString();
|
||||
serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString();
|
||||
serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString();
|
||||
serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
|
||||
serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
|
||||
serviceData.serviceInfo.name = serviceDescription.value(configKey::serviceName).toString();
|
||||
|
||||
serviceData.serviceInfo.cardDescription = serviceDescription.value(configKey::cardDescription).toString();
|
||||
serviceData.serviceInfo.description = serviceDescription.value(configKey::description).toString();
|
||||
serviceData.serviceInfo.features = serviceDescription.value(configKey::features).toString();
|
||||
serviceData.serviceInfo.termsOfUseUrl = serviceDescription.value(apiDefs::key::termsOfUseUrl).toString();
|
||||
serviceData.serviceInfo.privacyPolicyUrl = serviceDescription.value(apiDefs::key::privacyPolicyUrl).toString();
|
||||
|
||||
serviceData.subscriptionPlansJson = serviceDescription.value(configKey::subscriptionPlans).toArray();
|
||||
serviceData.benefits = serviceDescription.value(configKey::benefits).toArray();
|
||||
|
||||
serviceData.minPriceLabel = serviceDescription.value(configKey::minPriceLabel).toString().trimmed();
|
||||
|
||||
serviceData.supportInfo = data.value(apiDefs::key::supportInfo).toObject();
|
||||
|
||||
serviceData.type = serviceType;
|
||||
serviceData.protocol = serviceProtocol;
|
||||
@@ -270,7 +282,7 @@ ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJs
|
||||
serviceData.serviceInfo.object = serviceInfo;
|
||||
serviceData.availableCountries = availableCountries;
|
||||
|
||||
serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString();
|
||||
serviceData.subscription.endDate = subscriptionObject.value(apiDefs::key::endDate).toString();
|
||||
|
||||
return serviceData;
|
||||
}
|
||||
|
||||
@@ -4,65 +4,23 @@
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QVector>
|
||||
|
||||
class ApiServicesModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
CardDescriptionRole,
|
||||
ServiceDescriptionRole,
|
||||
IsServiceAvailableRole,
|
||||
SpeedRole,
|
||||
TimeLimitRole,
|
||||
RegionRole,
|
||||
FeaturesRole,
|
||||
PriceRole,
|
||||
EndDateRole,
|
||||
OrderRole
|
||||
};
|
||||
|
||||
explicit ApiServicesModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
void updateModel(const QJsonObject &data);
|
||||
|
||||
void setServiceIndex(const int index);
|
||||
|
||||
QJsonObject getSelectedServiceInfo();
|
||||
QString getSelectedServiceType();
|
||||
QString getSelectedServiceProtocol();
|
||||
QString getSelectedServiceName();
|
||||
QJsonArray getSelectedServiceCountries();
|
||||
|
||||
QString getCountryCode();
|
||||
|
||||
QString getStoreEndpoint();
|
||||
|
||||
QVariant getSelectedServiceData(const QString roleString);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
struct ServiceInfo
|
||||
{
|
||||
QString name;
|
||||
QString speed;
|
||||
QString timeLimit;
|
||||
QString region;
|
||||
QString price;
|
||||
|
||||
QString description;
|
||||
QString features;
|
||||
QString cardDescription;
|
||||
|
||||
QString termsOfUseUrl;
|
||||
QString privacyPolicyUrl;
|
||||
|
||||
QJsonObject object;
|
||||
};
|
||||
|
||||
@@ -80,11 +38,64 @@ private:
|
||||
QString storeEndpoint;
|
||||
|
||||
ServiceInfo serviceInfo;
|
||||
QJsonObject supportInfo;
|
||||
Subscription subscription;
|
||||
|
||||
QJsonArray availableCountries;
|
||||
|
||||
QJsonArray subscriptionPlansJson;
|
||||
QJsonArray benefits;
|
||||
|
||||
QString minPriceLabel;
|
||||
};
|
||||
|
||||
enum Roles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
CardDescriptionRole,
|
||||
ServiceDescriptionRole,
|
||||
IsServiceAvailableRole,
|
||||
PriceRole,
|
||||
EndDateRole,
|
||||
TermsOfUseUrlRole,
|
||||
PrivacyPolicyUrlRole,
|
||||
ShowRecommendedRole,
|
||||
OrderRole
|
||||
};
|
||||
|
||||
explicit ApiServicesModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
ApiServicesData selectedServiceData() const;
|
||||
|
||||
public slots:
|
||||
void updateModel(const QJsonObject &data);
|
||||
|
||||
void setServiceIndex(const int index);
|
||||
|
||||
QJsonObject getSelectedServiceInfo();
|
||||
QString getSelectedServiceType();
|
||||
QString getSelectedServiceProtocol();
|
||||
QString getSelectedServiceName();
|
||||
QJsonArray getSelectedServiceCountries();
|
||||
|
||||
QString getCountryCode();
|
||||
|
||||
QString getStoreEndpoint();
|
||||
|
||||
QVariant getSelectedServiceData(const QString roleString);
|
||||
|
||||
Q_INVOKABLE int serviceIndexForType(const QString &type) const;
|
||||
|
||||
signals:
|
||||
void serviceSelectionChanged();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
ApiServicesData getApiServicesData(const QJsonObject &data);
|
||||
|
||||
QString m_countryCode;
|
||||
@@ -93,4 +104,4 @@ private:
|
||||
int m_selectedServiceIndex;
|
||||
};
|
||||
|
||||
#endif // APISERVICESMODEL_H
|
||||
#endif
|
||||
|
||||
131
client/ui/models/api/apiSubscriptionPlansModel.cpp
Normal file
131
client/ui/models/api/apiSubscriptionPlansModel.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#include "apiSubscriptionPlansModel.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QModelIndex>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace configKey
|
||||
{
|
||||
constexpr char billingPeriod[] = "billing_period";
|
||||
constexpr char priceLabel[] = "price_label";
|
||||
constexpr char subtitle[] = "subtitle";
|
||||
constexpr char recommended[] = "recommended";
|
||||
constexpr char checkoutUrl[] = "checkout_url";
|
||||
constexpr char isTrial[] = "is_trial";
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
constexpr char storeProductId[] = "store_product_id";
|
||||
}
|
||||
}
|
||||
|
||||
ApiSubscriptionPlansModel::ApiSubscriptionPlansModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int ApiSubscriptionPlansModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return m_subscriptionPlans.size();
|
||||
}
|
||||
|
||||
QVariant ApiSubscriptionPlansModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_subscriptionPlans.size()) {
|
||||
return {};
|
||||
}
|
||||
const SubscriptionPlanItem &plan = m_subscriptionPlans.at(index.row());
|
||||
switch (role) {
|
||||
case BillingPeriodRole:
|
||||
return plan.billingPeriod;
|
||||
case PriceLabelRole:
|
||||
return plan.priceLabel;
|
||||
case SubtitleRole:
|
||||
return plan.subtitle;
|
||||
case RecommendedRole:
|
||||
return plan.recommended;
|
||||
case CheckoutUrlRole:
|
||||
return plan.checkoutUrl;
|
||||
case IsTrialRole:
|
||||
return plan.isTrial;
|
||||
case ServiceProtocolRole:
|
||||
return plan.serviceProtocol;
|
||||
case StoreProductIdRole:
|
||||
return plan.storeProductId;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ApiSubscriptionPlansModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ BillingPeriodRole, "billingPeriod" },
|
||||
{ PriceLabelRole, "priceLabel" },
|
||||
{ SubtitleRole, "subtitle" },
|
||||
{ RecommendedRole, "recommended" },
|
||||
{ CheckoutUrlRole, "checkoutUrl" },
|
||||
{ IsTrialRole, "isTrial" },
|
||||
{ ServiceProtocolRole, "serviceProtocol" },
|
||||
{ StoreProductIdRole, "storeProductId" },
|
||||
};
|
||||
}
|
||||
|
||||
void ApiSubscriptionPlansModel::updateModel(const QJsonArray &arr)
|
||||
{
|
||||
beginResetModel();
|
||||
m_subscriptionPlans.clear();
|
||||
m_subscriptionPlans.reserve(arr.size());
|
||||
for (const QJsonValue &planValue : arr) {
|
||||
if (!planValue.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject planObject = planValue.toObject();
|
||||
SubscriptionPlanItem subscriptionPlan;
|
||||
subscriptionPlan.billingPeriod = planObject.value(configKey::billingPeriod).toString();
|
||||
subscriptionPlan.priceLabel = planObject.value(configKey::priceLabel).toString();
|
||||
subscriptionPlan.subtitle = planObject.value(configKey::subtitle).toString();
|
||||
subscriptionPlan.recommended = planObject.value(configKey::recommended).toBool();
|
||||
subscriptionPlan.checkoutUrl = planObject.value(configKey::checkoutUrl).toString();
|
||||
subscriptionPlan.isTrial = planObject.value(configKey::isTrial).toBool();
|
||||
subscriptionPlan.serviceProtocol = planObject.value(configKey::serviceProtocol).toString();
|
||||
subscriptionPlan.storeProductId = planObject.value(configKey::storeProductId).toString();
|
||||
m_subscriptionPlans.append(std::move(subscriptionPlan));
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void ApiSubscriptionPlansModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_subscriptionPlans.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QVariantMap ApiSubscriptionPlansModel::planAt(int row) const
|
||||
{
|
||||
if (row < 0 || row >= m_subscriptionPlans.size()) {
|
||||
return {};
|
||||
}
|
||||
const QModelIndex modelIndex = index(row, 0);
|
||||
QVariantMap planMap;
|
||||
const QHash<int, QByteArray> roles = roleNames();
|
||||
for (auto roleIt = roles.cbegin(); roleIt != roles.cend(); ++roleIt) {
|
||||
planMap.insert(QString::fromUtf8(roleIt.value()), data(modelIndex, roleIt.key()));
|
||||
}
|
||||
return planMap;
|
||||
}
|
||||
|
||||
int ApiSubscriptionPlansModel::recommendedRowIndex() const
|
||||
{
|
||||
for (int planIndex = 0; planIndex < m_subscriptionPlans.size(); ++planIndex) {
|
||||
if (m_subscriptionPlans.at(planIndex).recommended) {
|
||||
return planIndex;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
53
client/ui/models/api/apiSubscriptionPlansModel.h
Normal file
53
client/ui/models/api/apiSubscriptionPlansModel.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#ifndef APISUBSCRIPTIONPLANSMODEL_H
|
||||
#define APISUBSCRIPTIONPLANSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QVector>
|
||||
|
||||
class ApiSubscriptionPlansModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
BillingPeriodRole = Qt::UserRole + 1,
|
||||
PriceLabelRole,
|
||||
SubtitleRole,
|
||||
RecommendedRole,
|
||||
CheckoutUrlRole,
|
||||
IsTrialRole,
|
||||
ServiceProtocolRole,
|
||||
StoreProductIdRole
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit ApiSubscriptionPlansModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void updateModel(const QJsonArray &arr);
|
||||
void clear();
|
||||
|
||||
Q_INVOKABLE QVariantMap planAt(int row) const;
|
||||
Q_INVOKABLE int recommendedRowIndex() const;
|
||||
|
||||
private:
|
||||
struct SubscriptionPlanItem
|
||||
{
|
||||
QString billingPeriod;
|
||||
QString priceLabel;
|
||||
QString subtitle;
|
||||
bool recommended = false;
|
||||
QString checkoutUrl;
|
||||
bool isTrial = false;
|
||||
QString serviceProtocol;
|
||||
QString storeProductId;
|
||||
};
|
||||
|
||||
QVector<SubscriptionPlanItem> m_subscriptionPlans;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -180,18 +180,35 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
|
||||
}
|
||||
case IsSubscriptionExpiredRole: {
|
||||
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
|
||||
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
|
||||
if (endDate.isEmpty()) return false;
|
||||
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) {
|
||||
return false;
|
||||
}
|
||||
if (apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false)) {
|
||||
return false;
|
||||
}
|
||||
if (apiConfig.value(apiDefs::key::subscriptionExpiredByServer).toBool(false)) {
|
||||
return true;
|
||||
}
|
||||
const QString endDate =
|
||||
apiConfig.value(apiDefs::key::subscription).toObject().value(apiDefs::key::endDate).toString();
|
||||
if (endDate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return apiUtils::isSubscriptionExpired(endDate);
|
||||
}
|
||||
case IsSubscriptionExpiringSoonRole: {
|
||||
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
|
||||
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
|
||||
if (endDate.isEmpty()) return false;
|
||||
if (apiUtils::isSubscriptionExpired(endDate)) return false;
|
||||
QDateTime endDateTime = QDateTime::fromString(endDate, Qt::ISODateWithMs);
|
||||
return endDateTime <= QDateTime::currentDateTimeUtc().addDays(10);
|
||||
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) {
|
||||
return false;
|
||||
}
|
||||
if (apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false)) {
|
||||
return false;
|
||||
}
|
||||
const QString endDate =
|
||||
apiConfig.value(apiDefs::key::subscription).toObject().value(apiDefs::key::endDate).toString();
|
||||
if (endDate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return apiUtils::isSubscriptionExpiringSoon(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,21 +761,21 @@ bool ServersModel::isServerFromApiAlreadyExists(const QString &userCountryCode,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServersModel::hasServerWithVpnKey(const QString &vpnKey) const
|
||||
int ServersModel::indexOfServerWithVpnKey(const QString &vpnKey) const
|
||||
{
|
||||
const QString normalizedInput = normalizeVpnKey(vpnKey);
|
||||
if (normalizedInput.isEmpty()) {
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (const auto &server : std::as_const(m_servers)) {
|
||||
const auto apiConfig = server.toObject().value(configKey::apiConfig).toObject();
|
||||
for (int i = 0; i < m_servers.size(); ++i) {
|
||||
const auto apiConfig = m_servers.at(i).toObject().value(configKey::apiConfig).toObject();
|
||||
const QString existingKey = normalizeVpnKey(apiConfig.value(apiDefs::key::vpnKey).toString());
|
||||
if (!existingKey.isEmpty() && existingKey == normalizedInput) {
|
||||
return true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
|
||||
|
||||
@@ -143,7 +143,7 @@ public slots:
|
||||
|
||||
bool isServerFromApiAlreadyExists(const quint16 crc);
|
||||
bool isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol);
|
||||
bool hasServerWithVpnKey(const QString &vpnKey) const;
|
||||
int indexOfServerWithVpnKey(const QString &vpnKey) const;
|
||||
|
||||
QVariant getDefaultServerData(const QString roleString);
|
||||
|
||||
|
||||
65
client/ui/qml/Components/BenefitRow.qml
Normal file
65
client/ui/qml/Components/BenefitRow.qml
Normal file
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string iconSource: ""
|
||||
property string titleText: ""
|
||||
property string bodyText: ""
|
||||
property bool accent: false
|
||||
|
||||
spacing: 12
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.preferredWidth: 22
|
||||
Layout.preferredHeight: 22
|
||||
source: root.iconSource
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
text: root.titleText
|
||||
color: AmneziaStyle.color.paleGray
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.DemiBold
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: bodyLabel.implicitHeight
|
||||
|
||||
LabelTextType {
|
||||
id: bodyLabel
|
||||
width: parent.width
|
||||
text: root.bodyText
|
||||
color: root.accent ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 14
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: bodyLabel
|
||||
visible: root.accent && root.bodyText.length > 0
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
var t = root.bodyText.trim()
|
||||
if (t.startsWith("@")) {
|
||||
Qt.openUrlExternally("https://t.me/" + t.substring(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
client/ui/qml/Components/BenefitsPanel.qml
Normal file
40
client/ui/qml/Components/BenefitsPanel.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "."
|
||||
|
||||
import Style 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var benefitsModel: null
|
||||
|
||||
visible: benefitsModel && benefitsModel.rowCount() > 0
|
||||
|
||||
radius: 16
|
||||
color: AmneziaStyle.color.benefitsPanelBackground
|
||||
implicitHeight: inner.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: inner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
spacing: 20
|
||||
|
||||
Repeater {
|
||||
model: benefitsModel
|
||||
|
||||
delegate: BenefitRow {
|
||||
Layout.fillWidth: true
|
||||
iconSource: model.icon
|
||||
titleText: model.title
|
||||
bodyText: model.body
|
||||
accent: !!model.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,12 @@ ListViewType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: name
|
||||
descriptionText: serverDescription
|
||||
descriptionText: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
|
||||
? (isSubscriptionExpired ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon."))
|
||||
: serverDescription
|
||||
descriptionColor: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
|
||||
? (isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot)
|
||||
: AmneziaStyle.color.mutedGray
|
||||
|
||||
checked: index === root.selectedIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
@@ -126,18 +131,6 @@ ListViewType {
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
visible: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 64
|
||||
Layout.bottomMargin: 8
|
||||
|
||||
text: isSubscriptionExpired ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon.")
|
||||
color: isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 0
|
||||
|
||||
@@ -12,6 +12,13 @@ import "../Controls2/TextTypes"
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
property bool isRenewalActionAvailable: false
|
||||
|
||||
onOpened: {
|
||||
isRenewalActionAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
|
||||
&& !ApiAccountInfoModel.data("isInAppPurchase")
|
||||
}
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
@@ -43,6 +50,8 @@ DrawerType2 {
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
visible: root.isRenewalActionAvailable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
@@ -53,6 +62,8 @@ DrawerType2 {
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
visible: root.isRenewalActionAvailable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
94
client/ui/qml/Components/SubscriptionPlanCard.qml
Normal file
94
client/ui/qml/Components/SubscriptionPlanCard.qml
Normal file
@@ -0,0 +1,94 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool selected: false
|
||||
property string billingPeriod: ""
|
||||
property string priceLabel: ""
|
||||
property string subtitle: ""
|
||||
property bool showRecommendedBadge: false
|
||||
property string recommendedText: "Recommended"
|
||||
|
||||
signal selectRequested
|
||||
|
||||
implicitHeight: cardLayout.implicitHeight + 28
|
||||
radius: 16
|
||||
color: AmneziaStyle.color.transparent
|
||||
border.width: selected ? 2 : 1
|
||||
border.color: selected ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.charcoalGray
|
||||
|
||||
ColumnLayout {
|
||||
id: cardLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
text: root.billingPeriod
|
||||
color: root.selected ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.paleGray
|
||||
font.pixelSize: 17
|
||||
font.weight: Font.DemiBold
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
text: root.priceLabel
|
||||
color: root.selected ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.paleGray
|
||||
font.pixelSize: 17
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.subtitle.length > 0 || root.showRecommendedBadge
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
text: root.subtitle
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.showRecommendedBadge
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: 10
|
||||
color: AmneziaStyle.color.softViolet
|
||||
implicitHeight: recLabel.implicitHeight + 8
|
||||
implicitWidth: recLabel.implicitWidth + 16
|
||||
|
||||
LabelTextType {
|
||||
id: recLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: root.recommendedText
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.selectRequested()
|
||||
}
|
||||
}
|
||||
35
client/ui/qml/Components/TermsAndPrivacyText.qml
Normal file
35
client/ui/qml/Components/TermsAndPrivacyText.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
ParagraphTextType {
|
||||
id: root
|
||||
|
||||
property string termsUrl: ""
|
||||
property string privacyUrl: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
|
||||
.arg(root.termsUrl)
|
||||
.arg(root.privacyUrl)
|
||||
.arg(AmneziaStyle.color.goldenApricotString)
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@ Button {
|
||||
property string bodyText
|
||||
property string footerText
|
||||
|
||||
property color headerTextColor: AmneziaStyle.color.paleGray
|
||||
property color bodyTextColor: AmneziaStyle.color.mutedGray
|
||||
property bool showRecommendedBadge: false
|
||||
property string recommendedText: ""
|
||||
|
||||
property string hoveredColor: AmneziaStyle.color.slateGray
|
||||
property string defaultColor: AmneziaStyle.color.onyxBlack
|
||||
|
||||
@@ -28,6 +33,7 @@ Button {
|
||||
property alias focusItem: rightImage
|
||||
|
||||
hoverEnabled: true
|
||||
clip: false
|
||||
|
||||
background: Rectangle {
|
||||
id: backgroundRect
|
||||
@@ -43,104 +49,151 @@ Button {
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
id: contentRoot
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitHeight: content.implicitHeight
|
||||
readonly property bool badgeVisible: root.showRecommendedBadge && root.recommendedText !== ""
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
implicitHeight: layoutCol.implicitHeight
|
||||
|
||||
anchors.fill: parent
|
||||
ColumnLayout {
|
||||
id: layoutCol
|
||||
|
||||
Image {
|
||||
id: leftImage
|
||||
source: leftImageSource
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
visible: leftImageSource !== ""
|
||||
Item {
|
||||
id: badgeTopSpacer
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 24
|
||||
Layout.leftMargin: 24
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
ListItemTitleType {
|
||||
text: root.headerText
|
||||
visible: text !== ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: root.bodyText !== "" ? 0 : 16
|
||||
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
text: root.bodyText
|
||||
visible: text !== ""
|
||||
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
textFormat: Text.RichText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: root.footerText !== "" ? 0 : 16
|
||||
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
|
||||
ButtonTextType {
|
||||
text: root.footerText
|
||||
visible: text !== ""
|
||||
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
id: rightImage
|
||||
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
|
||||
hoverEnabled: false
|
||||
image: rightImageSource
|
||||
imageColor: rightImageColor
|
||||
visible: rightImageSource ? true : false
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentRoot.badgeVisible ? (recBadge.height / 2 + 8) : 0
|
||||
|
||||
Rectangle {
|
||||
id: rightImageBackground
|
||||
id: recBadge
|
||||
|
||||
anchors.fill: parent
|
||||
radius: 12
|
||||
color: "transparent"
|
||||
visible: contentRoot.badgeVisible
|
||||
z: 2
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 20
|
||||
anchors.verticalCenter: parent.top
|
||||
|
||||
radius: 10
|
||||
color: AmneziaStyle.color.softViolet
|
||||
implicitHeight: recLabel.implicitHeight + 8
|
||||
implicitWidth: recLabel.implicitWidth + 16
|
||||
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
BadgeTextType {
|
||||
id: recLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: root.recommendedText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Image {
|
||||
id: leftImage
|
||||
source: leftImageSource
|
||||
|
||||
visible: leftImageSource !== ""
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 24
|
||||
Layout.leftMargin: 24
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
ListItemTitleType {
|
||||
text: root.headerText
|
||||
visible: text !== ""
|
||||
|
||||
color: root.headerTextColor
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.topMargin: contentRoot.badgeVisible ? 0 : 16
|
||||
Layout.bottomMargin: root.bodyText !== "" ? 0 : 16
|
||||
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
text: root.bodyText
|
||||
visible: text !== ""
|
||||
|
||||
color: root.bodyTextColor
|
||||
textFormat: Text.RichText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: root.footerText !== "" ? 0 : 8
|
||||
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
|
||||
ButtonTextType {
|
||||
text: root.footerText
|
||||
visible: text !== ""
|
||||
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
opacity: root.textOpacity
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.clicked()
|
||||
ImageButtonType {
|
||||
id: rightImage
|
||||
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
|
||||
hoverEnabled: false
|
||||
image: rightImageSource
|
||||
imageColor: rightImageColor
|
||||
visible: rightImageSource ? true : false
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
Rectangle {
|
||||
id: rightImageBackground
|
||||
|
||||
anchors.fill: parent
|
||||
radius: 12
|
||||
color: "transparent"
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
client/ui/qml/Controls2/TextTypes/BadgeTextType.qml
Normal file
15
client/ui/qml/Controls2/TextTypes/BadgeTextType.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
|
||||
import Style 1.0
|
||||
|
||||
Text {
|
||||
lineHeight: 10 + LanguageModel.getLineHeightAppend()
|
||||
lineHeightMode: Text.FixedHeight
|
||||
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
font.family: "PT Root UI VF"
|
||||
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
@@ -12,13 +12,17 @@ QtObject {
|
||||
readonly property color slateGray: '#2C2D30'
|
||||
readonly property color onyxBlack: '#1C1D21'
|
||||
readonly property color midnightBlack: '#0E0E11'
|
||||
readonly property color goldenApricot: '#FBB26A'
|
||||
readonly property color goldenApricot: goldenApricotString
|
||||
readonly property color benefitsPanelBackground: '#1C1C1E'
|
||||
readonly property color softViolet: '#A87BE2'
|
||||
readonly property color burntOrange: '#A85809'
|
||||
readonly property color mutedBrown: '#84603D'
|
||||
readonly property color richBrown: '#633303'
|
||||
readonly property color deepBrown: '#402102'
|
||||
readonly property color vibrantRed: '#EB5757'
|
||||
readonly property color darkCharcoal: '#261E1A'
|
||||
readonly property color pearlGray: '#EAEAEC'
|
||||
|
||||
readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12)
|
||||
readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08)
|
||||
readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05)
|
||||
@@ -26,9 +30,10 @@ QtObject {
|
||||
readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3)
|
||||
readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8)
|
||||
readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
|
||||
readonly property color pearlGray: '#EAEAEC'
|
||||
readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26)
|
||||
readonly property color translucentSlateGray: Qt.rgba(85/255, 86/255, 92/255, 0.13)
|
||||
readonly property color translucentOnyxBlack: Qt.rgba(28/255, 29/255, 33/255, 0.13)
|
||||
|
||||
readonly property string goldenApricotString: '#FBB26A'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,14 @@ PageType {
|
||||
property var processedServer
|
||||
property bool subscriptionExpired: false
|
||||
property bool subscriptionExpiringSoon: false
|
||||
property bool isSubscriptionRenewalAvailable: false
|
||||
property bool isInAppPurchase: false
|
||||
|
||||
function updateSubscriptionState() {
|
||||
root.subscriptionExpired = ServersModel.getProcessedServerData("isSubscriptionExpired")
|
||||
root.subscriptionExpiringSoon = ServersModel.getProcessedServerData("isSubscriptionExpiringSoon")
|
||||
root.isSubscriptionRenewalAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
|
||||
root.isInAppPurchase = ApiAccountInfoModel.data("isInAppPurchase")
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -38,6 +43,14 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ApiAccountInfoModel
|
||||
|
||||
function onModelReset() {
|
||||
root.updateSubscriptionState()
|
||||
}
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyServersModel
|
||||
objectName: "proxyServersModel"
|
||||
@@ -87,7 +100,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 4
|
||||
Layout.bottomMargin: root.subscriptionExpired || root.subscriptionExpiringSoon ? 0 : 4
|
||||
|
||||
actionButtonImage: "qrc:/images/controls/settings.svg"
|
||||
|
||||
@@ -105,26 +118,27 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
ParagraphTextType {
|
||||
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
Layout.topMargin: 12
|
||||
|
||||
text: root.subscriptionExpired ? qsTr("Subscription expired") : qsTr("Subscription expiring soon")
|
||||
color: root.subscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
|
||||
visible: (root.subscriptionExpired || root.subscriptionExpiringSoon)
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 4
|
||||
Layout.topMargin: 28
|
||||
Layout.bottomMargin: 0
|
||||
|
||||
defaultColor: AmneziaStyle.color.paleGray
|
||||
hoveredColor: AmneziaStyle.color.lightGray
|
||||
@@ -138,11 +152,11 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: (root.subscriptionExpired || root.subscriptionExpiringSoon) ? 8 : 4
|
||||
Layout.topMargin: (root.subscriptionExpired || root.subscriptionExpiringSoon) ? 12 : 4
|
||||
Layout.bottomMargin: 8
|
||||
|
||||
text: qsTr("Location for connection")
|
||||
|
||||
@@ -2,7 +2,6 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
@@ -55,10 +54,14 @@ PageType {
|
||||
|
||||
property bool isSubscriptionExpired: false
|
||||
property bool isSubscriptionExpiringSoon: false
|
||||
property bool isSubscriptionRenewalAvailable: false
|
||||
property bool isInAppPurchase: false
|
||||
|
||||
function updateSubscriptionState() {
|
||||
root.isSubscriptionExpired = ApiAccountInfoModel.data("isSubscriptionExpired")
|
||||
root.isSubscriptionExpiringSoon = ApiAccountInfoModel.data("isSubscriptionExpiringSoon")
|
||||
root.isSubscriptionRenewalAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
|
||||
root.isInAppPurchase = ApiAccountInfoModel.data("isInAppPurchase")
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -124,7 +127,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 10
|
||||
Layout.bottomMargin: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon ? 0 : 10
|
||||
|
||||
actionButtonImage: "qrc:/images/controls/edit-3.svg"
|
||||
|
||||
@@ -135,13 +138,13 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
ParagraphTextType {
|
||||
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
Layout.topMargin: 12
|
||||
|
||||
text: root.isSubscriptionExpired
|
||||
? qsTr("Subscription expired")
|
||||
@@ -150,10 +153,6 @@ PageType {
|
||||
color: root.isSubscriptionExpired
|
||||
? AmneziaStyle.color.vibrantRed
|
||||
: AmneziaStyle.color.goldenApricot
|
||||
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
@@ -170,7 +169,8 @@ PageType {
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
|
||||
visible: (root.isSubscriptionExpired || root.isSubscriptionExpiringSoon)
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -226,52 +226,33 @@ PageType {
|
||||
|
||||
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
|
||||
|
||||
Item {
|
||||
BasicButtonType {
|
||||
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: renewRow.implicitHeight + 32
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: ApiSettingsController.getRenewalLink()
|
||||
}
|
||||
implicitHeight: 25
|
||||
|
||||
Row {
|
||||
id: renewRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
textColor: AmneziaStyle.color.goldenApricot
|
||||
leftImageSource: "qrc:/images/controls/refresh-cw.svg"
|
||||
leftImageColor: AmneziaStyle.color.goldenApricot
|
||||
|
||||
Item {
|
||||
width: renewIcon.implicitWidth
|
||||
height: renewIcon.implicitHeight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Renew subscription")
|
||||
|
||||
Image {
|
||||
id: renewIcon
|
||||
source: "qrc:/images/controls/refresh-cw.svg"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: renewIcon
|
||||
source: renewIcon
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Renew subscription")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
clickedFunc: function() {
|
||||
ApiSettingsController.getRenewalLink()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
|
||||
140
client/ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml
Normal file
140
client/ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml
Normal file
@@ -0,0 +1,140 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string freeHeaderName: ""
|
||||
property string freeHeaderDescription: ""
|
||||
|
||||
function syncFromModel() {
|
||||
root.freeHeaderName = String(ApiServicesModel.getSelectedServiceData("name"))
|
||||
root.freeHeaderDescription = String(ApiServicesModel.getSelectedServiceData("serviceDescription"))
|
||||
}
|
||||
|
||||
Component.onCompleted: syncFromModel()
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (activeFocus) {
|
||||
flick.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flick
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: continueButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: scrollColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: scrollColumn
|
||||
|
||||
width: flick.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
headerText: root.freeHeaderName
|
||||
descriptionText: root.freeHeaderDescription
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 12
|
||||
|
||||
text: qsTr("Free features")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
BenefitsPanel {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
benefitsModel: ApiBenefitsModel
|
||||
}
|
||||
|
||||
TermsAndPrivacyText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
|
||||
|
||||
termsUrl: String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl"))
|
||||
privacyUrl: String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl"))
|
||||
}
|
||||
|
||||
TermsAndPrivacyText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild)
|
||||
|
||||
termsUrl: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
privacyUrl: LanguageModel.getCurrentSiteUrl("policy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
z: 2
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + SettingsController.safeAreaBottomMargin
|
||||
|
||||
text: qsTr("Continue")
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
client/ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml
Normal file
198
client/ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml
Normal file
@@ -0,0 +1,198 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
import PageEnum 1.0
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property int selectedPlanIndex: 0
|
||||
property string premiumHeaderName: ""
|
||||
property string premiumHeaderDescription: ""
|
||||
|
||||
readonly property var currentPlan: ApiSubscriptionPlansModel.planAt(selectedPlanIndex)
|
||||
|
||||
function syncFromModel() {
|
||||
root.selectedPlanIndex = ApiSubscriptionPlansModel.recommendedRowIndex()
|
||||
|
||||
root.premiumHeaderName = String(ApiServicesModel.getSelectedServiceData("name"))
|
||||
root.premiumHeaderDescription = String(ApiServicesModel.getSelectedServiceData("serviceDescription"))
|
||||
}
|
||||
|
||||
Component.onCompleted: syncFromModel()
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (activeFocus) {
|
||||
flick.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flick
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: continueButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: scrollColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: scrollColumn
|
||||
|
||||
width: flick.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
headerText: root.premiumHeaderName
|
||||
descriptionText: root.premiumHeaderDescription
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ApiSubscriptionPlansModel
|
||||
|
||||
delegate: SubscriptionPlanCard {
|
||||
required property int index
|
||||
required property var model
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: index === ApiSubscriptionPlansModel.rowCount() - 1 ? 24 : 12
|
||||
|
||||
selected: root.selectedPlanIndex === index
|
||||
billingPeriod: String(model.billingPeriod)
|
||||
priceLabel: String(model.priceLabel)
|
||||
subtitle: String(model.subtitle)
|
||||
showRecommendedBadge: !!model.recommended
|
||||
recommendedText: qsTr("Recommended")
|
||||
|
||||
onSelectRequested: root.selectedPlanIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 12
|
||||
|
||||
text: qsTr("Premium features")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
BenefitsPanel {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
benefitsModel: ApiBenefitsModel
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
visible: Qt.platform.os === "ios" || IsMacOsNeBuild
|
||||
spacing: 16
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
}
|
||||
|
||||
TermsAndPrivacyText {
|
||||
termsUrl: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
privacyUrl: LanguageModel.getCurrentSiteUrl("policy")
|
||||
}
|
||||
}
|
||||
|
||||
TermsAndPrivacyText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
|
||||
|
||||
termsUrl: String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl"))
|
||||
privacyUrl: String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
z: 2
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + SettingsController.safeAreaBottomMargin
|
||||
|
||||
text: {
|
||||
var plan = root.currentPlan
|
||||
if (!plan) {
|
||||
return qsTr("Continue")
|
||||
}
|
||||
return qsTr("Subscribe — %1 for %2").arg(String(plan.billingPeriod)).arg(String(plan.priceLabel))
|
||||
}
|
||||
|
||||
clickedFunc: function() {
|
||||
var plan = root.currentPlan
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
if (plan.isTrial) {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiTrialEmail)
|
||||
return
|
||||
}
|
||||
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
|
||||
PageController.showBusyIndicator(true)
|
||||
var storeId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
|
||||
ApiConfigsController.importPremiumFromAppStore(storeId)
|
||||
PageController.showBusyIndicator(false)
|
||||
return
|
||||
}
|
||||
if (plan.checkoutUrl) {
|
||||
Qt.openUrlExternally(plan.checkoutUrl)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
||||
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
@@ -84,12 +84,19 @@ PageType {
|
||||
bodyText: cardDescription
|
||||
footerText: price
|
||||
|
||||
showRecommendedBadge: showRecommended && isServiceAvailable
|
||||
recommendedText: qsTr("Recommended")
|
||||
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
onClicked: {
|
||||
if (isServiceAvailable) {
|
||||
ApiServicesModel.setServiceIndex(proxyApiServicesModel.mapToSource(index))
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
|
||||
if (ApiServicesModel.getSelectedServiceType() === "amnezia-premium") {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiPremiumInfo)
|
||||
} else {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiFreeInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
138
client/ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml
Normal file
138
client/ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml
Normal file
@@ -0,0 +1,138 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
property string trialEmailErrorMessage: ""
|
||||
|
||||
Connections {
|
||||
target: ApiConfigsController
|
||||
|
||||
function onTrialEmailError(message) {
|
||||
root.trialEmailErrorMessage = message
|
||||
emailField.errorText = message
|
||||
}
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (activeFocus) {
|
||||
flick.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flick
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: continueButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: scrollColumn.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: scrollColumn
|
||||
|
||||
width: flick.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
headerText: qsTr("Create an account")
|
||||
descriptionText: qsTr("To manage your subscription")
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: emailField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
headerText: qsTr("Email")
|
||||
textField.placeholderText: qsTr("Email")
|
||||
textField.inputMethodHints: Qt.ImhEmailCharactersOnly
|
||||
|
||||
Connections {
|
||||
target: emailField.textField
|
||||
|
||||
function onTextChanged() {
|
||||
if (root.trialEmailErrorMessage !== "") {
|
||||
root.trialEmailErrorMessage = ""
|
||||
emailField.errorText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
text: qsTr("We will create an account for your trial subscription and send important subscription updates to this email.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
z: 2
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + SettingsController.safeAreaBottomMargin
|
||||
|
||||
text: qsTr("Continue")
|
||||
|
||||
clickedFunc: function() {
|
||||
root.trialEmailErrorMessage = ""
|
||||
emailField.errorText = ""
|
||||
|
||||
var raw = emailField.textField.text.trim()
|
||||
if (raw.length === 0 || raw.indexOf("@") < 0) {
|
||||
PageController.showNotificationMessage(qsTr("Enter a valid email address"))
|
||||
return
|
||||
}
|
||||
PageController.showBusyIndicator(true)
|
||||
var ok = ApiConfigsController.importTrialFromGateway(raw)
|
||||
PageController.showBusyIndicator(false)
|
||||
if (ok) {
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,6 +222,9 @@ PageType {
|
||||
headerText: title
|
||||
bodyText: description
|
||||
|
||||
showRecommendedBadge: featuredAmneziaConnection
|
||||
recommendedText: featuredAmneziaConnection ? qsTr("Recommended") : ""
|
||||
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
leftImageSource: imageSource
|
||||
|
||||
@@ -275,8 +278,9 @@ PageType {
|
||||
id: amneziaVpn
|
||||
|
||||
property string title: qsTr("VPN by Amnezia")
|
||||
property string description: qsTr("Connect to classic paid and free VPN services from Amnezia")
|
||||
property string description: qsTr("The easiest way to connect to VPN")
|
||||
property string imageSource: "qrc:/images/controls/amnezia.svg"
|
||||
property bool featuredAmneziaConnection: true
|
||||
property bool isVisible: true
|
||||
property var handler: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
@@ -291,6 +295,7 @@ PageType {
|
||||
QtObject {
|
||||
id: selfHostVpn
|
||||
|
||||
property bool featuredAmneziaConnection: false
|
||||
property string title: qsTr("Self-hosted VPN")
|
||||
property string description: qsTr("Configure Amnezia VPN on your own server")
|
||||
property string imageSource: "qrc:/images/controls/server.svg"
|
||||
@@ -303,6 +308,7 @@ PageType {
|
||||
QtObject {
|
||||
id: backupRestore
|
||||
|
||||
property bool featuredAmneziaConnection: false
|
||||
property string title: qsTr("Restore from backup")
|
||||
property string description: qsTr("")
|
||||
property string imageSource: "qrc:/images/controls/archive-restore.svg"
|
||||
@@ -321,6 +327,7 @@ PageType {
|
||||
QtObject {
|
||||
id: fileOpen
|
||||
|
||||
property bool featuredAmneziaConnection: false
|
||||
property string title: qsTr("File with connection settings")
|
||||
property string description: qsTr("")
|
||||
property string imageSource: "qrc:/images/controls/folder-search-2.svg"
|
||||
@@ -340,6 +347,7 @@ PageType {
|
||||
QtObject {
|
||||
id: qrScan
|
||||
|
||||
property bool featuredAmneziaConnection: false
|
||||
property string title: qsTr("QR code")
|
||||
property string description: qsTr("")
|
||||
property string imageSource: "qrc:/images/controls/scan-line.svg"
|
||||
@@ -355,13 +363,14 @@ PageType {
|
||||
QtObject {
|
||||
id: restorePurchases
|
||||
|
||||
property bool featuredAmneziaConnection: false
|
||||
property string title: qsTr("Restore purchases")
|
||||
property string description: qsTr("")
|
||||
property string imageSource: "qrc:/images/controls/refresh-cw.svg"
|
||||
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild
|
||||
property var handler: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
ApiConfigsController.restoreSerivceFromAppStore()
|
||||
ApiConfigsController.restoreServiceFromAppStore()
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
@@ -369,6 +378,7 @@ PageType {
|
||||
QtObject {
|
||||
id: siteLink
|
||||
|
||||
property bool featuredAmneziaConnection: false
|
||||
property string title: qsTr("I have nothing")
|
||||
property string description: qsTr("")
|
||||
property string imageSource: "qrc:/images/controls/help-circle.svg"
|
||||
|
||||
@@ -225,9 +225,13 @@ PageType {
|
||||
Connections {
|
||||
target: ApiConfigsController
|
||||
|
||||
function onInstallServerFromApiFinished(message) {
|
||||
function onInstallServerFromApiFinished(message, preferredDefaultIndex) {
|
||||
if (!ConnectionController.isConnected) {
|
||||
ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1);
|
||||
if (preferredDefaultIndex !== undefined && preferredDefaultIndex >= 0) {
|
||||
ServersModel.setDefaultServerIndex(preferredDefaultIndex)
|
||||
} else {
|
||||
ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1)
|
||||
}
|
||||
ServersModel.processedIndex = ServersModel.defaultIndex
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user