Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Yaroslav Yashin
15607f0beb fix: Add transaction details to StoreKit callbacks 2025-06-16 19:05:18 +03:00
Yaroslav
95aad7ac82 fix: init StoreKit controller on startup 2025-06-15 20:22:12 +03:00
Yaroslav
63cd18dd9e Add in-app purchase methods 2025-06-15 20:22:12 +03:00
5 changed files with 224 additions and 0 deletions

View file

@ -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
)

View file

@ -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 <Foundation/Foundation.h>
@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

View file

@ -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 <StoreKit/StoreKit.h>
@interface StoreKitController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
@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<SKPaymentTransaction *> *)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

View file

@ -2,6 +2,7 @@
#define IOS_CONTROLLER_H
#include "protocols/vpnprotocol.h"
#include <functional>
#ifdef __OBJC__
#import <Foundation/Foundation.h>
@ -54,6 +55,14 @@ public:
bool shareText(const QStringList &filesToSend);
QString openFile();
void purchaseProduct(const QString &productId,
std::function<void(bool success,
const QString &transactionId,
const QString &purchasedProductId,
const QString &errorString)> &&callback);
void restorePurchases(std::function<void(bool success,
const QString &errorString)> &&callback);
void requestInetAccess();
signals:
void connectionStateChanged(Vpn::ConnectionState state);

View file

@ -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]
@ -845,6 +849,50 @@ QString IosController::openFile() {
return filePath;
}
void IosController::purchaseProduct(const QString &productId,
std::function<void(bool success,
const QString &transactionId,
const QString &purchasedProductId,
const QString &errorString)> &&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<void(bool success,
const QString &errorString)> &&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) {