diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index a498a5b1..b1774114 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -34,6 +34,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h ) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE) @@ -46,6 +47,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm ) diff --git a/client/platforms/ios/StoreKitController.h b/client/platforms/ios/StoreKitController.h new file mode 100644 index 00000000..7a29d11b --- /dev/null +++ b/client/platforms/ios/StoreKitController.h @@ -0,0 +1,24 @@ +/* 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/. */ + +#ifndef STOREKITCONTROLLER_H +#define STOREKITCONTROLLER_H + +#import + +@interface StoreKitController : NSObject + ++ (instancetype)sharedInstance; + +- (void)purchaseProduct:(NSString *)productIdentifier + completion:(void (^)(BOOL success, + NSString *_Nullable transactionId, + NSString *_Nullable productId, + NSError *_Nullable error))completion; + +- (void)restorePurchasesWithCompletion:(void (^)(BOOL success, NSError *_Nullable error))completion; + +@end + +#endif // STOREKITCONTROLLER_H diff --git a/client/platforms/ios/StoreKitController.mm b/client/platforms/ios/StoreKitController.mm new file mode 100644 index 00000000..8edaae79 --- /dev/null +++ b/client/platforms/ios/StoreKitController.mm @@ -0,0 +1,141 @@ +/* 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/. */ + +#import "StoreKitController.h" +#import + +@interface StoreKitController () +@property (nonatomic, copy) void (^purchaseCompletion)(BOOL success, + NSString *_Nullable transactionId, + NSString *_Nullable productId, + NSError *_Nullable error); +@property (nonatomic, copy) void (^restoreCompletion)(BOOL success, NSError *_Nullable error); +@property (nonatomic, strong) SKProductsRequest *productsRequest; +@end + +@implementation StoreKitController + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + static StoreKitController *instance; + dispatch_once(&onceToken, ^{ + instance = [[StoreKitController alloc] init]; + }); + return instance; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; + } + return self; +} + +- (void)dealloc +{ + [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; +} + +- (void)purchaseProduct:(NSString *)productIdentifier + completion:(void (^)(BOOL success, + NSString *_Nullable transactionId, + NSString *_Nullable productId, + NSError *_Nullable error))completion +{ + self.purchaseCompletion = completion; + self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productIdentifier]]; + self.productsRequest.delegate = self; + [self.productsRequest start]; +} + +- (void)restorePurchasesWithCompletion:(void (^)(BOOL success, NSError *_Nullable error))completion +{ + self.restoreCompletion = completion; + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; +} + +#pragma mark - SKProductsRequestDelegate + +- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response +{ + SKProduct *product = response.products.firstObject; + if (!product) { + if (self.purchaseCompletion) { + NSError *error = [NSError errorWithDomain:@"StoreKitController" + code:0 + userInfo:@{ NSLocalizedDescriptionKey : @"Product not found" }]; + self.purchaseCompletion(NO, nil, nil, error); + self.purchaseCompletion = nil; + } + return; + } + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [[SKPaymentQueue defaultQueue] addPayment:payment]; + self.productsRequest = nil; +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error +{ + if (self.purchaseCompletion) { + self.purchaseCompletion(NO, nil, nil, error); + self.purchaseCompletion = nil; + } + self.productsRequest = nil; +} + +#pragma mark - SKPaymentTransactionObserver + +- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions +{ + for (SKPaymentTransaction *transaction in transactions) { + switch (transaction.transactionState) { + case SKPaymentTransactionStatePurchased: + if (self.purchaseCompletion) { + self.purchaseCompletion(YES, + transaction.transactionIdentifier, + transaction.payment.productIdentifier, + nil); + self.purchaseCompletion = nil; + } + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + self.productsRequest = nil; + break; + case SKPaymentTransactionStateFailed: + if (self.purchaseCompletion) { + self.purchaseCompletion(NO, + transaction.transactionIdentifier, + transaction.payment.productIdentifier, + transaction.error); + self.purchaseCompletion = nil; + } + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + self.productsRequest = nil; + break; + case SKPaymentTransactionStateRestored: [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; break; + case SKPaymentTransactionStatePurchasing: + case SKPaymentTransactionStateDeferred: break; + } + } +} + +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue +{ + if (self.restoreCompletion) { + self.restoreCompletion(YES, nil); + self.restoreCompletion = nil; + } +} + +- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error +{ + if (self.restoreCompletion) { + self.restoreCompletion(NO, error); + self.restoreCompletion = nil; + } +} + +@end diff --git a/client/platforms/ios/ios_controller.h b/client/platforms/ios/ios_controller.h index 85580769..3d9ccb29 100644 --- a/client/platforms/ios/ios_controller.h +++ b/client/platforms/ios/ios_controller.h @@ -2,6 +2,7 @@ #define IOS_CONTROLLER_H #include "protocols/vpnprotocol.h" +#include #ifdef __OBJC__ #import @@ -54,6 +55,14 @@ public: bool shareText(const QStringList &filesToSend); QString openFile(); + void purchaseProduct(const QString &productId, + std::function &&callback); + void restorePurchases(std::function &&callback); + void requestInetAccess(); signals: void connectionStateChanged(Vpn::ConnectionState state); diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index e64c6dce..8c7e8c9e 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -10,6 +10,7 @@ #include "../protocols/vpnprotocol.h" #import "ios_controller_wrapper.h" +#import "StoreKitController.h" const char* Action::start = "start"; const char* Action::restart = "restart"; @@ -65,6 +66,9 @@ IosController::IosController() : QObject() s_instance = this; m_iosControllerWrapper = [[IosControllerWrapper alloc] initWithCppController:this]; + // Initialize StoreKitController early to start observing the payment queue + [StoreKitController sharedInstance]; + [[NSNotificationCenter defaultCenter] removeObserver: (__bridge NSObject *)m_iosControllerWrapper]; [[NSNotificationCenter defaultCenter] @@ -859,6 +863,50 @@ QString IosController::openFile() { return filePath; } +void IosController::purchaseProduct(const QString &productId, + std::function &&callback) +{ + StoreKitController *controller = [StoreKitController sharedInstance]; + [controller purchaseProduct:productId.toNSString() completion:^(BOOL s, + NSString * _Nullable transactionId, + NSString * _Nullable prodId, + NSError * _Nullable error) { + QString txId; + QString pId; + QString err; + if (transactionId) { + txId = QString::fromUtf8(transactionId.UTF8String); + } + if (prodId) { + pId = QString::fromUtf8(prodId.UTF8String); + } + if (error) { + err = QString::fromUtf8(error.localizedDescription.UTF8String); + } + if (callback) { + callback(s, txId, pId, err); + } + }]; +} + +void IosController::restorePurchases(std::function &&callback) +{ + StoreKitController *controller = [StoreKitController sharedInstance]; + [controller restorePurchasesWithCompletion:^(BOOL s, NSError * _Nullable error) { + QString err; + if (error) { + err = QString::fromUtf8(error.localizedDescription.UTF8String); + } + if (callback) { + callback(s, err); + } + }]; +} + void IosController::requestInetAccess() { NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"]; if (!url) {