mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
feat: additional parsing for storekit subscription plans
This commit is contained in:
@@ -94,20 +94,54 @@ public class StoreKit2Helper: NSObject {
|
|||||||
product.priceFormatStyle.locale.currencyCode ?? ""
|
product.priceFormatStyle.locale.currencyCode ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func subscriptionBillingMonths(_ period: Product.SubscriptionPeriod) -> Double {
|
||||||
|
let v = Double(period.value)
|
||||||
|
switch period.unit {
|
||||||
|
case .day:
|
||||||
|
return v / 30.0
|
||||||
|
case .week:
|
||||||
|
return v * 7.0 / 30.0
|
||||||
|
case .month:
|
||||||
|
return v
|
||||||
|
case .year:
|
||||||
|
return v * 12.0
|
||||||
|
@unknown default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
|
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
|
||||||
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 -> NSDictionary in
|
||||||
let currencyCode = storefrontCurrencyCode(for: product)
|
let currencyCode = storefrontCurrencyCode(for: product)
|
||||||
return [
|
var dict: [String: Any] = [
|
||||||
"productId": product.id,
|
"productId": product.id,
|
||||||
"title": product.displayName,
|
"title": product.displayName,
|
||||||
"description": product.description,
|
"description": product.description,
|
||||||
"price": "\(product.price)",
|
"price": "\(product.price)",
|
||||||
"displayPrice": product.displayPrice,
|
"displayPrice": product.displayPrice,
|
||||||
"currencyCode": currencyCode
|
"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) }
|
||||||
|
|||||||
@@ -1157,6 +1157,15 @@ void IosController::fetchProducts(const QStringList &productIds,
|
|||||||
m["displayPrice"] = QString::fromUtf8([p[@"displayPrice"] UTF8String]);
|
m["displayPrice"] = QString::fromUtf8([p[@"displayPrice"] UTF8String]);
|
||||||
}
|
}
|
||||||
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
||||||
|
if (p[@"priceAmount"]) {
|
||||||
|
m["priceAmount"] = [p[@"priceAmount"] doubleValue];
|
||||||
|
}
|
||||||
|
if (p[@"subscriptionBillingMonths"]) {
|
||||||
|
m["subscriptionBillingMonths"] = [p[@"subscriptionBillingMonths"] doubleValue];
|
||||||
|
}
|
||||||
|
if (p[@"displayPricePerMonth"]) {
|
||||||
|
m["displayPricePerMonth"] = QString::fromUtf8([p[@"displayPricePerMonth"] UTF8String]);
|
||||||
|
}
|
||||||
outProducts.push_back(m);
|
outProducts.push_back(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
#include "ui/controllers/systemController.h"
|
#include "ui/controllers/systemController.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "platforms/ios/ios_controller.h"
|
#include "platforms/ios/ios_controller.h"
|
||||||
|
|
||||||
@@ -47,6 +49,9 @@ namespace
|
|||||||
constexpr char subscriptionPlans[] = "subscription_plans";
|
constexpr char subscriptionPlans[] = "subscription_plans";
|
||||||
constexpr char storeProductId[] = "store_product_id";
|
constexpr char storeProductId[] = "store_product_id";
|
||||||
constexpr char priceLabel[] = "price_label";
|
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 apiPayload[] = "api_payload";
|
||||||
constexpr char keyPayload[] = "key_payload";
|
constexpr char keyPayload[] = "key_payload";
|
||||||
@@ -249,6 +254,13 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
|
struct StoreKitPlanQuote {
|
||||||
|
QString displayPrice;
|
||||||
|
double priceAmount = 0.0;
|
||||||
|
double subscriptionBillingMonths = 0.0;
|
||||||
|
QString displayPricePerMonth;
|
||||||
|
};
|
||||||
|
|
||||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||||
{
|
{
|
||||||
QJsonArray services = data.value(configKey::services).toArray();
|
QJsonArray services = data.value(configKey::services).toArray();
|
||||||
@@ -299,20 +311,25 @@ namespace
|
|||||||
});
|
});
|
||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
QHash<QString, QString> idToDisplayPrice;
|
QHash<QString, StoreKitPlanQuote> idToQuote;
|
||||||
idToDisplayPrice.reserve(fetchedProducts.size());
|
idToQuote.reserve(fetchedProducts.size());
|
||||||
for (const QVariantMap &product : fetchedProducts) {
|
for (const QVariantMap &product : fetchedProducts) {
|
||||||
const QString id = product.value(QStringLiteral("productId")).toString();
|
const QString id = product.value(QStringLiteral("productId")).toString();
|
||||||
if (id.isEmpty()) {
|
if (id.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
StoreKitPlanQuote quote;
|
||||||
QString display = product.value(QStringLiteral("displayPrice")).toString();
|
QString display = product.value(QStringLiteral("displayPrice")).toString();
|
||||||
if (display.isEmpty()) {
|
if (display.isEmpty()) {
|
||||||
const QString price = product.value(QStringLiteral("price")).toString();
|
const QString price = product.value(QStringLiteral("price")).toString();
|
||||||
const QString currencyCode = product.value(QStringLiteral("currencyCode")).toString();
|
const QString currencyCode = product.value(QStringLiteral("currencyCode")).toString();
|
||||||
display = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
display = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||||
}
|
}
|
||||||
idToDisplayPrice.insert(id, display);
|
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 i = 0; i < services.size(); ++i) {
|
||||||
@@ -324,25 +341,56 @@ namespace
|
|||||||
QJsonArray plans = description.value(configKey::subscriptionPlans).toArray();
|
QJsonArray plans = description.value(configKey::subscriptionPlans).toArray();
|
||||||
|
|
||||||
QJsonArray mergedPlans;
|
QJsonArray mergedPlans;
|
||||||
|
double minMonthlyAmount = std::numeric_limits<double>::infinity();
|
||||||
|
QString minMonthlyDisplay;
|
||||||
|
|
||||||
for (const QJsonValue &planValue : plans) {
|
for (const QJsonValue &planValue : plans) {
|
||||||
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 storeId = planObject.value(configKey::storeProductId).toString();
|
const QString storeId = planObject.value(configKey::storeProductId).toString();
|
||||||
|
|
||||||
if (storeId.isEmpty()) {
|
if (storeId.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto priceIt = idToDisplayPrice.constFind(storeId);
|
|
||||||
if (priceIt == idToDisplayPrice.cend()) {
|
const auto quoteIt = idToQuote.constFind(storeId);
|
||||||
|
if (quoteIt == idToQuote.cend()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
planObject.insert(configKey::priceLabel, *priceIt);
|
|
||||||
|
const StoreKitPlanQuote "e = *quoteIt;
|
||||||
|
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
||||||
|
|
||||||
|
const double months = quote.subscriptionBillingMonths;
|
||||||
|
if (!trial && months > 1.0 + 1e-6 && !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;
|
||||||
|
const double monthly = quote.priceAmount / monthsForMin;
|
||||||
|
if (monthly < minMonthlyAmount - 1e-9) {
|
||||||
|
minMonthlyAmount = monthly;
|
||||||
|
minMonthlyDisplay = !quote.displayPricePerMonth.isEmpty() ? quote.displayPricePerMonth : quote.displayPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mergedPlans.append(planObject);
|
mergedPlans.append(planObject);
|
||||||
}
|
}
|
||||||
plans = mergedPlans;
|
|
||||||
|
|
||||||
description.insert(configKey::subscriptionPlans, plans);
|
description.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));
|
||||||
|
}
|
||||||
service.insert(configKey::serviceDescription, description);
|
service.insert(configKey::serviceDescription, description);
|
||||||
services.replace(i, service);
|
services.replace(i, service);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,14 +222,8 @@ PageType {
|
|||||||
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
|
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
|
||||||
PageController.showBusyIndicator(true)
|
PageController.showBusyIndicator(true)
|
||||||
var storeId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
|
var storeId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
|
||||||
var ok = ApiConfigsController.importPremiumFromAppStore(storeId)
|
ApiConfigsController.importPremiumFromAppStore(storeId)
|
||||||
PageController.showBusyIndicator(false)
|
PageController.showBusyIndicator(false)
|
||||||
if (!ok) {
|
|
||||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
|
||||||
Qt.openUrlExternally(endpoint)
|
|
||||||
PageController.closePage()
|
|
||||||
PageController.closePage()
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (plan.checkoutUrl) {
|
if (plan.checkoutUrl) {
|
||||||
@@ -238,15 +232,6 @@ PageType {
|
|||||||
PageController.closePage()
|
PageController.closePage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
PageController.showBusyIndicator(true)
|
|
||||||
var importOk = ApiConfigsController.importService()
|
|
||||||
PageController.showBusyIndicator(false)
|
|
||||||
if (!importOk) {
|
|
||||||
var fallbackEndpoint = ApiServicesModel.getStoreEndpoint()
|
|
||||||
Qt.openUrlExternally(fallbackEndpoint)
|
|
||||||
PageController.closePage()
|
|
||||||
PageController.closePage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user