iOS: fix in app store

Enforce strong reference for SKRequests and delegate objects for transactions to work correctly
This commit is contained in:
Sergey Minakov 2020-10-16 15:58:52 +03:00
parent eb3d7f29be
commit 21bcf3f793
2 changed files with 208 additions and 104 deletions

View File

@ -35,6 +35,17 @@
#include "core/object.h" #include "core/object.h"
#ifdef __OBJC__
@class GodotProductsDelegate;
@class GodotTransactionsObserver;
typedef GodotProductsDelegate InAppStoreProductDelegate;
typedef GodotTransactionsObserver InAppStoreTransactionObserver;
#else
typedef void InAppStoreProductDelegate;
typedef void InAppStoreTransactionObserver;
#endif
class InAppStore : public Object { class InAppStore : public Object {
GDCLASS(InAppStore, Object); GDCLASS(InAppStore, Object);
@ -43,6 +54,9 @@ class InAppStore : public Object {
List<Variant> pending_events; List<Variant> pending_events;
InAppStoreProductDelegate *products_request_delegate;
InAppStoreTransactionObserver *transactions_observer;
public: public:
Error request_product_info(Dictionary p_params); Error request_product_info(Dictionary p_params);
Error restore_purchases(); Error restore_purchases();

View File

@ -32,24 +32,22 @@
#include "in_app_store.h" #include "in_app_store.h"
extern "C" {
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h> #import <StoreKit/StoreKit.h>
};
bool auto_finish_transactions = true; InAppStore *InAppStore::instance = NULL;
NSMutableDictionary *pending_transactions = [NSMutableDictionary dictionary];
static NSArray *latestProducts;
@interface SKProduct (LocalizedPrice) @interface SKProduct (LocalizedPrice)
@property(nonatomic, readonly) NSString *localizedPrice; @property(nonatomic, readonly) NSString *localizedPrice;
@end @end
//----------------------------------// //----------------------------------//
// SKProduct extension // SKProduct extension
//----------------------------------// //----------------------------------//
@implementation SKProduct (LocalizedPrice) @implementation SKProduct (LocalizedPrice)
- (NSString *)localizedPrice { - (NSString *)localizedPrice {
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
@ -61,29 +59,91 @@ static NSArray *latestProducts;
@end @end
InAppStore *InAppStore::instance = NULL; @interface GodotProductsDelegate : NSObject <SKProductsRequestDelegate>
void InAppStore::_bind_methods() { @property(nonatomic, strong) NSMutableArray *loadedProducts;
ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); @property(nonatomic, strong) NSMutableArray *pendingRequests;
ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases);
ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase);
ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); - (void)performRequestWithProductIDs:(NSSet *)productIDs;
ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); - (Error)purchaseProductWithProductID:(NSString *)productID;
ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); - (void)reset;
ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction);
};
@interface ProductsDelegate : NSObject <SKProductsRequestDelegate> {
};
@end @end
@implementation ProductsDelegate @implementation GodotProductsDelegate
- (instancetype)init {
self = [super init];
if (self) {
[self godot_commonInit];
}
return self;
}
- (void)godot_commonInit {
self.loadedProducts = [NSMutableArray new];
self.pendingRequests = [NSMutableArray new];
}
- (void)performRequestWithProductIDs:(NSSet *)productIDs {
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs];
request.delegate = self;
[self.pendingRequests addObject:request];
[request start];
}
- (Error)purchaseProductWithProductID:(NSString *)productID {
SKProduct *product = nil;
NSLog(@"searching for product!");
if (self.loadedProducts) {
for (SKProduct *storedProduct in self.loadedProducts) {
if ([storedProduct.productIdentifier isEqualToString:productID]) {
product = storedProduct;
break;
}
}
}
if (!product) {
return ERR_INVALID_PARAMETER;
}
NSLog(@"product found!");
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
NSLog(@"purchase sent!");
return OK;
}
- (void)reset {
[self.loadedProducts removeAllObjects];
[self.pendingRequests removeAllObjects];
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
[self.pendingRequests removeObject:request];
Dictionary ret;
ret["type"] = "product_info";
ret["result"] = "error";
ret["error"] = String::utf8([error.localizedDescription UTF8String]);
InAppStore::get_singleton()->_post_event(ret);
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
[self.pendingRequests removeObject:request];
NSArray *products = response.products; NSArray *products = response.products;
latestProducts = products; [self.loadedProducts addObjectsFromArray:products];
Dictionary ret; Dictionary ret;
ret["type"] = "product_info"; ret["type"] = "product_info";
@ -107,7 +167,8 @@ void InAppStore::_bind_methods() {
ids.push_back(String::utf8([product.productIdentifier UTF8String])); ids.push_back(String::utf8([product.productIdentifier UTF8String]));
localized_prices.push_back(String::utf8([product.localizedPrice UTF8String])); localized_prices.push_back(String::utf8([product.localizedPrice UTF8String]));
currency_codes.push_back(String::utf8([[[product priceLocale] objectForKey:NSLocaleCurrencyCode] UTF8String])); currency_codes.push_back(String::utf8([[[product priceLocale] objectForKey:NSLocaleCurrencyCode] UTF8String]));
}; }
ret["titles"] = titles; ret["titles"] = titles;
ret["descriptions"] = descriptions; ret["descriptions"] = descriptions;
ret["prices"] = prices; ret["prices"] = prices;
@ -119,50 +180,54 @@ void InAppStore::_bind_methods() {
for (NSString *ipid in response.invalidProductIdentifiers) { for (NSString *ipid in response.invalidProductIdentifiers) {
invalid_ids.push_back(String::utf8([ipid UTF8String])); invalid_ids.push_back(String::utf8([ipid UTF8String]));
}; }
ret["invalid_ids"] = invalid_ids; ret["invalid_ids"] = invalid_ids;
InAppStore::get_singleton()->_post_event(ret); InAppStore::get_singleton()->_post_event(ret);
}; }
@end @end
Error InAppStore::request_product_info(Dictionary p_params) { @interface GodotTransactionsObserver : NSObject <SKPaymentTransactionObserver>
ERR_FAIL_COND_V(!p_params.has("product_ids"), ERR_INVALID_PARAMETER);
PackedStringArray pids = p_params["product_ids"]; @property(nonatomic, assign) BOOL shouldAutoFinishTransactions;
printf("************ request product info! %i\n", pids.size()); @property(nonatomic, strong) NSMutableDictionary *pendingTransactions;
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:pids.size()]; - (void)finishTransactionWithProductID:(NSString *)productID;
for (int i = 0; i < pids.size(); i++) { - (void)reset;
printf("******** adding %s to product list\n", pids[i].utf8().get_data());
NSString *pid = [[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()];
[array addObject:pid];
};
NSSet *products = [[NSSet alloc] initWithArray:array];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:products];
ProductsDelegate *delegate = [[ProductsDelegate alloc] init];
request.delegate = delegate;
[request start];
return OK;
};
Error InAppStore::restore_purchases() {
printf("restoring purchases!\n");
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
return OK;
};
@interface TransObserver : NSObject <SKPaymentTransactionObserver> {
};
@end @end
@implementation TransObserver @implementation GodotTransactionsObserver
- (instancetype)init {
self = [super init];
if (self) {
[self godot_commonInit];
}
return self;
}
- (void)godot_commonInit {
self.pendingTransactions = [NSMutableDictionary new];
}
- (void)finishTransactionWithProductID:(NSString *)productID {
SKPaymentTransaction *transaction = self.pendingTransactions[productID];
if (transaction) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
self.pendingTransactions[productID] = nil;
}
- (void)reset {
[self.pendingTransactions removeAllObjects];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
printf("transactions updated!\n"); printf("transactions updated!\n");
@ -190,6 +255,7 @@ Error InAppStore::restore_purchases() {
receipt = [NSData dataWithContentsOfURL:receiptFileURL]; receipt = [NSData dataWithContentsOfURL:receiptFileURL];
NSString *receipt_to_send = nil; NSString *receipt_to_send = nil;
if (receipt != nil) { if (receipt != nil) {
receipt_to_send = [receipt base64EncodedStringWithOptions:0]; receipt_to_send = [receipt base64EncodedStringWithOptions:0];
} }
@ -200,13 +266,13 @@ Error InAppStore::restore_purchases() {
InAppStore::get_singleton()->_post_event(ret); InAppStore::get_singleton()->_post_event(ret);
if (auto_finish_transactions) { if (self.shouldAutoFinishTransactions) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
} else { } else {
[pending_transactions setObject:transaction forKey:transaction.payment.productIdentifier]; self.pendingTransactions[transaction.payment.productIdentifier] = transaction;
} }
}; break; } break;
case SKPaymentTransactionStateFailed: { case SKPaymentTransactionStateFailed: {
printf("status transaction failed!\n"); printf("status transaction failed!\n");
String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); String pid = String::utf8([transaction.payment.productIdentifier UTF8String]);
@ -231,95 +297,119 @@ Error InAppStore::restore_purchases() {
} break; } break;
default: { default: {
printf("status default %i!\n", (int)transaction.transactionState); printf("status default %i!\n", (int)transaction.transactionState);
}; break; } break;
}; }
}; }
}; }
@end @end
Error InAppStore::purchase(Dictionary p_params) { void InAppStore::_bind_methods() {
ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE); ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info);
if (![SKPaymentQueue canMakePayments]) ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases);
return ERR_UNAVAILABLE; ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase);
printf("purchasing!\n"); ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count);
ERR_FAIL_COND_V(!p_params.has("product_id"), ERR_INVALID_PARAMETER); ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event);
ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction);
ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction);
}
NSString *pid = [[NSString alloc] initWithUTF8String:String(p_params["product_id"]).utf8().get_data()]; Error InAppStore::request_product_info(Dictionary p_params) {
ERR_FAIL_COND_V(!p_params.has("product_ids"), ERR_INVALID_PARAMETER);
SKProduct *product = nil; PackedStringArray pids = p_params["product_ids"];
printf("************ request product info! %i\n", pids.size());
if (latestProducts) { NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:pids.size()];
for (SKProduct *storedProduct in latestProducts) { for (int i = 0; i < pids.size(); i++) {
if ([storedProduct.productIdentifier isEqualToString:pid]) { printf("******** adding %s to product list\n", pids[i].utf8().get_data());
product = storedProduct; NSString *pid = [[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()];
break; [array addObject:pid];
} };
}
}
if (!product) { NSSet *products = [[NSSet alloc] initWithArray:array];
return ERR_INVALID_PARAMETER;
}
SKPayment *payment = [SKPayment paymentWithProduct:product]; [products_request_delegate performRequestWithProductIDs:products];
SKPaymentQueue *defq = [SKPaymentQueue defaultQueue];
[defq addPayment:payment];
printf("purchase sent!\n");
return OK; return OK;
}; }
Error InAppStore::restore_purchases() {
printf("restoring purchases!\n");
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
return OK;
}
Error InAppStore::purchase(Dictionary p_params) {
ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE);
if (![SKPaymentQueue canMakePayments]) {
return ERR_UNAVAILABLE;
}
printf("purchasing!\n");
Dictionary params = p_params;
ERR_FAIL_COND_V(!params.has("product_id"), ERR_INVALID_PARAMETER);
NSString *pid = [[NSString alloc] initWithUTF8String:String(params["product_id"]).utf8().get_data()];
return [products_request_delegate purchaseProductWithProductID:pid];
}
int InAppStore::get_pending_event_count() { int InAppStore::get_pending_event_count() {
return pending_events.size(); return pending_events.size();
}; }
Variant InAppStore::pop_pending_event() { Variant InAppStore::pop_pending_event() {
Variant front = pending_events.front()->get(); Variant front = pending_events.front()->get();
pending_events.pop_front(); pending_events.pop_front();
return front; return front;
}; }
void InAppStore::_post_event(Variant p_event) { void InAppStore::_post_event(Variant p_event) {
pending_events.push_back(p_event); pending_events.push_back(p_event);
}; }
void InAppStore::_record_purchase(String product_id) { void InAppStore::_record_purchase(String product_id) {
String skey = "purchased/" + product_id; String skey = "purchased/" + product_id;
NSString *key = [[NSString alloc] initWithUTF8String:skey.utf8().get_data()]; NSString *key = [[NSString alloc] initWithUTF8String:skey.utf8().get_data()];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key];
[[NSUserDefaults standardUserDefaults] synchronize]; [[NSUserDefaults standardUserDefaults] synchronize];
}; }
InAppStore *InAppStore::get_singleton() { InAppStore *InAppStore::get_singleton() {
return instance; return instance;
}; }
InAppStore::InAppStore() { InAppStore::InAppStore() {
ERR_FAIL_COND(instance != NULL); ERR_FAIL_COND(instance != NULL);
instance = this; instance = this;
auto_finish_transactions = false;
TransObserver *observer = [[TransObserver alloc] init]; products_request_delegate = [[GodotProductsDelegate alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; transactions_observer = [[GodotTransactionsObserver alloc] init];
//pending_transactions = [NSMutableDictionary dictionary];
}; [[SKPaymentQueue defaultQueue] addTransactionObserver:transactions_observer];
}
void InAppStore::finish_transaction(String product_id) { void InAppStore::finish_transaction(String product_id) {
NSString *prod_id = [NSString stringWithCString:product_id.utf8().get_data() encoding:NSUTF8StringEncoding]; NSString *prod_id = [NSString stringWithCString:product_id.utf8().get_data() encoding:NSUTF8StringEncoding];
if ([pending_transactions objectForKey:prod_id]) { [transactions_observer finishTransactionWithProductID:prod_id];
[[SKPaymentQueue defaultQueue] finishTransaction:[pending_transactions objectForKey:prod_id]];
[pending_transactions removeObjectForKey:prod_id];
}
};
void InAppStore::set_auto_finish_transaction(bool b) {
auto_finish_transactions = b;
} }
InAppStore::~InAppStore() {} void InAppStore::set_auto_finish_transaction(bool b) {
transactions_observer.shouldAutoFinishTransactions = b;
}
InAppStore::~InAppStore() {
[products_request_delegate reset];
[transactions_observer reset];
products_request_delegate = nil;
[[SKPaymentQueue defaultQueue] removeTransactionObserver:transactions_observer];
transactions_observer = nil;
}
#endif #endif