mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
feat: add iap support for new premium info page
This commit is contained in:
@@ -105,6 +105,7 @@ public class StoreKit2Helper: NSObject {
|
||||
"title": product.displayName,
|
||||
"description": product.description,
|
||||
"price": "\(product.price)",
|
||||
"displayPrice": product.displayPrice,
|
||||
"currencyCode": currencyCode
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1153,6 +1153,9 @@ void IosController::fetchProducts(const QStringList &productIds,
|
||||
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]);
|
||||
}
|
||||
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
||||
outProducts.push_back(m);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QSet>
|
||||
#include <QVariantMap>
|
||||
|
||||
@@ -40,6 +42,12 @@ 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 primaryRight[] = "primary_right";
|
||||
|
||||
constexpr char apiPayload[] = "api_payload";
|
||||
constexpr char keyPayload[] = "key_payload";
|
||||
|
||||
@@ -239,6 +247,108 @@ namespace
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
QJsonArray services = data.value(configKey::services).toArray();
|
||||
if (services.isEmpty()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
QHash<QString, QString> idToDisplayPrice;
|
||||
idToDisplayPrice.reserve(fetchedProducts.size());
|
||||
for (const QVariantMap &product : fetchedProducts) {
|
||||
const QString id = product.value(QStringLiteral("productId")).toString();
|
||||
if (id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
idToDisplayPrice.insert(id, display);
|
||||
}
|
||||
|
||||
for (int i = 0; i < services.size(); ++i) {
|
||||
QJsonObject service = services.at(i).toObject();
|
||||
if (service.value(configKey::serviceType).toString() != serviceType::amneziaPremium) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject description = service.value(configKey::serviceDescription).toObject();
|
||||
QJsonArray plans = description.value(configKey::subscriptionPlans).toArray();
|
||||
|
||||
QJsonArray mergedPlans;
|
||||
for (const QJsonValue &planValue : plans) {
|
||||
if (!planValue.isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject planObject = planValue.toObject();
|
||||
const QString storeId = planObject.value(configKey::storeProductId).toString();
|
||||
if (storeId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto priceIt = idToDisplayPrice.constFind(storeId);
|
||||
if (priceIt == idToDisplayPrice.cend()) {
|
||||
continue;
|
||||
}
|
||||
planObject.insert(configKey::primaryRight, *priceIt);
|
||||
mergedPlans.append(planObject);
|
||||
}
|
||||
plans = mergedPlans;
|
||||
|
||||
description.insert(configKey::subscriptionPlans, plans);
|
||||
service.insert(configKey::serviceDescription, description);
|
||||
services.replace(i, service);
|
||||
}
|
||||
data.insert(configKey::services, services);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &serversModel,
|
||||
@@ -395,51 +505,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);
|
||||
@@ -457,7 +527,7 @@ bool ApiConfigsController::importService()
|
||||
|
||||
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
||||
if (isIosOrMacOsNe) {
|
||||
return importServiceFromAppStore();
|
||||
return importPremiumFromAppStore(QString());
|
||||
}
|
||||
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
||||
importFreeFromGateway();
|
||||
@@ -466,22 +536,27 @@ bool ApiConfigsController::importService()
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::importServiceFromAppStore()
|
||||
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"),
|
||||
IosController::Instance()->purchaseProduct(productId,
|
||||
[&](bool success, const QString &txId, const QString &purchasedProductId,
|
||||
const QString &originalTxId, const QString &errorString) {
|
||||
purchaseOk = success;
|
||||
originalTransactionId = originalTxId;
|
||||
storeTransactionId = txId;
|
||||
storeProductId = purchasedProductId;
|
||||
purchasedStoreProductId = purchasedProductId;
|
||||
purchaseError = errorString;
|
||||
waitPurchase.quit();
|
||||
});
|
||||
@@ -493,7 +568,7 @@ bool ApiConfigsController::importServiceFromAppStore()
|
||||
return false;
|
||||
}
|
||||
qInfo().noquote() << "[IAP] Purchase success. transactionId =" << storeTransactionId
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << storeProductId;
|
||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << purchasedStoreProductId;
|
||||
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -529,8 +604,11 @@ bool ApiConfigsController::importServiceFromAppStore()
|
||||
}
|
||||
emit installServerFromApiFinished(
|
||||
tr("%1 was added to the app.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
#endif
|
||||
return true;
|
||||
#else
|
||||
Q_UNUSED(storeProductId);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ApiConfigsController::restoreServiceFromAppStore()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef APICONFIGSCONTROLLER_H
|
||||
#define APICONFIGSCONTROLLER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
#include "configurators/openvpn_configurator.h"
|
||||
@@ -31,7 +32,7 @@ public slots:
|
||||
|
||||
bool fillAvailableServices();
|
||||
bool importService();
|
||||
bool importServiceFromAppStore();
|
||||
bool importPremiumFromAppStore(const QString &storeProductId);
|
||||
bool restoreServiceFromAppStore();
|
||||
bool importFreeFromGateway();
|
||||
bool importTrialFromGateway(const QString &email);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QVariantMap>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
@@ -16,12 +15,14 @@ namespace configKey
|
||||
constexpr char checkoutUrl[] = "checkout_url";
|
||||
constexpr char isTrial[] = "is_trial";
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
constexpr char storeProductId[] = "store_product_id";
|
||||
|
||||
constexpr char primaryLeftCamel[] = "primaryLeft";
|
||||
constexpr char primaryRightCamel[] = "primaryRight";
|
||||
constexpr char checkoutUrlCamel[] = "checkoutUrl";
|
||||
constexpr char isTrialCamel[] = "isTrial";
|
||||
constexpr char serviceProtocolCamel[] = "serviceProtocol";
|
||||
constexpr char storeProductIdCamel[] = "storeProductId";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +60,8 @@ QVariant ApiSubscriptionPlansModel::data(const QModelIndex &index, int role) con
|
||||
return plan.isTrial;
|
||||
case ServiceProtocolRole:
|
||||
return plan.serviceProtocol;
|
||||
case StoreProductIdRole:
|
||||
return plan.storeProductId;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -74,6 +77,7 @@ QHash<int, QByteArray> ApiSubscriptionPlansModel::roleNames() const
|
||||
{ CheckoutUrlRole, "checkoutUrl" },
|
||||
{ IsTrialRole, "isTrial" },
|
||||
{ ServiceProtocolRole, "serviceProtocol" },
|
||||
{ StoreProductIdRole, "storeProductId" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,6 +99,7 @@ void ApiSubscriptionPlansModel::updateModel(const QJsonArray &arr)
|
||||
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();
|
||||
@@ -121,6 +126,7 @@ QVariantMap ApiSubscriptionPlansModel::planAt(int row) const
|
||||
planMap.insert(QLatin1String(configKey::checkoutUrlCamel), plan.checkoutUrl);
|
||||
planMap.insert(QLatin1String(configKey::isTrialCamel), plan.isTrial);
|
||||
planMap.insert(QLatin1String(configKey::serviceProtocolCamel), plan.serviceProtocol);
|
||||
planMap.insert(QLatin1String(configKey::storeProductIdCamel), plan.storeProductId);
|
||||
return planMap;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ public:
|
||||
RecommendedRole,
|
||||
CheckoutUrlRole,
|
||||
IsTrialRole,
|
||||
ServiceProtocolRole
|
||||
ServiceProtocolRole,
|
||||
StoreProductIdRole
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
@@ -43,6 +44,7 @@ private:
|
||||
QString checkoutUrl;
|
||||
bool isTrial = false;
|
||||
QString serviceProtocol;
|
||||
QString storeProductId;
|
||||
};
|
||||
|
||||
QVector<SubscriptionPlanItem> m_subscriptionPlans;
|
||||
|
||||
@@ -219,6 +219,19 @@ PageType {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiTrialEmail)
|
||||
return
|
||||
}
|
||||
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
|
||||
PageController.showBusyIndicator(true)
|
||||
var storeId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
|
||||
var ok = ApiConfigsController.importPremiumFromAppStore(storeId)
|
||||
PageController.showBusyIndicator(false)
|
||||
if (!ok) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (plan.checkoutUrl) {
|
||||
Qt.openUrlExternally(plan.checkoutUrl)
|
||||
PageController.closePage()
|
||||
@@ -226,11 +239,11 @@ PageType {
|
||||
return
|
||||
}
|
||||
PageController.showBusyIndicator(true)
|
||||
var ok = ApiConfigsController.importService()
|
||||
var importOk = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
if (!ok) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
if (!importOk) {
|
||||
var fallbackEndpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(fallbackEndpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user