chore: minor codestyle fixes

This commit is contained in:
vkamn
2026-04-02 17:32:42 +08:00
parent 6249eea905
commit d3eaead779
9 changed files with 279 additions and 270 deletions

View File

@@ -6,6 +6,7 @@ import StoreKit
public class StoreKit2Helper: NSObject {
public static let shared = StoreKit2Helper()
private static let errorDomain = "StoreKit2Helper"
private struct EntitlementInfo {
let transactionId: UInt64
@@ -57,8 +58,8 @@ public class StoreKit2Helper: NSObject {
do {
let products = try await Product.products(for: [productIdentifier])
guard let product = products.first else {
let error = NSError(domain: "StoreKit2Helper", code: 0, userInfo: [NSLocalizedDescriptionKey: "Product not found"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 0, description: "Product not found"))
return
}
let result = try await product.purchase()
@@ -67,25 +68,25 @@ public class StoreKit2Helper: NSObject {
switch verification {
case .verified(let transaction):
await transaction.finish()
let txId = String(transaction.id)
let origTxId = String(transaction.originalID)
let pId = transaction.productID
DispatchQueue.main.async { completion(true, txId, pId, origTxId, nil) }
completePurchase(completion: completion, success: true, transactionId: String(transaction.id),
productId: transaction.productID, originalTransactionId: String(transaction.originalID), error: nil)
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:
let error = NSError(domain: "StoreKit2Helper", code: 1, userInfo: [NSLocalizedDescriptionKey: "Purchase cancelled"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 1, description: "Purchase cancelled"))
case .pending:
let error = NSError(domain: "StoreKit2Helper", code: 2, userInfo: [NSLocalizedDescriptionKey: "Purchase pending"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 2, description: "Purchase pending"))
@unknown default:
let error = NSError(domain: "StoreKit2Helper", code: 3, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
error: makeError(code: 3, description: "Unknown purchase result"))
}
} 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 {
let v = Double(period.value)
let periodValue = Double(period.value)
switch period.unit {
case .day:
return v / 30.0
return periodValue / 30.0
case .week:
return v * 7.0 / 30.0
return periodValue * 7.0 / 30.0
case .month:
return v
return periodValue
case .year:
return v * 12.0
return periodValue * 12.0
@unknown default:
return v
return periodValue
}
}
@@ -114,35 +115,7 @@ public class StoreKit2Helper: NSObject {
Task {
do {
let products = try await Product.products(for: identifiers)
let productDicts = products.map { product -> NSDictionary in
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 productDicts = products.map { product in productDictionary(for: product) }
let fetchedIds = Set(products.map { $0.id })
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
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))
}
}

View File

@@ -9,6 +9,14 @@
#include <QtCore/QDebug>
#include <QtCore/QString>
namespace
{
QString toQString(NSString *value)
{
return QString::fromUtf8((value ?: @"").UTF8String);
}
}
API_AVAILABLE(ios(15.0), macos(12.0))
@implementation StoreKitController
@@ -45,11 +53,10 @@ API_AVAILABLE(ios(15.0), macos(12.0))
NSString *originalTransactionId,
NSError *error) {
if (success) {
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << QString::fromUtf8(transactionId.UTF8String)
<< "originalTransactionId =" << QString::fromUtf8(originalTransactionId.UTF8String)
<< "productId =" << QString::fromUtf8(productId.UTF8String);
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << toQString(transactionId)
<< "originalTransactionId =" << toQString(originalTransactionId) << "productId =" << toQString(productId);
} 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) {
completion(success, transactionId, productId, originalTransactionId, error);
@@ -67,15 +74,14 @@ API_AVAILABLE(ios(15.0), macos(12.0))
if (success) {
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
for (NSDictionary *info in entitlements) {
for (NSDictionary *entitlement in entitlements) {
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
<< "transactionId=" << QString::fromUtf8([info[@"transactionId"] UTF8String])
<< "originalTransactionId=" << QString::fromUtf8([info[@"originalTransactionId"] UTF8String])
<< "productId=" << QString::fromUtf8([info[@"productId"] UTF8String]);
<< "transactionId=" << toQString(entitlement[@"transactionId"])
<< "originalTransactionId=" << toQString(entitlement[@"originalTransactionId"])
<< "productId=" << toQString(entitlement[@"productId"]);
}
} else {
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:"
<< QString::fromUtf8(error.localizedDescription.UTF8String);
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:" << toQString(error.localizedDescription);
}
if (completion) {
completion(success, entitlements, error);
@@ -93,10 +99,10 @@ API_AVAILABLE(ios(15.0), macos(12.0))
NSArray<NSString *> *invalidIdentifiers,
NSError *error) {
if (!error) {
for (NSDictionary *p in products) {
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << QString::fromUtf8([p[@"productId"] UTF8String])
<< "price=" << QString::fromUtf8([p[@"price"] UTF8String])
<< "currency=" << QString::fromUtf8([p[@"currencyCode"] UTF8String]);
for (NSDictionary *productInfo in products) {
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << toQString(productInfo[@"productId"])
<< "price=" << toQString(productInfo[@"price"])
<< "currency=" << toQString(productInfo[@"currencyCode"]);
}
}
if (completion) {

View File

@@ -1147,26 +1147,26 @@ void IosController::fetchProducts(const QStringList &productIds,
NSArray<NSString *> * _Nonnull invalidIdentifiers,
NSError * _Nullable error) {
QList<QVariantMap> outProducts;
for (NSDictionary *p in products) {
QVariantMap m;
m["productId"] = QString::fromUtf8([p[@"productId"] UTF8String]);
m["title"] = QString::fromUtf8([p[@"title"] UTF8String]);
m["description"] = QString::fromUtf8([p[@"description"] UTF8String]);
m["price"] = QString::fromUtf8([p[@"price"] UTF8String]);
if (p[@"displayPrice"]) {
m["displayPrice"] = QString::fromUtf8([p[@"displayPrice"] UTF8String]);
for (NSDictionary *productInfo in products) {
QVariantMap productData;
productData["productId"] = QString::fromUtf8([productInfo[@"productId"] UTF8String]);
productData["title"] = QString::fromUtf8([productInfo[@"title"] UTF8String]);
productData["description"] = QString::fromUtf8([productInfo[@"description"] UTF8String]);
productData["price"] = QString::fromUtf8([productInfo[@"price"] UTF8String]);
if (productInfo[@"displayPrice"]) {
productData["displayPrice"] = QString::fromUtf8([productInfo[@"displayPrice"] UTF8String]);
}
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
if (p[@"priceAmount"]) {
m["priceAmount"] = [p[@"priceAmount"] doubleValue];
productData["currencyCode"] = QString::fromUtf8([productInfo[@"currencyCode"] UTF8String]);
if (productInfo[@"priceAmount"]) {
productData["priceAmount"] = [productInfo[@"priceAmount"] doubleValue];
}
if (p[@"subscriptionBillingMonths"]) {
m["subscriptionBillingMonths"] = [p[@"subscriptionBillingMonths"] doubleValue];
if (productInfo[@"subscriptionBillingMonths"]) {
productData["subscriptionBillingMonths"] = [productInfo[@"subscriptionBillingMonths"] doubleValue];
}
if (p[@"displayPricePerMonth"]) {
m["displayPricePerMonth"] = QString::fromUtf8([p[@"displayPricePerMonth"] UTF8String]);
if (productInfo[@"displayPricePerMonth"]) {
productData["displayPricePerMonth"] = QString::fromUtf8([productInfo[@"displayPricePerMonth"] UTF8String]);
}
outProducts.push_back(m);
outProducts.push_back(productData);
}
QStringList invalid;

View File

@@ -139,6 +139,7 @@
<file>ui/qml/Components/BenefitRow.qml</file>
<file>ui/qml/Components/BenefitsPanel.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/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>

View File

@@ -261,6 +261,65 @@ namespace
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();
@@ -268,28 +327,7 @@ namespace
return;
}
QStringList productIds;
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);
}
}
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;
@@ -311,71 +349,53 @@ namespace
});
loop.exec();
QHash<QString, StoreKitPlanQuote> idToQuote;
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);
}
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services.at(i).toObject();
if (service.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
QJsonObject serviceObject = services.at(serviceIndex).toObject();
if (serviceObject.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
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;
double minMonthlyAmount = std::numeric_limits<double>::infinity();
QString minMonthlyDisplay;
for (const QJsonValue &planValue : plans) {
for (const QJsonValue &planValue : sourcePlans) {
if (!planValue.isObject()) {
continue;
}
QJsonObject planObject = planValue.toObject();
const bool trial = planObject.value(configKey::isTrial).toBool();
const QString storeId = planObject.value(configKey::storeProductId).toString();
if (storeId.isEmpty()) {
const QString storeProductId = planObject.value(configKey::storeProductId).toString();
if (storeProductId.isEmpty()) {
continue;
}
const auto quoteIt = idToQuote.constFind(storeId);
if (quoteIt == idToQuote.cend()) {
const auto quoteIterator = quotesByProductId.constFind(storeProductId);
if (quoteIterator == quotesByProductId.cend()) {
continue;
}
const StoreKitPlanQuote &quote = *quoteIt;
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
const StoreKitPlanQuote &quote = *quoteIterator;
planObject.insert(configKey::priceLabel, quote.displayPrice);
const double months = quote.subscriptionBillingMonths;
if (!trial && months > 1.0 + 1e-6 && !quote.displayPricePerMonth.isEmpty()) {
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 (!trial && quote.priceAmount > 0.0) {
const double monthsForMin = months > 1e-6 ? months : 1.0;
if (!isTrialPlan && quote.priceAmount > 0.0) {
const double monthsForMin = months > kMonthsFallbackThreshold ? months : 1.0;
const double monthly = quote.priceAmount / monthsForMin;
if (monthly < minMonthlyAmount - 1e-9) {
if (monthly < minMonthlyAmount - kMonthlyPriceEpsilon) {
minMonthlyAmount = monthly;
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
}
@@ -384,15 +404,15 @@ namespace
mergedPlans.append(planObject);
}
description.insert(configKey::subscriptionPlans, mergedPlans);
descriptionObject.insert(configKey::subscriptionPlans, mergedPlans);
if (minMonthlyAmount < std::numeric_limits<double>::infinity() && !minMonthlyDisplay.isEmpty()) {
description.insert(configKey::minPriceLabel,
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
"IAP: card footer minimum monthly price from StoreKit")
.arg(minMonthlyDisplay));
descriptionObject.insert(configKey::minPriceLabel,
QCoreApplication::translate("ApiConfigsController", "from %1 per month",
"IAP: card footer minimum monthly price from StoreKit")
.arg(minMonthlyDisplay));
}
service.insert(configKey::serviceDescription, description);
services.replace(i, service);
serviceObject.insert(configKey::serviceDescription, descriptionObject);
services.replace(serviceIndex, serviceObject);
}
data.insert(configKey::services, services);
}
@@ -469,6 +489,8 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
return false;
}
qDebug() << responseBody;
QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object();
QString nativeConfig = jsonConfig.value(configKey::config).toString();
nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey);
@@ -568,9 +590,9 @@ 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) {
@@ -578,8 +600,7 @@ bool ApiConfigsController::importService()
return importPremiumFromAppStore(QString());
}
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
importFreeFromGateway();
return true;
return importFreeFromGateway();
}
return false;
}
@@ -599,11 +620,11 @@ bool ApiConfigsController::importPremiumFromAppStore(const QString &storeProduct
QString purchaseError;
QEventLoop waitPurchase;
IosController::Instance()->purchaseProduct(productId,
[&](bool success, const QString &txId, const QString &purchasedProductId,
const QString &originalTxId, const QString &errorString) {
[&](bool success, const QString &transactionId, const QString &purchasedProductId,
const QString &originalTransactionIdResponse, const QString &errorString) {
purchaseOk = success;
originalTransactionId = originalTxId;
storeTransactionId = txId;
originalTransactionId = originalTransactionIdResponse;
storeTransactionId = transactionId;
purchasedStoreProductId = purchasedProductId;
purchaseError = errorString;
waitPurchase.quit();
@@ -675,20 +696,12 @@ bool ApiConfigsController::restoreServiceFromAppStore()
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;
@@ -1196,47 +1209,54 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
#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;
}
QString normalizedKey = key;
normalizedKey.replace(QStringLiteral("vpn://"), QString());
QString normalizedVpnKey = rawVpnKey;
normalizedVpnKey.replace(QStringLiteral("vpn://"), QString());
QByteArray configString = QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(configString);
const bool payloadWasCompressed = !configUncompressed.isEmpty();
if (payloadWasCompressed) {
configString = configUncompressed;
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(normalizedVpnKey);
if (duplicateServerIndex >= 0) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
return ErrorCode::ApiConfigAlreadyAdded;
}
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";
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);
configString = QJsonDocument(configObject).toJson();
configPayload = QJsonDocument(configObject).toJson();
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) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
return ErrorCode::ApiConfigAlreadyAdded;
}
apiConfig.insert(apiDefs::key::vpnKey, normalizedKey);
apiConfig.insert(apiDefs::key::vpnKey, normalizedVpnKey);
configObject.insert(apiDefs::key::apiConfig, apiConfig);
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());

View 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
}
}

View File

@@ -85,7 +85,7 @@ PageType {
benefitsModel: ApiBenefitsModel
}
ParagraphTextType {
TermsAndPrivacyText {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -93,30 +93,11 @@ PageType {
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
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
}
termsUrl: String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl"))
privacyUrl: String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl"))
}
ParagraphTextType {
TermsAndPrivacyText {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -124,27 +105,8 @@ PageType {
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild)
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: %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
}
termsUrl: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
privacyUrl: LanguageModel.getCurrentSiteUrl("policy")
}
}
}

View File

@@ -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.")
}
ParagraphTextType {
Layout.fillWidth: true
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: %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
}
TermsAndPrivacyText {
termsUrl: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
privacyUrl: LanguageModel.getCurrentSiteUrl("policy")
}
}
ParagraphTextType {
TermsAndPrivacyText {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -166,27 +146,8 @@ PageType {
visible: !(Qt.platform.os === "ios" || IsMacOsNeBuild)
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
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
}
termsUrl: String(ApiServicesModel.getSelectedServiceData("termsOfUseUrl"))
privacyUrl: String(ApiServicesModel.getSelectedServiceData("privacyPolicyUrl"))
}
}
}

View File

@@ -78,7 +78,7 @@ PageType {
wrapMode: Text.WordWrap
color: AmneziaStyle.color.mutedGray
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.")
}
}
}