mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
chore: minor codestyle fixes
This commit is contained in:
@@ -6,6 +6,7 @@ import StoreKit
|
|||||||
public class StoreKit2Helper: NSObject {
|
public class StoreKit2Helper: NSObject {
|
||||||
|
|
||||||
public static let shared = StoreKit2Helper()
|
public static let shared = StoreKit2Helper()
|
||||||
|
private static let errorDomain = "StoreKit2Helper"
|
||||||
|
|
||||||
private struct EntitlementInfo {
|
private struct EntitlementInfo {
|
||||||
let transactionId: UInt64
|
let transactionId: UInt64
|
||||||
@@ -57,8 +58,8 @@ public class StoreKit2Helper: NSObject {
|
|||||||
do {
|
do {
|
||||||
let products = try await Product.products(for: [productIdentifier])
|
let products = try await Product.products(for: [productIdentifier])
|
||||||
guard let product = products.first else {
|
guard let product = products.first else {
|
||||||
let error = NSError(domain: "StoreKit2Helper", code: 0, userInfo: [NSLocalizedDescriptionKey: "Product not found"])
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
error: makeError(code: 0, description: "Product not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let result = try await product.purchase()
|
let result = try await product.purchase()
|
||||||
@@ -67,25 +68,25 @@ public class StoreKit2Helper: NSObject {
|
|||||||
switch verification {
|
switch verification {
|
||||||
case .verified(let transaction):
|
case .verified(let transaction):
|
||||||
await transaction.finish()
|
await transaction.finish()
|
||||||
let txId = String(transaction.id)
|
completePurchase(completion: completion, success: true, transactionId: String(transaction.id),
|
||||||
let origTxId = String(transaction.originalID)
|
productId: transaction.productID, originalTransactionId: String(transaction.originalID), error: nil)
|
||||||
let pId = transaction.productID
|
|
||||||
DispatchQueue.main.async { completion(true, txId, pId, origTxId, nil) }
|
|
||||||
case .unverified(_, let error):
|
case .unverified(_, let error):
|
||||||
DispatchQueue.main.async { completion(false, nil, nil, nil, error as NSError) }
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: error as NSError)
|
||||||
}
|
}
|
||||||
case .userCancelled:
|
case .userCancelled:
|
||||||
let error = NSError(domain: "StoreKit2Helper", code: 1, userInfo: [NSLocalizedDescriptionKey: "Purchase cancelled"])
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
error: makeError(code: 1, description: "Purchase cancelled"))
|
||||||
case .pending:
|
case .pending:
|
||||||
let error = NSError(domain: "StoreKit2Helper", code: 2, userInfo: [NSLocalizedDescriptionKey: "Purchase pending"])
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
error: makeError(code: 2, description: "Purchase pending"))
|
||||||
@unknown default:
|
@unknown default:
|
||||||
let error = NSError(domain: "StoreKit2Helper", code: 3, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
error: makeError(code: 3, description: "Unknown purchase result"))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async { completion(false, nil, nil, nil, error as NSError) }
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: error as NSError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,18 +96,18 @@ public class StoreKit2Helper: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func subscriptionBillingMonths(_ period: Product.SubscriptionPeriod) -> Double {
|
private func subscriptionBillingMonths(_ period: Product.SubscriptionPeriod) -> Double {
|
||||||
let v = Double(period.value)
|
let periodValue = Double(period.value)
|
||||||
switch period.unit {
|
switch period.unit {
|
||||||
case .day:
|
case .day:
|
||||||
return v / 30.0
|
return periodValue / 30.0
|
||||||
case .week:
|
case .week:
|
||||||
return v * 7.0 / 30.0
|
return periodValue * 7.0 / 30.0
|
||||||
case .month:
|
case .month:
|
||||||
return v
|
return periodValue
|
||||||
case .year:
|
case .year:
|
||||||
return v * 12.0
|
return periodValue * 12.0
|
||||||
@unknown default:
|
@unknown default:
|
||||||
return v
|
return periodValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,35 +115,7 @@ public class StoreKit2Helper: NSObject {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let products = try await Product.products(for: identifiers)
|
let products = try await Product.products(for: identifiers)
|
||||||
let productDicts = products.map { product -> NSDictionary in
|
let productDicts = products.map { product in productDictionary(for: product) }
|
||||||
let currencyCode = storefrontCurrencyCode(for: product)
|
|
||||||
var dict: [String: Any] = [
|
|
||||||
"productId": product.id,
|
|
||||||
"title": product.displayName,
|
|
||||||
"description": product.description,
|
|
||||||
"price": "\(product.price)",
|
|
||||||
"displayPrice": product.displayPrice,
|
|
||||||
"currencyCode": currencyCode,
|
|
||||||
"priceAmount": NSDecimalNumber(decimal: product.price).doubleValue
|
|
||||||
]
|
|
||||||
if let sub = product.subscription {
|
|
||||||
let months = subscriptionBillingMonths(sub.subscriptionPeriod)
|
|
||||||
dict["subscriptionBillingMonths"] = months
|
|
||||||
if months > 1e-6 {
|
|
||||||
let perMonth = product.price / Decimal(months)
|
|
||||||
let formatter = NumberFormatter()
|
|
||||||
formatter.numberStyle = .currency
|
|
||||||
formatter.locale = product.priceFormatStyle.locale
|
|
||||||
if !currencyCode.isEmpty {
|
|
||||||
formatter.currencyCode = currencyCode
|
|
||||||
}
|
|
||||||
if let perMonthStr = formatter.string(from: NSDecimalNumber(decimal: perMonth)) {
|
|
||||||
dict["displayPricePerMonth"] = perMonthStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dict as NSDictionary
|
|
||||||
}
|
|
||||||
let fetchedIds = Set(products.map { $0.id })
|
let fetchedIds = Set(products.map { $0.id })
|
||||||
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
|
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
|
||||||
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
|
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
|
||||||
@@ -151,4 +124,55 @@ public class StoreKit2Helper: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeError(code: Int, description: String) -> NSError {
|
||||||
|
NSError(domain: Self.errorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: description])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func completePurchase(completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void,
|
||||||
|
success: Bool,
|
||||||
|
transactionId: String?,
|
||||||
|
productId: String?,
|
||||||
|
originalTransactionId: String?,
|
||||||
|
error: NSError?) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(success, transactionId, productId, originalTransactionId, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func productDictionary(for product: Product) -> NSDictionary {
|
||||||
|
let currencyCode = storefrontCurrencyCode(for: product)
|
||||||
|
var productData: [String: Any] = [
|
||||||
|
"productId": product.id,
|
||||||
|
"title": product.displayName,
|
||||||
|
"description": product.description,
|
||||||
|
"price": "\(product.price)",
|
||||||
|
"displayPrice": product.displayPrice,
|
||||||
|
"currencyCode": currencyCode,
|
||||||
|
"priceAmount": NSDecimalNumber(decimal: product.price).doubleValue
|
||||||
|
]
|
||||||
|
if let subscription = product.subscription {
|
||||||
|
let billingMonths = subscriptionBillingMonths(subscription.subscriptionPeriod)
|
||||||
|
productData["subscriptionBillingMonths"] = billingMonths
|
||||||
|
if let perMonthPrice = displayPricePerMonth(for: product, billingMonths: billingMonths, currencyCode: currencyCode) {
|
||||||
|
productData["displayPricePerMonth"] = perMonthPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return productData as NSDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayPricePerMonth(for product: Product, billingMonths: Double, currencyCode: String) -> String? {
|
||||||
|
if billingMonths <= 1e-6 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let perMonthPrice = product.price / Decimal(billingMonths)
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .currency
|
||||||
|
formatter.locale = product.priceFormatStyle.locale
|
||||||
|
if !currencyCode.isEmpty {
|
||||||
|
formatter.currencyCode = currencyCode
|
||||||
|
}
|
||||||
|
return formatter.string(from: NSDecimalNumber(decimal: perMonthPrice))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,14 @@
|
|||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
#include <QtCore/QString>
|
#include <QtCore/QString>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QString toQString(NSString *value)
|
||||||
|
{
|
||||||
|
return QString::fromUtf8((value ?: @"").UTF8String);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
API_AVAILABLE(ios(15.0), macos(12.0))
|
API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
@implementation StoreKitController
|
@implementation StoreKitController
|
||||||
|
|
||||||
@@ -45,11 +53,10 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
NSString *originalTransactionId,
|
NSString *originalTransactionId,
|
||||||
NSError *error) {
|
NSError *error) {
|
||||||
if (success) {
|
if (success) {
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << QString::fromUtf8(transactionId.UTF8String)
|
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << toQString(transactionId)
|
||||||
<< "originalTransactionId =" << QString::fromUtf8(originalTransactionId.UTF8String)
|
<< "originalTransactionId =" << toQString(originalTransactionId) << "productId =" << toQString(productId);
|
||||||
<< "productId =" << QString::fromUtf8(productId.UTF8String);
|
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << QString::fromUtf8(error.localizedDescription.UTF8String);
|
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << toQString(error.localizedDescription);
|
||||||
}
|
}
|
||||||
if (completion) {
|
if (completion) {
|
||||||
completion(success, transactionId, productId, originalTransactionId, error);
|
completion(success, transactionId, productId, originalTransactionId, error);
|
||||||
@@ -67,15 +74,14 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
if (success) {
|
if (success) {
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
|
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
|
||||||
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
|
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
|
||||||
for (NSDictionary *info in entitlements) {
|
for (NSDictionary *entitlement in entitlements) {
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
|
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
|
||||||
<< "transactionId=" << QString::fromUtf8([info[@"transactionId"] UTF8String])
|
<< "transactionId=" << toQString(entitlement[@"transactionId"])
|
||||||
<< "originalTransactionId=" << QString::fromUtf8([info[@"originalTransactionId"] UTF8String])
|
<< "originalTransactionId=" << toQString(entitlement[@"originalTransactionId"])
|
||||||
<< "productId=" << QString::fromUtf8([info[@"productId"] UTF8String]);
|
<< "productId=" << toQString(entitlement[@"productId"]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:"
|
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:" << toQString(error.localizedDescription);
|
||||||
<< QString::fromUtf8(error.localizedDescription.UTF8String);
|
|
||||||
}
|
}
|
||||||
if (completion) {
|
if (completion) {
|
||||||
completion(success, entitlements, error);
|
completion(success, entitlements, error);
|
||||||
@@ -93,10 +99,10 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
NSArray<NSString *> *invalidIdentifiers,
|
NSArray<NSString *> *invalidIdentifiers,
|
||||||
NSError *error) {
|
NSError *error) {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
for (NSDictionary *p in products) {
|
for (NSDictionary *productInfo in products) {
|
||||||
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << QString::fromUtf8([p[@"productId"] UTF8String])
|
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << toQString(productInfo[@"productId"])
|
||||||
<< "price=" << QString::fromUtf8([p[@"price"] UTF8String])
|
<< "price=" << toQString(productInfo[@"price"])
|
||||||
<< "currency=" << QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
<< "currency=" << toQString(productInfo[@"currencyCode"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (completion) {
|
if (completion) {
|
||||||
|
|||||||
@@ -1147,26 +1147,26 @@ void IosController::fetchProducts(const QStringList &productIds,
|
|||||||
NSArray<NSString *> * _Nonnull invalidIdentifiers,
|
NSArray<NSString *> * _Nonnull invalidIdentifiers,
|
||||||
NSError * _Nullable error) {
|
NSError * _Nullable error) {
|
||||||
QList<QVariantMap> outProducts;
|
QList<QVariantMap> outProducts;
|
||||||
for (NSDictionary *p in products) {
|
for (NSDictionary *productInfo in products) {
|
||||||
QVariantMap m;
|
QVariantMap productData;
|
||||||
m["productId"] = QString::fromUtf8([p[@"productId"] UTF8String]);
|
productData["productId"] = QString::fromUtf8([productInfo[@"productId"] UTF8String]);
|
||||||
m["title"] = QString::fromUtf8([p[@"title"] UTF8String]);
|
productData["title"] = QString::fromUtf8([productInfo[@"title"] UTF8String]);
|
||||||
m["description"] = QString::fromUtf8([p[@"description"] UTF8String]);
|
productData["description"] = QString::fromUtf8([productInfo[@"description"] UTF8String]);
|
||||||
m["price"] = QString::fromUtf8([p[@"price"] UTF8String]);
|
productData["price"] = QString::fromUtf8([productInfo[@"price"] UTF8String]);
|
||||||
if (p[@"displayPrice"]) {
|
if (productInfo[@"displayPrice"]) {
|
||||||
m["displayPrice"] = QString::fromUtf8([p[@"displayPrice"] UTF8String]);
|
productData["displayPrice"] = QString::fromUtf8([productInfo[@"displayPrice"] UTF8String]);
|
||||||
}
|
}
|
||||||
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
productData["currencyCode"] = QString::fromUtf8([productInfo[@"currencyCode"] UTF8String]);
|
||||||
if (p[@"priceAmount"]) {
|
if (productInfo[@"priceAmount"]) {
|
||||||
m["priceAmount"] = [p[@"priceAmount"] doubleValue];
|
productData["priceAmount"] = [productInfo[@"priceAmount"] doubleValue];
|
||||||
}
|
}
|
||||||
if (p[@"subscriptionBillingMonths"]) {
|
if (productInfo[@"subscriptionBillingMonths"]) {
|
||||||
m["subscriptionBillingMonths"] = [p[@"subscriptionBillingMonths"] doubleValue];
|
productData["subscriptionBillingMonths"] = [productInfo[@"subscriptionBillingMonths"] doubleValue];
|
||||||
}
|
}
|
||||||
if (p[@"displayPricePerMonth"]) {
|
if (productInfo[@"displayPricePerMonth"]) {
|
||||||
m["displayPricePerMonth"] = QString::fromUtf8([p[@"displayPricePerMonth"] UTF8String]);
|
productData["displayPricePerMonth"] = QString::fromUtf8([productInfo[@"displayPricePerMonth"] UTF8String]);
|
||||||
}
|
}
|
||||||
outProducts.push_back(m);
|
outProducts.push_back(productData);
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList invalid;
|
QStringList invalid;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@
|
|||||||
<file>ui/qml/Components/BenefitRow.qml</file>
|
<file>ui/qml/Components/BenefitRow.qml</file>
|
||||||
<file>ui/qml/Components/BenefitsPanel.qml</file>
|
<file>ui/qml/Components/BenefitsPanel.qml</file>
|
||||||
<file>ui/qml/Components/SubscriptionPlanCard.qml</file>
|
<file>ui/qml/Components/SubscriptionPlanCard.qml</file>
|
||||||
|
<file>ui/qml/Components/TermsAndPrivacyText.qml</file>
|
||||||
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
||||||
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
|
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
|
||||||
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
|
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
|
||||||
|
|||||||
@@ -261,6 +261,65 @@ namespace
|
|||||||
QString displayPricePerMonth;
|
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)
|
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||||
{
|
{
|
||||||
QJsonArray services = data.value(configKey::services).toArray();
|
QJsonArray services = data.value(configKey::services).toArray();
|
||||||
@@ -268,28 +327,7 @@ namespace
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList productIds;
|
const QStringList productIds = collectPremiumStoreProductIds(services);
|
||||||
QSet<QString> seen;
|
|
||||||
for (int i = 0; i < services.size(); ++i) {
|
|
||||||
const QJsonObject service = services.at(i).toObject();
|
|
||||||
if (service.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QJsonObject description = service.value(configKey::serviceDescription).toObject();
|
|
||||||
const QJsonArray plans = description.value(configKey::subscriptionPlans).toArray();
|
|
||||||
for (const QJsonValue &planValue : plans) {
|
|
||||||
if (!planValue.isObject()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QString id = planValue.toObject().value(configKey::storeProductId).toString();
|
|
||||||
if (id.isEmpty() || seen.contains(id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.insert(id);
|
|
||||||
productIds.append(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (productIds.isEmpty()) {
|
if (productIds.isEmpty()) {
|
||||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||||
return;
|
return;
|
||||||
@@ -311,71 +349,53 @@ namespace
|
|||||||
});
|
});
|
||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
QHash<QString, StoreKitPlanQuote> idToQuote;
|
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
|
||||||
idToQuote.reserve(fetchedProducts.size());
|
|
||||||
for (const QVariantMap &product : fetchedProducts) {
|
|
||||||
const QString id = product.value(QStringLiteral("productId")).toString();
|
|
||||||
if (id.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
StoreKitPlanQuote quote;
|
|
||||||
QString display = product.value(QStringLiteral("displayPrice")).toString();
|
|
||||||
if (display.isEmpty()) {
|
|
||||||
const QString price = product.value(QStringLiteral("price")).toString();
|
|
||||||
const QString currencyCode = product.value(QStringLiteral("currencyCode")).toString();
|
|
||||||
display = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
|
||||||
}
|
|
||||||
quote.displayPrice = display;
|
|
||||||
quote.priceAmount = product.value(QStringLiteral("priceAmount")).toDouble();
|
|
||||||
quote.subscriptionBillingMonths = product.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
|
||||||
quote.displayPricePerMonth = product.value(QStringLiteral("displayPricePerMonth")).toString();
|
|
||||||
idToQuote.insert(id, quote);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < services.size(); ++i) {
|
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
|
||||||
QJsonObject service = services.at(i).toObject();
|
QJsonObject serviceObject = services.at(serviceIndex).toObject();
|
||||||
if (service.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
QJsonObject description = service.value(configKey::serviceDescription).toObject();
|
|
||||||
QJsonArray plans = description.value(configKey::subscriptionPlans).toArray();
|
QJsonObject descriptionObject = serviceObject.value(configKey::serviceDescription).toObject();
|
||||||
|
const QJsonArray sourcePlans = descriptionObject.value(configKey::subscriptionPlans).toArray();
|
||||||
|
|
||||||
QJsonArray mergedPlans;
|
QJsonArray mergedPlans;
|
||||||
double minMonthlyAmount = std::numeric_limits<double>::infinity();
|
double minMonthlyAmount = std::numeric_limits<double>::infinity();
|
||||||
QString minMonthlyDisplay;
|
QString minMonthlyDisplay;
|
||||||
|
|
||||||
for (const QJsonValue &planValue : plans) {
|
for (const QJsonValue &planValue : sourcePlans) {
|
||||||
if (!planValue.isObject()) {
|
if (!planValue.isObject()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject planObject = planValue.toObject();
|
QJsonObject planObject = planValue.toObject();
|
||||||
const bool trial = planObject.value(configKey::isTrial).toBool();
|
const QString storeProductId = planObject.value(configKey::storeProductId).toString();
|
||||||
const QString storeId = planObject.value(configKey::storeProductId).toString();
|
if (storeProductId.isEmpty()) {
|
||||||
|
|
||||||
if (storeId.isEmpty()) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto quoteIt = idToQuote.constFind(storeId);
|
const auto quoteIterator = quotesByProductId.constFind(storeProductId);
|
||||||
if (quoteIt == idToQuote.cend()) {
|
if (quoteIterator == quotesByProductId.cend()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StoreKitPlanQuote "e = *quoteIt;
|
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
|
||||||
|
const StoreKitPlanQuote "e = *quoteIterator;
|
||||||
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
||||||
|
|
||||||
const double months = quote.subscriptionBillingMonths;
|
const double months = quote.subscriptionBillingMonths;
|
||||||
if (!trial && months > 1.0 + 1e-6 && !quote.displayPricePerMonth.isEmpty()) {
|
if (!isTrialPlan && months > kOneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
|
||||||
planObject.insert(
|
planObject.insert(
|
||||||
configKey::subtitle,
|
configKey::subtitle,
|
||||||
QCoreApplication::translate("ApiConfigsController", "%1/mo", "IAP: price per month in plan subtitle")
|
QCoreApplication::translate("ApiConfigsController", "%1/mo", "IAP: price per month in plan subtitle")
|
||||||
.arg(quote.displayPricePerMonth));
|
.arg(quote.displayPricePerMonth));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!trial && quote.priceAmount > 0.0) {
|
if (!isTrialPlan && quote.priceAmount > 0.0) {
|
||||||
const double monthsForMin = months > 1e-6 ? months : 1.0;
|
const double monthsForMin = months > kMonthsFallbackThreshold ? months : 1.0;
|
||||||
const double monthly = quote.priceAmount / monthsForMin;
|
const double monthly = quote.priceAmount / monthsForMin;
|
||||||
if (monthly < minMonthlyAmount - 1e-9) {
|
if (monthly < minMonthlyAmount - kMonthlyPriceEpsilon) {
|
||||||
minMonthlyAmount = monthly;
|
minMonthlyAmount = monthly;
|
||||||
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
|
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
|
||||||
}
|
}
|
||||||
@@ -384,15 +404,15 @@ namespace
|
|||||||
mergedPlans.append(planObject);
|
mergedPlans.append(planObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
description.insert(configKey::subscriptionPlans, mergedPlans);
|
descriptionObject.insert(configKey::subscriptionPlans, mergedPlans);
|
||||||
if (minMonthlyAmount < std::numeric_limits<double>::infinity() && !minMonthlyDisplay.isEmpty()) {
|
if (minMonthlyAmount < std::numeric_limits<double>::infinity() && !minMonthlyDisplay.isEmpty()) {
|
||||||
description.insert(configKey::minPriceLabel,
|
descriptionObject.insert(configKey::minPriceLabel,
|
||||||
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
|
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
|
||||||
"IAP: card footer minimum monthly price from StoreKit")
|
"IAP: card footer minimum monthly price from StoreKit")
|
||||||
.arg(minMonthlyDisplay));
|
.arg(minMonthlyDisplay));
|
||||||
}
|
}
|
||||||
service.insert(configKey::serviceDescription, description);
|
serviceObject.insert(configKey::serviceDescription, descriptionObject);
|
||||||
services.replace(i, service);
|
services.replace(serviceIndex, serviceObject);
|
||||||
}
|
}
|
||||||
data.insert(configKey::services, services);
|
data.insert(configKey::services, services);
|
||||||
}
|
}
|
||||||
@@ -469,6 +489,8 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qDebug() << responseBody;
|
||||||
|
|
||||||
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
|
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
|
||||||
QString nativeConfig = jsonConfig.value(configKey::config).toString();
|
QString nativeConfig = jsonConfig.value(configKey::config).toString();
|
||||||
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
|
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
|
||||||
@@ -568,9 +590,9 @@ bool ApiConfigsController::fillAvailableServices()
|
|||||||
bool ApiConfigsController::importService()
|
bool ApiConfigsController::importService()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
bool isIosOrMacOsNe = true;
|
const bool isIosOrMacOsNe = true;
|
||||||
#else
|
#else
|
||||||
bool isIosOrMacOsNe = false;
|
const bool isIosOrMacOsNe = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
||||||
@@ -578,8 +600,7 @@ bool ApiConfigsController::importService()
|
|||||||
return importPremiumFromAppStore(QString());
|
return importPremiumFromAppStore(QString());
|
||||||
}
|
}
|
||||||
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
||||||
importFreeFromGateway();
|
return importFreeFromGateway();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -599,11 +620,11 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
|
|||||||
QString purchaseError;
|
QString purchaseError;
|
||||||
QEventLoop waitPurchase;
|
QEventLoop waitPurchase;
|
||||||
IosController::Instance()->purchaseProduct(productId,
|
IosController::Instance()->purchaseProduct(productId,
|
||||||
[&](bool success, const QString &txId, const QString &purchasedProductId,
|
[&](bool success, const QString &transactionId, const QString &purchasedProductId,
|
||||||
const QString &originalTxId, const QString &errorString) {
|
const QString &originalTransactionIdResponse, const QString &errorString) {
|
||||||
purchaseOk = success;
|
purchaseOk = success;
|
||||||
originalTransactionId = originalTxId;
|
originalTransactionId = originalTransactionIdResponse;
|
||||||
storeTransactionId = txId;
|
storeTransactionId = transactionId;
|
||||||
purchasedStoreProductId = purchasedProductId;
|
purchasedStoreProductId = purchasedProductId;
|
||||||
purchaseError = errorString;
|
purchaseError = errorString;
|
||||||
waitPurchase.quit();
|
waitPurchase.quit();
|
||||||
@@ -675,20 +696,12 @@ bool ApiConfigsController::restoreServiceFromAppStore()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have a valid premium selection for gateway requests
|
const int premiumServiceIndex = m_apiServicesModel->serviceIndexForType(premiumServiceType);
|
||||||
bool premiumSelected = false;
|
if (premiumServiceIndex < 0) {
|
||||||
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
|
|
||||||
m_apiServicesModel->setServiceIndex(i);
|
|
||||||
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
|
|
||||||
premiumSelected = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!premiumSelected) {
|
|
||||||
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
m_apiServicesModel->setServiceIndex(premiumServiceIndex);
|
||||||
|
|
||||||
bool restoreSuccess = false;
|
bool restoreSuccess = false;
|
||||||
QList<QVariantMap> restoredTransactions;
|
QList<QVariantMap> restoredTransactions;
|
||||||
@@ -1196,47 +1209,54 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
|||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
duplicateServerIndex = -1;
|
duplicateServerIndex = -1;
|
||||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
||||||
QString key = responseObject.value(QStringLiteral("key")).toString();
|
const QString rawVpnKey = responseObject.value(QStringLiteral("key")).toString();
|
||||||
if (key.isEmpty()) {
|
if (rawVpnKey.isEmpty()) {
|
||||||
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
||||||
return ErrorCode::ApiPurchaseError;
|
return ErrorCode::ApiPurchaseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString normalizedKey = key;
|
QString normalizedVpnKey = rawVpnKey;
|
||||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
normalizedVpnKey.replace(QStringLiteral("vpn://"), QString());
|
||||||
|
|
||||||
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
||||||
QByteArray configUncompressed = qUncompress(configString);
|
if (duplicateServerIndex >= 0) {
|
||||||
const bool payloadWasCompressed = !configUncompressed.isEmpty();
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||||
if (payloadWasCompressed) {
|
return ErrorCode::ApiConfigAlreadyAdded;
|
||||||
configString = configUncompressed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configString.isEmpty()) {
|
QByteArray configPayload =
|
||||||
|
QByteArray::fromBase64(normalizedVpnKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||||
|
QByteArray configUncompressed = qUncompress(configPayload);
|
||||||
|
const bool payloadWasCompressed = !configUncompressed.isEmpty();
|
||||||
|
if (payloadWasCompressed) {
|
||||||
|
configPayload = configUncompressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPayload.isEmpty()) {
|
||||||
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
||||||
return ErrorCode::ApiPurchaseError;
|
return ErrorCode::ApiPurchaseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
|
QJsonObject configObject = QJsonDocument::fromJson(configPayload).object();
|
||||||
|
|
||||||
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
auto apiConfig = configObject.value(apiDefs::key::apiConfig).toObject();
|
||||||
apiConfig.insert(apiDefs::key::isTestPurchase, isTestPurchase);
|
apiConfig.insert(apiDefs::key::isTestPurchase, isTestPurchase);
|
||||||
apiConfig.insert(apiDefs::key::isInAppPurchase, true);
|
apiConfig.insert(apiDefs::key::isInAppPurchase, true);
|
||||||
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
||||||
|
|
||||||
configString = QJsonDocument(configObject).toJson();
|
configPayload = QJsonDocument(configObject).toJson();
|
||||||
if (payloadWasCompressed) {
|
if (payloadWasCompressed) {
|
||||||
configString = qCompress(configString, 8);
|
configPayload = qCompress(configPayload, 8);
|
||||||
}
|
}
|
||||||
normalizedKey = QString(configString.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
normalizedVpnKey = QString(configPayload.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||||
|
|
||||||
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedKey);
|
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
|
||||||
if (duplicateServerIndex >= 0) {
|
if (duplicateServerIndex >= 0) {
|
||||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||||
return ErrorCode::ApiConfigAlreadyAdded;
|
return ErrorCode::ApiConfigAlreadyAdded;
|
||||||
}
|
}
|
||||||
|
|
||||||
apiConfig.insert(apiDefs::key::vpnKey, normalizedKey);
|
apiConfig.insert(apiDefs::key::vpnKey, normalizedVpnKey);
|
||||||
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
configObject.insert(apiDefs::key::apiConfig, apiConfig);
|
||||||
|
|
||||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ PageType {
|
|||||||
benefitsModel: ApiBenefitsModel
|
benefitsModel: ApiBenefitsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
TermsAndPrivacyText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
@@ -93,30 +93,11 @@ PageType {
|
|||||||
|
|
||||||
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
|
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
termsUrl: String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl"))
|
||||||
textFormat: Text.RichText
|
privacyUrl: String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl"))
|
||||||
color: AmneziaStyle.color.mutedGray
|
|
||||||
font.pixelSize: 12
|
|
||||||
|
|
||||||
text: {
|
|
||||||
return 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(String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl")))
|
|
||||||
.arg(String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl")))
|
|
||||||
.arg(AmneziaStyle.color.goldenApricotString)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLinkActivated: function(link) {
|
|
||||||
Qt.openUrlExternally(link)
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
TermsAndPrivacyText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
@@ -124,27 +105,8 @@ PageType {
|
|||||||
|
|
||||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild)
|
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild)
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
termsUrl: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||||
textFormat: Text.RichText
|
privacyUrl: LanguageModel.getCurrentSiteUrl("policy")
|
||||||
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: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
|
|
||||||
.arg(termsUrl).arg(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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,33 +132,13 @@ PageType {
|
|||||||
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.")
|
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
TermsAndPrivacyText {
|
||||||
Layout.fillWidth: true
|
termsUrl: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
privacyUrl: LanguageModel.getCurrentSiteUrl("policy")
|
||||||
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: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
|
|
||||||
.arg(termsUrl).arg(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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
TermsAndPrivacyText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
@@ -166,27 +146,8 @@ PageType {
|
|||||||
|
|
||||||
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
|
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
termsUrl: String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl"))
|
||||||
textFormat: Text.RichText
|
privacyUrl: String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl"))
|
||||||
color: AmneziaStyle.color.mutedGray
|
|
||||||
font.pixelSize: 12
|
|
||||||
|
|
||||||
text: {
|
|
||||||
return 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(String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl")))
|
|
||||||
.arg(String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl")))
|
|
||||||
.arg(AmneziaStyle.color.goldenApricotString)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLinkActivated: function(link) {
|
|
||||||
Qt.openUrlExternally(link)
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ PageType {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
color: AmneziaStyle.color.mutedGray
|
color: AmneziaStyle.color.mutedGray
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
text: qsTr("Additional details for this step can be described here.")
|
text: qsTr("We will create an account for your trial subscription and send important subscription updates to this email.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user