/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "platforms/ios/iosiaphandler.h" #include "constants.h" #include "iosutils.h" #include "leakdetector.h" #include "logger.h" #include "mozillavpn.h" #include "networkrequest.h" #include "settingsholder.h" #include #include #include #include #include #include #import #import namespace { Logger logger(LOG_IAP, "IOSIAPHandler"); } // namespace @interface IOSIAPHandlerDelegate : NSObject { IOSIAPHandler* m_handler; } @end @implementation IOSIAPHandlerDelegate - (id)initWithObject:(IOSIAPHandler*)handler { self = [super init]; if (self) { m_handler = handler; } return self; } - (void)productsRequest:(nonnull SKProductsRequest*)request didReceiveResponse:(nonnull SKProductsResponse*)response { logger.debug() << "Registration completed"; if (response.invalidProductIdentifiers) { NSArray* products = response.invalidProductIdentifiers; logger.error() << "Registration failure" << [products count]; for (unsigned long i = 0, count = [products count]; i < count; ++i) { NSString* identifier = [products objectAtIndex:i]; QMetaObject::invokeMethod(m_handler, "unknownProductRegistered", Qt::QueuedConnection, Q_ARG(QString, QString::fromNSString(identifier))); } } NSArray* products = response.products; if (products) { logger.debug() << "Products registered" << [products count]; for (unsigned long i = 0, count = [products count]; i < count; ++i) { SKProduct* product = [[products objectAtIndex:i] retain]; QMetaObject::invokeMethod(m_handler, "productRegistered", Qt::QueuedConnection, Q_ARG(void*, product)); } } QMetaObject::invokeMethod(m_handler, "productsRegistrationCompleted", Qt::QueuedConnection); [request release]; } - (void)paymentQueue:(nonnull SKPaymentQueue*)queue updatedTransactions:(nonnull NSArray*)transactions { logger.debug() << "payment queue:" << [transactions count]; QStringList completedTransactionIds; bool failedTransactions = false; bool canceledTransactions = false; bool completedTransactions = false; for (SKPaymentTransaction* transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStateFailed: logger.error() << "transaction failed"; if (transaction.error.code == SKErrorPaymentCancelled) { canceledTransactions = true; } else { failedTransactions = true; } break; case SKPaymentTransactionStateRestored: [[fallthrough]]; case SKPaymentTransactionStatePurchased: { QString identifier = QString::fromNSString(transaction.transactionIdentifier); QDateTime date = QDateTime::fromNSDate(transaction.transactionDate); logger.debug() << "transaction purchased - identifier: " << identifier << "- date:" << date.toString(); if (transaction.transactionState == SKPaymentTransactionStateRestored) { SKPaymentTransaction* originalTransaction = transaction.originalTransaction; if (originalTransaction) { QString originalIdentifier = QString::fromNSString(originalTransaction.transactionIdentifier); QDateTime originalDate = QDateTime::fromNSDate(originalTransaction.transactionDate); logger.debug() << "original transaction identifier: " << originalIdentifier << "- date:" << originalDate.toString(); } } completedTransactions = true; SettingsHolder* settingsHolder = SettingsHolder::instance(); if (settingsHolder->hasSubscriptionTransaction(identifier)) { logger.warning() << "This transaction has already been processed. Let's ignore it."; } else { completedTransactionIds.append(identifier); } break; } case SKPaymentTransactionStatePurchasing: logger.debug() << "transaction purchasing"; break; case SKPaymentTransactionStateDeferred: logger.debug() << "transaction deferred"; break; default: logger.warning() << "transaction unknwon state"; break; } } if (!completedTransactions && !canceledTransactions && !failedTransactions) { // Nothing completed, nothing restored, nothing failed. Just purchasing transactions. return; } if (canceledTransactions) { logger.debug() << "Subscription canceled"; QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection); QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection); } else if (failedTransactions) { logger.error() << "Subscription failed"; QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection); QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection); } else if (completedTransactionIds.isEmpty()) { Q_ASSERT(completedTransactions); logger.debug() << "Subscription completed - but all the transactions are known"; QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection); QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection); } else if (AmneziaVPN::instance()->userAuthenticated()) { Q_ASSERT(completedTransactions); logger.debug() << "Subscription completed. Let's start the validation"; QMetaObject::invokeMethod(m_handler, "processCompletedTransactions", Qt::QueuedConnection, Q_ARG(QStringList, completedTransactionIds)); } else { Q_ASSERT(completedTransactions); logger.debug() << "Subscription completed - but the user is not authenticated yet"; QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection); QMetaObject::invokeMethod(m_handler, "subscriptionCanceled", Qt::QueuedConnection); } for (SKPaymentTransaction* transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStateFailed: [[fallthrough]]; case SKPaymentTransactionStateRestored: [[fallthrough]]; case SKPaymentTransactionStatePurchased: [queue finishTransaction:transaction]; break; default: break; } } } - (void)requestDidFinish:(SKRequest*)request { logger.debug() << "Receipt refreshed correctly"; QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection); QMetaObject::invokeMethod(m_handler, "processCompletedTransactions", Qt::QueuedConnection, Q_ARG(QStringList, QStringList())); } - (void)request:(SKRequest*)request didFailWithError:(NSError*)error { logger.error() << "Failed to refresh the receipt" << QString::fromNSString(error.localizedDescription); QMetaObject::invokeMethod(m_handler, "stopSubscription", Qt::QueuedConnection); QMetaObject::invokeMethod(m_handler, "subscriptionFailed", Qt::QueuedConnection); } @end IOSIAPHandler::IOSIAPHandler(QObject* parent) : IAPHandler(parent) { MVPN_COUNT_CTOR(IOSIAPHandler); m_delegate = [[IOSIAPHandlerDelegate alloc] initWithObject:this]; [[SKPaymentQueue defaultQueue] addTransactionObserver:static_cast(m_delegate)]; } IOSIAPHandler::~IOSIAPHandler() { MVPN_COUNT_DTOR(IOSIAPHandler); IOSIAPHandlerDelegate* delegate = static_cast(m_delegate); [[SKPaymentQueue defaultQueue] removeTransactionObserver:delegate]; [delegate dealloc]; m_delegate = nullptr; } void IOSIAPHandler::nativeRegisterProducts() { NSSet* productIdentifiers = [NSSet set]; for (const Product& product : m_products) { productIdentifiers = [productIdentifiers setByAddingObject:product.m_name.toNSString()]; } logger.debug() << "We are about to register" << [productIdentifiers count] << "products"; SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers]; IOSIAPHandlerDelegate* delegate = static_cast(m_delegate); productsRequest.delegate = delegate; [productsRequest start]; } void IOSIAPHandler::nativeStartSubscription(Product* product) { Q_ASSERT(product->m_extra); SKProduct* skProduct = static_cast(product->m_extra); SKPayment* payment = [SKPayment paymentWithProduct:skProduct]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } void IOSIAPHandler::productRegistered(void* a_product) { SKProduct* product = static_cast(a_product); Q_ASSERT(m_productsRegistrationState == eRegistering); logger.debug() << "Product registered"; NSString* nsProductIdentifier = [product productIdentifier]; QString productIdentifier = QString::fromNSString(nsProductIdentifier); Product* productData = findProduct(productIdentifier); Q_ASSERT(productData); logger.debug() << "Id:" << productIdentifier; logger.debug() << "Title:" << QString::fromNSString([product localizedTitle]); logger.debug() << "Description:" << QString::fromNSString([product localizedDescription]); QString priceValue; { NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:(NSNumberFormatterStyle)NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:product.priceLocale]; NSString* price = [numberFormatter stringFromNumber:product.price]; priceValue = QString::fromNSString(price); [numberFormatter release]; } logger.debug() << "Price:" << priceValue; QString monthlyPriceValue; NSDecimalNumber* monthlyPriceNS = nullptr; { NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:(NSNumberFormatterStyle)NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:product.priceLocale]; int32_t mounthCount = productTypeToMonthCount(productData->m_type); Q_ASSERT(mounthCount >= 1); if (mounthCount == 1) { monthlyPriceNS = product.price; } else { NSDecimalNumber* divider = [[NSDecimalNumber alloc] initWithDouble:(double)mounthCount]; monthlyPriceNS = [product.price decimalNumberByDividingBy:divider]; [divider release]; } NSString* price = [numberFormatter stringFromNumber:monthlyPriceNS]; monthlyPriceValue = QString::fromNSString(price); [numberFormatter release]; } logger.debug() << "Monthly Price:" << monthlyPriceValue; productData->m_price = priceValue; productData->m_monthlyPrice = monthlyPriceValue; productData->m_nonLocalizedMonthlyPrice = [monthlyPriceNS doubleValue]; productData->m_extra = product; } void IOSIAPHandler::processCompletedTransactions(const QStringList& ids) { logger.debug() << "process completed transactions"; if (m_subscriptionState != eActive) { logger.warning() << "Random transaction to be completed. Let's ignore it"; return; } QString receipt = IOSUtils::IAPReceipt(); if (receipt.isEmpty()) { logger.warning() << "Empty receipt found"; emit subscriptionFailed(); return; } NetworkRequest* request = NetworkRequest::createForIOSPurchase(this, receipt); connect(request, &NetworkRequest::requestFailed, [this](QNetworkReply::NetworkError error, const QByteArray& data) { logger.error() << "Purchase request failed" << error; if (m_subscriptionState != eActive) { logger.warning() << "We have been canceled in the meantime"; return; } stopSubscription(); QJsonDocument json = QJsonDocument::fromJson(data); if (!json.isObject()) { AmneziaVPN::instance()->errorHandle(ErrorHandler::toErrorType(error)); emit subscriptionFailed(); return; } QJsonObject obj = json.object(); QJsonValue errorValue = obj.value("errno"); if (!errorValue.isDouble()) { AmneziaVPN::instance()->errorHandle(ErrorHandler::toErrorType(error)); emit subscriptionFailed(); return; } int errorNumber = errorValue.toInt(); if (errorNumber != 145) { AmneziaVPN::instance()->errorHandle(ErrorHandler::toErrorType(error)); emit subscriptionFailed(); return; } emit alreadySubscribed(); }); connect(request, &NetworkRequest::requestCompleted, [this, ids](const QByteArray&) { logger.debug() << "Purchase request completed"; SettingsHolder::instance()->addSubscriptionTransactions(ids); if (m_subscriptionState != eActive) { logger.warning() << "We have been canceled in the meantime"; return; } stopSubscription(); emit subscriptionCompleted(); }); }