1194 lines
43 KiB
Objective-C
1194 lines
43 KiB
Objective-C
#import "UnityPurchasing.h"
|
|
#if MAC_APPSTORE
|
|
#import "Base64.h"
|
|
#endif
|
|
|
|
#if !MAC_APPSTORE
|
|
#import "UnityEarlyTransactionObserver.h"
|
|
#endif
|
|
|
|
@implementation ProductDefinition
|
|
|
|
@synthesize id;
|
|
@synthesize storeSpecificId;
|
|
@synthesize type;
|
|
|
|
@end
|
|
|
|
void UnityPurchasingLog(NSString *format, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, format);
|
|
NSString *message = [[NSString alloc] initWithFormat: format arguments: args];
|
|
va_end(args);
|
|
|
|
NSLog(@"UnityIAP: %@", message);
|
|
}
|
|
|
|
@implementation ReceiptRefresher
|
|
|
|
- (id)initWithCallback:(void (^)(BOOL, NSString*))callbackBlock
|
|
{
|
|
self.callback = callbackBlock;
|
|
return [super init];
|
|
}
|
|
|
|
- (void)requestDidFinish:(SKRequest *)request
|
|
{
|
|
self.callback(true, NULL);
|
|
}
|
|
|
|
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
|
|
{
|
|
NSString* errorMessage = [NSString stringWithFormat: @"Error code: %ld, error description: %@", error.code, error.description];
|
|
self.callback(false, errorMessage);
|
|
}
|
|
|
|
@end
|
|
|
|
#if !MAC_APPSTORE
|
|
@interface UnityPurchasing ()<UnityEarlyTransactionObserverDelegate>
|
|
@end
|
|
#endif
|
|
|
|
@implementation UnityPurchasing
|
|
|
|
// The max time we wait in between retrying failed SKProductRequests.
|
|
static const int MAX_REQUEST_PRODUCT_RETRY_DELAY = 60;
|
|
// The currency code for unknown locales, from https://en.wikipedia.org/wiki/ISO_4217#X_currencies
|
|
static const NSString* ISO_CURRENCY_CODE_UNKNOWN = @"XXX";
|
|
|
|
// Track our accumulated delay.
|
|
int delayInSeconds = 2;
|
|
|
|
- (NSString*)getAppReceipt
|
|
{
|
|
NSBundle* bundle = [NSBundle mainBundle];
|
|
if ([bundle respondsToSelector: @selector(appStoreReceiptURL)])
|
|
{
|
|
NSURL *receiptURL = [bundle appStoreReceiptURL];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath: [receiptURL path]])
|
|
{
|
|
NSData *receipt = [NSData dataWithContentsOfURL: receiptURL];
|
|
|
|
#if MAC_APPSTORE
|
|
// The base64EncodedStringWithOptions method was only added in OSX 10.9.
|
|
NSString* result = [receipt mgb64_base64EncodedString];
|
|
#else
|
|
NSString* result = [receipt base64EncodedStringWithOptions: 0];
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
UnityPurchasingLog(@"No App Receipt found");
|
|
return @"";
|
|
}
|
|
|
|
- (NSDate*)getAppReceiptModificationDate
|
|
{
|
|
NSBundle* bundle = [NSBundle mainBundle];
|
|
if ([bundle respondsToSelector: @selector(appStoreReceiptURL)])
|
|
{
|
|
NSURL *receiptURL = [bundle appStoreReceiptURL];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath: [receiptURL path]])
|
|
{
|
|
NSDate *modDate = [[[NSFileManager defaultManager] attributesOfItemAtPath: [receiptURL path] error: nil] fileModificationDate];
|
|
return modDate;
|
|
}
|
|
}
|
|
|
|
UnityPurchasingLog(@"No App Receipt found");
|
|
return nil;
|
|
}
|
|
|
|
- (NSString*)getTransactionReceiptForProductId:(NSString *)productId
|
|
{
|
|
NSString *result = transactionReceipts[productId];
|
|
if (!result)
|
|
{
|
|
UnityPurchasingLog(@"No Transaction Receipt found for product %@", productId);
|
|
}
|
|
return result ? : @"";
|
|
}
|
|
|
|
- (void)UnitySendMessage:(NSString*)subject payload:(NSString*)payload
|
|
{
|
|
messageCallback(subject.UTF8String, payload.UTF8String, @"".UTF8String, @"".UTF8String, @"".UTF8String, false);
|
|
}
|
|
|
|
- (void)UnitySendMessage:(NSString*)subject payload:(NSString*)payload receipt:(NSString*)receipt
|
|
{
|
|
messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, @"".UTF8String, @"".UTF8String, false);
|
|
}
|
|
|
|
- (void)UnitySendMessage:(NSString*)subject payload:(NSString*)payload receipt:(NSString*)receipt transactionId:(NSString*)transactionId originalTransactionId:(NSString*)originalTransactionId isRestored:(Boolean)isRestored
|
|
{
|
|
messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, transactionId.UTF8String, originalTransactionId.UTF8String, isRestored);
|
|
}
|
|
|
|
- (void)setCallback:(UnityPurchasingCallback)callback
|
|
{
|
|
messageCallback = callback;
|
|
}
|
|
|
|
#if !MAC_APPSTORE
|
|
- (BOOL)isiOS6OrEarlier
|
|
{
|
|
float version = [[[UIDevice currentDevice] systemVersion] floatValue];
|
|
return version < 7;
|
|
}
|
|
|
|
#endif
|
|
|
|
// Retrieve a receipt for the transaction, which will either
|
|
// be the old style transaction receipt on <= iOS 6,
|
|
// or the App Receipt in OSX and iOS 7+.
|
|
- (NSString*)selectReceipt:(SKPaymentTransaction*)transaction
|
|
{
|
|
#if MAC_APPSTORE
|
|
return @"";
|
|
#else
|
|
if ([self isiOS6OrEarlier])
|
|
{
|
|
if (nil == transaction)
|
|
{
|
|
return @"";
|
|
}
|
|
NSString* receipt;
|
|
receipt = [[NSString alloc] initWithData: transaction.transactionReceipt encoding: NSUTF8StringEncoding];
|
|
|
|
return receipt;
|
|
}
|
|
else
|
|
{
|
|
return @"";
|
|
}
|
|
#endif
|
|
}
|
|
|
|
- (void)refreshReceipt
|
|
{
|
|
#if !MAC_APPSTORE
|
|
if ([self isiOS6OrEarlier])
|
|
{
|
|
UnityPurchasingLog(@"RefreshReceipt not supported on iOS < 7!");
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
self.receiptRefresher = [[ReceiptRefresher alloc] initWithCallback:^(BOOL success, NSString* errorMessage) {
|
|
if (success)
|
|
{
|
|
UnityPurchasingLog(@"RefreshReceipt status %d", success);
|
|
[self UnitySendMessage: @"onAppReceiptRefreshed" payload: [self getAppReceipt]];
|
|
}
|
|
else
|
|
{
|
|
UnityPurchasingLog(@"RefreshReceipt status %d - Error message: %@", success, errorMessage);
|
|
[self UnitySendMessage: @"onAppReceiptRefreshFailed" payload: errorMessage];
|
|
}
|
|
}];
|
|
self.refreshRequest = [[SKReceiptRefreshRequest alloc] init];
|
|
self.refreshRequest.delegate = self.receiptRefresher;
|
|
[self.refreshRequest start];
|
|
}
|
|
|
|
// Handle a new or restored purchase transaction by informing Unity.
|
|
- (void)onTransactionSucceeded:(SKPaymentTransaction*)transaction isRestored:(Boolean)isRestored
|
|
{
|
|
NSString* transactionId = transaction.transactionIdentifier;
|
|
NSString* originalTransactionId = transaction.originalTransaction.transactionIdentifier;
|
|
|
|
// This should never happen according to Apple's docs, but it does!
|
|
if (nil == transactionId)
|
|
{
|
|
// Make something up, allowing us to identifiy the transaction when finishing it.
|
|
transactionId = [[NSUUID UUID] UUIDString];
|
|
UnityPurchasingLog(@"Missing transaction Identifier!");
|
|
}
|
|
|
|
// This transaction was marked as finished, but was not cleared from the queue. Try to clear it now, then pass the error up the stack as a DuplicateTransaction
|
|
if ([finishedTransactions containsObject: transactionId])
|
|
{
|
|
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
|
|
UnityPurchasingLog(@"DuplicateTransaction error with product %@ and transactionId %@", transaction.payment.productIdentifier, transactionId);
|
|
[self onPurchaseFailed: transaction.payment.productIdentifier reason: @"DuplicateTransaction" errorCode: @"" errorDescription: @"Duplicate transaction occurred"];
|
|
return; // EARLY RETURN
|
|
}
|
|
|
|
// Item was successfully purchased or restored.
|
|
if (nil == [pendingTransactions objectForKey: transactionId])
|
|
{
|
|
[pendingTransactions setObject: transaction forKey: transactionId];
|
|
}
|
|
|
|
[self UnitySendMessage: @"OnPurchaseSucceeded" payload: transaction.payment.productIdentifier receipt: [self selectReceipt: transaction] transactionId: transactionId originalTransactionId: originalTransactionId isRestored: isRestored];
|
|
}
|
|
|
|
// Called back by managed code when the transaction has been logged.
|
|
- (void)finishTransaction:(NSString *)transactionIdentifier hasProduct:(Boolean)hasProduct
|
|
{
|
|
SKPaymentTransaction* transaction = [pendingTransactions objectForKey: transactionIdentifier];
|
|
if (nil != transaction)
|
|
{
|
|
if (hasProduct)
|
|
{
|
|
UnityPurchasingLog(@"Finishing transaction %@", transactionIdentifier);
|
|
}
|
|
[[SKPaymentQueue defaultQueue] finishTransaction: transaction]; // If this fails (user not logged into the store?), transaction is already removed from pendingTransactions, so future calls to finishTransaction will not retry
|
|
[pendingTransactions removeObjectForKey: transactionIdentifier];
|
|
[finishedTransactions addObject: transactionIdentifier];
|
|
}
|
|
else
|
|
{
|
|
UnityPurchasingLog(@"Transaction %@ not pending, nothing to finish here", transactionIdentifier);
|
|
}
|
|
}
|
|
|
|
// Request information about our products from Apple.
|
|
- (void)requestProducts:(NSSet*)paramIds
|
|
{
|
|
productIds = paramIds;
|
|
UnityPurchasingLog(@"Requesting %lu products", (unsigned long)[productIds count]);
|
|
// Start an immediate poll.
|
|
[self initiateProductPoll: 0];
|
|
}
|
|
|
|
// Execute a product metadata retrieval request via GCD.
|
|
- (void)initiateProductPoll:(int)delayInSeconds
|
|
{
|
|
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
|
|
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
|
|
UnityPurchasingLog(@"Requesting product data...");
|
|
request = [[SKProductsRequest alloc] initWithProductIdentifiers: productIds];
|
|
request.delegate = self;
|
|
[request start];
|
|
});
|
|
}
|
|
|
|
// Called by managed code when a user requests a purchase.
|
|
- (void)purchaseProduct:(ProductDefinition*)productDef
|
|
{
|
|
// Look up our corresponding product.
|
|
SKProduct* requestedProduct = [validProducts objectForKey: productDef.storeSpecificId];
|
|
|
|
if (requestedProduct != nil)
|
|
{
|
|
UnityPurchasingLog(@"PurchaseProduct: %@", requestedProduct.productIdentifier);
|
|
|
|
if ([SKPaymentQueue canMakePayments])
|
|
{
|
|
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct: requestedProduct];
|
|
|
|
// Modify payment request for testing ask-to-buy
|
|
if (_simulateAskToBuyEnabled)
|
|
{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
|
if ([payment respondsToSelector: @selector(setSimulatesAskToBuyInSandbox:)])
|
|
{
|
|
UnityPurchasingLog(@"Queueing payment request with simulatesAskToBuyInSandbox enabled");
|
|
[payment performSelector: @selector(setSimulatesAskToBuyInSandbox:) withObject: @YES];
|
|
//payment.simulatesAskToBuyInSandbox = YES;
|
|
}
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
// Modify payment request with "applicationUsername" for fraud detection
|
|
if (_applicationUsername != nil)
|
|
{
|
|
if ([payment respondsToSelector: @selector(setApplicationUsername:)])
|
|
{
|
|
UnityPurchasingLog(@"Setting applicationUsername to %@", _applicationUsername);
|
|
[payment performSelector: @selector(setApplicationUsername:) withObject: _applicationUsername];
|
|
//payment.applicationUsername = _applicationUsername;
|
|
}
|
|
}
|
|
|
|
[[SKPaymentQueue defaultQueue] addPayment: payment];
|
|
}
|
|
else
|
|
{
|
|
UnityPurchasingLog(@"PurchaseProduct: IAP Disabled");
|
|
[self onPurchaseFailed: productDef.storeSpecificId reason: @"PurchasingUnavailable" errorCode: @"SKErrorPaymentNotAllowed" errorDescription: @"User is not authorized to make payments"];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self onPurchaseFailed: productDef.storeSpecificId reason: @"ItemUnavailable" errorCode: @"" errorDescription: @"Unity IAP could not find requested product"];
|
|
}
|
|
}
|
|
|
|
// Initiate a request to Apple to restore previously made purchases.
|
|
- (void)restorePurchases
|
|
{
|
|
UnityPurchasingLog(@"RestorePurchase");
|
|
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
|
}
|
|
|
|
// A transaction observer should be added at startup (by managed code)
|
|
// and maintained for the life of the app, since transactions can
|
|
// be delivered at any time.
|
|
- (void)addTransactionObserver
|
|
{
|
|
SKPaymentQueue* defaultQueue = [SKPaymentQueue defaultQueue];
|
|
|
|
// Detect whether an existing transaction observer is in place.
|
|
// An existing observer will have processed any transactions already pending,
|
|
// so when we add our own storekit will not call our updatedTransactions handler.
|
|
// We workaround this by explicitly processing any existing transactions if they exist.
|
|
BOOL processExistingTransactions = false;
|
|
if (defaultQueue != nil && defaultQueue.transactions != nil)
|
|
{
|
|
if ([[defaultQueue transactions] count] > 0)
|
|
{
|
|
processExistingTransactions = true;
|
|
}
|
|
}
|
|
|
|
[defaultQueue addTransactionObserver: self];
|
|
if (processExistingTransactions)
|
|
{
|
|
[self paymentQueue: defaultQueue updatedTransactions: defaultQueue.transactions];
|
|
}
|
|
|
|
#if !MAC_APPSTORE
|
|
UnityEarlyTransactionObserver *observer = [UnityEarlyTransactionObserver defaultObserver];
|
|
if (observer)
|
|
{
|
|
observer.readyToReceiveTransactionUpdates = YES;
|
|
if (self.interceptPromotionalPurchases)
|
|
{
|
|
observer.delegate = self;
|
|
}
|
|
else
|
|
{
|
|
[observer initiateQueuedPayments];
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
- (void)initiateQueuedEarlyTransactionObserverPayments
|
|
{
|
|
#if !MAC_APPSTORE
|
|
[[UnityEarlyTransactionObserver defaultObserver] initiateQueuedPayments];
|
|
#endif
|
|
}
|
|
|
|
- (void)presentCodeRedemptionSheet
|
|
{
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 && TARGET_OS_TV == 0 && !MAC_APPSTORE
|
|
if (@available(iOS 14, *))
|
|
{
|
|
[[SKPaymentQueue defaultQueue] presentCodeRedemptionSheet];
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
UnityPurchasingLog(@"Offer Code redemption is available on iOS and iPadOS 14 and later");
|
|
}
|
|
}
|
|
|
|
#if !MAC_APPSTORE
|
|
#pragma mark -
|
|
#pragma mark UnityEarlyTransactionObserverDelegate Methods
|
|
|
|
- (void)promotionalPurchaseAttempted:(SKPayment *)payment
|
|
{
|
|
UnityPurchasingLog(@"Promotional purchase attempted");
|
|
[self UnitySendMessage: @"onPromotionalPurchaseAttempted" payload: payment.productIdentifier];
|
|
}
|
|
|
|
#endif
|
|
|
|
#pragma mark -
|
|
#pragma mark SKProductsRequestDelegate Methods
|
|
|
|
// Store Kit returns a response from an SKProductsRequest.
|
|
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
|
|
{
|
|
UnityPurchasingLog(@"Received %lu products", (unsigned long)[response.products count]);
|
|
// Add the retrieved products to our set of valid products.
|
|
NSDictionary* fetchedProducts = [NSDictionary dictionaryWithObjects: response.products forKeys: [response.products valueForKey: @"productIdentifier"]];
|
|
[validProducts addEntriesFromDictionary: fetchedProducts];
|
|
|
|
NSString* productJSON = [UnityPurchasing serializeProductMetadata: response.products];
|
|
|
|
// Send the app receipt as a separate parameter to avoid JSON parsing a large string.
|
|
[self UnitySendMessage: @"OnProductsRetrieved" payload: productJSON];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark SKPaymentTransactionObserver Methods
|
|
// A product metadata retrieval request failed.
|
|
// We handle it by retrying at an exponentially increasing interval.
|
|
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
|
|
{
|
|
delayInSeconds = MIN(MAX_REQUEST_PRODUCT_RETRY_DELAY, 2 * delayInSeconds);
|
|
UnityPurchasingLog(@"SKProductRequest::didFailWithError: %ld, %@. Unity Purchasing will retry in %i seconds", (long)error.code, error.description, delayInSeconds);
|
|
|
|
[self initiateProductPoll: delayInSeconds];
|
|
}
|
|
|
|
- (void)requestDidFinish:(SKRequest *)req
|
|
{
|
|
request = nil;
|
|
}
|
|
|
|
- (void)onPurchaseFailed:(NSString*)productId reason:(NSString*)reason errorCode:(NSString*)errorCode errorDescription:(NSString*)errorDescription
|
|
{
|
|
NSMutableDictionary* dic = [[NSMutableDictionary alloc] init];
|
|
[dic setObject: productId forKey: @"productId"];
|
|
[dic setObject: reason forKey: @"reason"];
|
|
[dic setObject: errorCode forKey: @"storeSpecificErrorCode"];
|
|
[dic setObject: errorDescription forKey: @"message"];
|
|
|
|
NSData* data = [NSJSONSerialization dataWithJSONObject: dic options: 0 error: nil];
|
|
NSString* result = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
|
|
|
|
[self UnitySendMessage: @"OnPurchaseFailed" payload: result];
|
|
}
|
|
|
|
- (NSString*)purchaseErrorCodeToReason:(NSInteger)errorCode
|
|
{
|
|
switch (errorCode)
|
|
{
|
|
case SKErrorPaymentCancelled:
|
|
return @"UserCancelled";
|
|
case SKErrorPaymentInvalid:
|
|
return @"PaymentDeclined";
|
|
case SKErrorPaymentNotAllowed:
|
|
return @"PurchasingUnavailable";
|
|
}
|
|
|
|
return @"Unknown";
|
|
}
|
|
|
|
// The transaction status of the SKPaymentQueue is sent here.
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
|
|
{
|
|
UnityPurchasingLog(@"UpdatedTransactions");
|
|
for (SKPaymentTransaction *transaction in transactions)
|
|
{
|
|
[self handleTransaction: transaction];
|
|
}
|
|
}
|
|
|
|
// Called when one or more transactions have been removed from the queue.
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions
|
|
{
|
|
// Nothing to do here.
|
|
}
|
|
|
|
// Called when SKPaymentQueue has finished sending restored transactions.
|
|
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
|
|
{
|
|
UnityPurchasingLog(@"PaymentQueueRestoreCompletedTransactionsFinished");
|
|
[self UnitySendMessage: @"onTransactionsRestoredSuccess" payload: @""];
|
|
}
|
|
|
|
// Called if an error occurred while restoring transactions.
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
|
|
{
|
|
UnityPurchasingLog(@"restoreCompletedTransactionsFailedWithError");
|
|
// Restore was cancelled or an error occurred, so notify user.
|
|
|
|
[self UnitySendMessage: @"onTransactionsRestoredFail" payload: error.localizedDescription];
|
|
}
|
|
|
|
// Called when an entitlement was revoked.
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue didRevokeEntitlementsForProductIdentifiers:(NSArray *)productIdentifiers
|
|
{
|
|
UnityPurchasingLog(@"didRevokeEntitlementsForProductIdentifiers");
|
|
NSString* productIdsJSON = [UnityPurchasing serializeProductIdList: productIdentifiers];
|
|
|
|
[self UnitySendMessage: @"onEntitlementsRevoked" payload: productIdsJSON];
|
|
}
|
|
|
|
- (void)handleTransaction:(SKPaymentTransaction *)transaction
|
|
{
|
|
if (transaction.payment.productIdentifier == nil)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SKProduct* product = [validProducts objectForKey: transaction.payment.productIdentifier];
|
|
|
|
switch (transaction.transactionState)
|
|
{
|
|
case SKPaymentTransactionStatePurchasing:
|
|
// Item is still in the process of being purchased
|
|
break;
|
|
case SKPaymentTransactionStatePurchased:
|
|
[self handleTransactionPurchased: transaction forProduct: product];
|
|
break;
|
|
case SKPaymentTransactionStateRestored:
|
|
[self handleTransactionRestored: transaction forProduct: product];
|
|
break;
|
|
case SKPaymentTransactionStateDeferred:
|
|
[self handleTransactionDeferred: transaction forProduct: product];
|
|
break;
|
|
case SKPaymentTransactionStateFailed:
|
|
[self handleTransactionFailed: transaction];
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)handleTransactionPurchased:(SKPaymentTransaction*)transaction forProduct:(SKProduct*)product
|
|
{
|
|
#if MAC_APPSTORE
|
|
// There is no transactionReceipt on Mac
|
|
NSString* receipt = @"";
|
|
#else
|
|
// The transactionReceipt field is deprecated, but is being used here to validate Ask-To-Buy purchases
|
|
NSString* receipt = [transaction.transactionReceipt base64EncodedStringWithOptions: 0];
|
|
#endif
|
|
|
|
transactionReceipts[transaction.payment.productIdentifier] = receipt;
|
|
|
|
if (product != nil)
|
|
{
|
|
[self onTransactionSucceeded: transaction isRestored: false];
|
|
}
|
|
}
|
|
|
|
- (void)handleTransactionRestored:(SKPaymentTransaction*)transaction forProduct:(SKProduct*)product
|
|
{
|
|
if (product != nil)
|
|
{
|
|
[self onTransactionSucceeded: transaction isRestored: true];
|
|
}
|
|
}
|
|
|
|
- (void)handleTransactionDeferred:(SKPaymentTransaction*)transaction forProduct:(SKProduct*)product
|
|
{
|
|
if (product != nil)
|
|
{
|
|
UnityPurchasingLog(@"PurchaseDeferred");
|
|
[self UnitySendMessage: @"onProductPurchaseDeferred" payload: transaction.payment.productIdentifier];
|
|
}
|
|
}
|
|
|
|
- (void)handleTransactionFailed:(SKPaymentTransaction*)transaction
|
|
{
|
|
// Purchase was either cancelled by user or an error occurred.
|
|
NSString* errorCode = [NSString stringWithFormat: @"%ld", (long)transaction.error.code];
|
|
UnityPurchasingLog(@"PurchaseFailed: %@", errorCode);
|
|
|
|
NSString* reason = [self purchaseErrorCodeToReason: transaction.error.code];
|
|
NSString* errorCodeString = [UnityPurchasing storeKitErrorCodeNames][@(transaction.error.code)];
|
|
if (errorCodeString == nil)
|
|
{
|
|
errorCodeString = @"SKErrorUnknown";
|
|
}
|
|
NSString* errorDescription = [NSString stringWithFormat: @"APPLE_%@", transaction.error.localizedDescription];
|
|
[self onPurchaseFailed: transaction.payment.productIdentifier reason: reason errorCode: errorCodeString errorDescription: errorDescription];
|
|
|
|
// Finished transactions should be removed from the payment queue.
|
|
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
|
|
}
|
|
|
|
- (void)fetchStorePromotionOrder
|
|
{
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
if (@available(iOS 11.0, *))
|
|
{
|
|
[[SKProductStorePromotionController defaultController] fetchStorePromotionOrderWithCompletionHandler:^(NSArray<SKProduct *> * _Nonnull storePromotionOrder, NSError * _Nullable error) {
|
|
if (error)
|
|
{
|
|
UnityPurchasingLog(@"Error in fetchStorePromotionOrder: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
|
|
|
|
[self UnitySendMessage: @"onFetchStorePromotionOrderFailed" payload: nil];
|
|
}
|
|
else
|
|
{
|
|
UnityPurchasingLog(@"Fetched %lu store-promotion ordered products", (unsigned long)[storePromotionOrder count]);
|
|
|
|
NSString *productIdsJSON = [UnityPurchasing serializeSKProductIdList: storePromotionOrder];
|
|
|
|
[self UnitySendMessage: @"onFetchStorePromotionOrderSucceeded" payload: productIdsJSON];
|
|
}
|
|
}];
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
UnityPurchasingLog(@"Fetch store promotion order is only available on iOS and tvOS 11 or later");
|
|
[self UnitySendMessage: @"onFetchStorePromotionOrderFailed" payload: nil];
|
|
}
|
|
}
|
|
|
|
- (void)updateStorePromotionOrder:(NSArray*)productIds
|
|
{
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
if (@available(iOS 11_0, *))
|
|
{
|
|
NSMutableArray* products = [[NSMutableArray alloc] init];
|
|
|
|
for (NSString* productId in productIds)
|
|
{
|
|
SKProduct* product = [validProducts objectForKey: productId];
|
|
if (product)
|
|
[products addObject: product];
|
|
}
|
|
|
|
SKProductStorePromotionController* controller = [SKProductStorePromotionController defaultController];
|
|
[controller updateStorePromotionOrder: products completionHandler:^(NSError* error) {
|
|
if (error)
|
|
UnityPurchasingLog(@"Error in updateStorePromotionOrder: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
|
|
}];
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
UnityPurchasingLog(@"Update store promotion order is only available on iOS and tvOS 11 or later");
|
|
}
|
|
}
|
|
|
|
- (void)fetchStorePromotionVisibilityForProduct:(NSString*)productId
|
|
{
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
if (@available(iOS 11.0, macOS 11.0, tvOS 11.0, *))
|
|
{
|
|
SKProduct *product = [validProducts objectForKey: productId];
|
|
|
|
[[SKProductStorePromotionController defaultController]
|
|
fetchStorePromotionVisibilityForProduct: product completionHandler:^(SKProductStorePromotionVisibility storePromotionVisibility, NSError * _Nullable error) {
|
|
if (error)
|
|
{
|
|
UnityPurchasingLog(@"Error in fetchStorePromotionVisibilityForProduct: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
|
|
|
|
[self UnitySendMessage: @"onFetchStorePromotionVisibilityFailed" payload: nil];
|
|
}
|
|
else
|
|
{
|
|
NSString *visibility = [UnityPurchasing getStringForStorePromotionVisibility: storePromotionVisibility];
|
|
|
|
UnityPurchasingLog(@"Fetched Store Promotion Visibility for %@", product.productIdentifier);
|
|
|
|
NSString *payload = [UnityPurchasing serializeVisibilityResultForProduct: productId withVisiblity: visibility];
|
|
|
|
[self UnitySendMessage: @"onFetchStorePromotionVisibilitySucceeded" payload: payload];
|
|
}
|
|
}
|
|
];
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
UnityPurchasingLog(@"Fetch store promotion visibility is only available on iOS, macOS and tvOS 11 or later");
|
|
[self UnitySendMessage: @"onFetchStorePromotionVisibilityFailed" payload: nil];
|
|
}
|
|
}
|
|
|
|
// visibility should be one of "Default", "Hide", or "Show"
|
|
- (void)updateStorePromotionVisibility:(NSString*)visibility forProduct:(NSString*)productId
|
|
{
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
if (@available(iOS 11_0, *))
|
|
{
|
|
SKProduct *product = [validProducts objectForKey: productId];
|
|
if (!product)
|
|
{
|
|
UnityPurchasingLog(@"updateStorePromotionVisibility unable to find product %@", productId);
|
|
return;
|
|
}
|
|
|
|
SKProductStorePromotionVisibility v = SKProductStorePromotionVisibilityDefault;
|
|
if ([visibility isEqualToString: @"Hide"])
|
|
v = SKProductStorePromotionVisibilityHide;
|
|
else if ([visibility isEqualToString: @"Show"])
|
|
v = SKProductStorePromotionVisibilityShow;
|
|
|
|
SKProductStorePromotionController* controller = [SKProductStorePromotionController defaultController];
|
|
[controller updateStorePromotionVisibility: v forProduct: product completionHandler:^(NSError* error) {
|
|
if (error)
|
|
UnityPurchasingLog(@"Error in updateStorePromotionVisibility: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
|
|
}];
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
UnityPurchasingLog(@"Update store promotion visibility is only available on iOS and tvOS 11 or later");
|
|
}
|
|
}
|
|
|
|
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product
|
|
{
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
if (@available(iOS 11_0, *))
|
|
{
|
|
// Just defer to the early transaction observer. This should have no effect, just return whatever the observer returns.
|
|
return [[UnityEarlyTransactionObserver defaultObserver] paymentQueue: queue shouldAddStorePayment: payment forProduct: product];
|
|
}
|
|
#endif
|
|
return YES;
|
|
}
|
|
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
|
|
+ (NSString*)getStringForStorePromotionVisibility:(SKProductStorePromotionVisibility)storePromotionVisibility
|
|
API_AVAILABLE(macos(11.0), ios(11.0), tvos(11.0))
|
|
{
|
|
switch (storePromotionVisibility)
|
|
{
|
|
case SKProductStorePromotionVisibilityShow:
|
|
return @"Show";
|
|
case SKProductStorePromotionVisibilityHide:
|
|
return @"Hide";
|
|
case SKProductStorePromotionVisibilityDefault:
|
|
return @"Default";
|
|
default:
|
|
return @"Default";
|
|
}
|
|
}
|
|
#endif
|
|
|
|
+ (ProductDefinition*)decodeProductDefinition:(NSDictionary*)hash
|
|
{
|
|
ProductDefinition* product = [[ProductDefinition alloc] init];
|
|
product.id = [hash objectForKey: @"id"];
|
|
product.storeSpecificId = [hash objectForKey: @"storeSpecificId"];
|
|
product.type = [hash objectForKey: @"type"];
|
|
return product;
|
|
}
|
|
|
|
+ (NSArray*)deserializeProductDefs:(NSString*)json
|
|
{
|
|
NSData* data = [json dataUsingEncoding: NSUTF8StringEncoding];
|
|
NSArray* hashes = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil];
|
|
|
|
NSMutableArray* result = [[NSMutableArray alloc] init];
|
|
for (NSDictionary* hash in hashes)
|
|
{
|
|
[result addObject: [self decodeProductDefinition: hash]];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
+ (ProductDefinition*)deserializeProductDef:(NSString*)json
|
|
{
|
|
NSData* data = [json dataUsingEncoding: NSUTF8StringEncoding];
|
|
NSDictionary* hash = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil];
|
|
return [self decodeProductDefinition: hash];
|
|
}
|
|
|
|
+ (NSString*)serializeProductMetadata:(NSArray*)appleProducts
|
|
{
|
|
NSMutableArray* hashes = [[NSMutableArray alloc] init];
|
|
for (id product in appleProducts)
|
|
{
|
|
if (NULL == [product productIdentifier])
|
|
{
|
|
UnityPurchasingLog(@"Product is missing an identifier!");
|
|
continue;
|
|
}
|
|
|
|
NSMutableDictionary* hash = [[NSMutableDictionary alloc] init];
|
|
[hashes addObject: hash];
|
|
|
|
[hash setObject: [product productIdentifier] forKey: @"storeSpecificId"];
|
|
|
|
NSMutableDictionary* metadata = [[NSMutableDictionary alloc] init];
|
|
[hash setObject: metadata forKey: @"metadata"];
|
|
|
|
if (NULL != [product price])
|
|
{
|
|
[metadata setObject: [product price] forKey: @"localizedPrice"];
|
|
}
|
|
|
|
if (NULL != [product priceLocale])
|
|
{
|
|
NSString *currencyCode = [[product priceLocale] objectForKey: NSLocaleCurrencyCode];
|
|
// NSLocaleCurrencyCode has been seen to return nil. Avoid crashing and report the issue to the log. E.g. https://developer.apple.com/forums/thread/119838
|
|
if (currencyCode != nil)
|
|
{
|
|
[metadata setObject: currencyCode forKey: @"isoCurrencyCode"];
|
|
}
|
|
else
|
|
{
|
|
UnityPurchasingLog(@"Error: unable to determine localized currency code for product {%@}, [SKProduct priceLocale] identifier {%@}. NSLocaleCurrencyCode {%@} is nil. Using ISO Unknown Currency code, instead: {%@}.", [product productIdentifier], [[product priceLocale] localeIdentifier], currencyCode, ISO_CURRENCY_CODE_UNKNOWN);
|
|
[metadata setObject: ISO_CURRENCY_CODE_UNKNOWN forKey: @"isoCurrencyCode"];
|
|
}
|
|
}
|
|
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
|
if (@available(iOS 14, macOS 11.0,*))
|
|
{
|
|
[product isFamilyShareable] ? [metadata setObject: @"true" forKey: @"isFamilyShareable"] : [metadata setObject: @"false" forKey: @"isFamilyShareable"];
|
|
}
|
|
#endif
|
|
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 || __TV_OS_VERSION_MAX_ALLOWED >= 110000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
|
|
if ((@available(iOS 11_2, macOS 10_13_2, tvOS 11_2, *)) && (nil != [product introductoryPrice]))
|
|
{
|
|
[metadata setObject: [[product introductoryPrice] price] forKey: @"introductoryPrice"];
|
|
if (nil != [[product introductoryPrice] priceLocale])
|
|
{
|
|
NSString *currencyCode = [[[product introductoryPrice] priceLocale] objectForKey: NSLocaleCurrencyCode];
|
|
[metadata setObject: currencyCode forKey: @"introductoryPriceLocale"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"introductoryPriceLocale"];
|
|
}
|
|
if (nil != [[product introductoryPrice] numberOfPeriods])
|
|
{
|
|
NSNumber *numberOfPeriods = [NSNumber numberWithInt: [[product introductoryPrice] numberOfPeriods]];
|
|
[metadata setObject: numberOfPeriods forKey: @"introductoryPriceNumberOfPeriods"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"introductoryPriceNumberOfPeriods"];
|
|
}
|
|
if (nil != [[product introductoryPrice] subscriptionPeriod])
|
|
{
|
|
if (nil != [[[product introductoryPrice] subscriptionPeriod] numberOfUnits])
|
|
{
|
|
NSNumber *numberOfUnits = [NSNumber numberWithInt: [[[product introductoryPrice] subscriptionPeriod] numberOfUnits]];
|
|
[metadata setObject: numberOfUnits forKey: @"numberOfUnits"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"numberOfUnits"];
|
|
}
|
|
if (nil != [[[product introductoryPrice] subscriptionPeriod] unit])
|
|
{
|
|
NSNumber *unit = [NSNumber numberWithInt: [[[product introductoryPrice] subscriptionPeriod] unit]];
|
|
[metadata setObject: unit forKey: @"unit"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"unit"];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"numberOfUnits"];
|
|
[metadata setObject: @"" forKey: @"unit"];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"introductoryPrice"];
|
|
[metadata setObject: @"" forKey: @"introductoryPriceLocale"];
|
|
[metadata setObject: @"" forKey: @"introductoryPriceNumberOfPeriods"];
|
|
[metadata setObject: @"" forKey: @"numberOfUnits"];
|
|
[metadata setObject: @"" forKey: @"unit"];
|
|
}
|
|
|
|
if ((@available(iOS 11_2, macOS 10_13_2, tvOS 11_2, *)) && (nil != [product subscriptionPeriod]))
|
|
{
|
|
if (nil != [[product subscriptionPeriod] numberOfUnits])
|
|
{
|
|
NSNumber *numberOfUnits = [NSNumber numberWithInt: [[product subscriptionPeriod] numberOfUnits]];
|
|
[metadata setObject: numberOfUnits forKey: @"subscriptionNumberOfUnits"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"subscriptionNumberOfUnits"];
|
|
}
|
|
if (nil != [[product subscriptionPeriod] unit])
|
|
{
|
|
NSNumber *unit = [NSNumber numberWithInt: [[product subscriptionPeriod] unit]];
|
|
[metadata setObject: unit forKey: @"subscriptionPeriodUnit"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"subscriptionPeriodUnit"];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: @"" forKey: @"subscriptionNumberOfUnits"];
|
|
[metadata setObject: @"" forKey: @"subscriptionPeriodUnit"];
|
|
}
|
|
#else
|
|
[metadata setObject: @"" forKey: @"introductoryPrice"];
|
|
[metadata setObject: @"" forKey: @"introductoryPriceLocale"];
|
|
[metadata setObject: @"" forKey: @"introductoryPriceNumberOfPeriods"];
|
|
[metadata setObject: @"" forKey: @"numberOfUnits"];
|
|
[metadata setObject: @"" forKey: @"unit"];
|
|
[metadata setObject: @"" forKey: @"subscriptionNumberOfUnits"];
|
|
[metadata setObject: @"" forKey: @"subscriptionPeriodUnit"];
|
|
#endif
|
|
|
|
|
|
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
|
|
[numberFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4];
|
|
[numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle];
|
|
[numberFormatter setLocale: [product priceLocale]];
|
|
NSString *formattedString = [numberFormatter stringFromNumber: [product price]];
|
|
|
|
if (NULL == formattedString)
|
|
{
|
|
UnityPurchasingLog(@"Unable to format a localized price");
|
|
[metadata setObject: @"" forKey: @"localizedPriceString"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: formattedString forKey: @"localizedPriceString"];
|
|
}
|
|
if (NULL == [product localizedTitle])
|
|
{
|
|
UnityPurchasingLog(@"No localized title for: %@. Have your products been disapproved in itunes connect?", [product productIdentifier]);
|
|
[metadata setObject: @"" forKey: @"localizedTitle"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: [product localizedTitle] forKey: @"localizedTitle"];
|
|
}
|
|
|
|
if (NULL == [product localizedDescription])
|
|
{
|
|
UnityPurchasingLog(@"No localized description for: %@. Have your products been disapproved in itunes connect?", [product productIdentifier]);
|
|
[metadata setObject: @"" forKey: @"localizedDescription"];
|
|
}
|
|
else
|
|
{
|
|
[metadata setObject: [product localizedDescription] forKey: @"localizedDescription"];
|
|
}
|
|
}
|
|
|
|
|
|
NSData *data = [NSJSONSerialization dataWithJSONObject: hashes options: 0 error: nil];
|
|
return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
|
|
}
|
|
|
|
+ (NSString*)serializeSKProductIdList:(NSArray<SKProduct *> *)products
|
|
{
|
|
NSMutableArray *productIds = [NSMutableArray arrayWithCapacity: products.count];
|
|
for (SKProduct *product in products)
|
|
{
|
|
[productIds addObject: product.productIdentifier];
|
|
}
|
|
|
|
return [UnityPurchasing serializeProductIdList: products];
|
|
}
|
|
|
|
+ (NSString*)serializeProductIdList:(NSArray*)products
|
|
{
|
|
NSData *data = [NSJSONSerialization dataWithJSONObject: products options: 0 error: nil];
|
|
return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
|
|
}
|
|
|
|
+ (NSArray*)deserializeProductIdList:(NSString*)json
|
|
{
|
|
NSData* data = [json dataUsingEncoding: NSUTF8StringEncoding];
|
|
NSDictionary* dict = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil];
|
|
return [[dict objectForKey: @"products"] copy];
|
|
}
|
|
|
|
+ (NSString*)serializeVisibilityResultForProduct:(NSString *)productId withVisiblity:(NSString *)visibility
|
|
{
|
|
NSDictionary *result = @{@"productId": productId, @"visibility": visibility};
|
|
|
|
NSData *data = [NSJSONSerialization dataWithJSONObject: result options: 0 error: nil];
|
|
return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
|
|
}
|
|
|
|
// Note: this will need to be updated if Apple ever adds more StoreKit error codes.
|
|
+ (NSDictionary<NSNumber *, NSString *> *)storeKitErrorCodeNames
|
|
{
|
|
return @{
|
|
@(SKErrorUnknown): @"SKErrorUnknown",
|
|
@(SKErrorClientInvalid): @"SKErrorClientInvalid",
|
|
@(SKErrorPaymentCancelled): @"SKErrorPaymentCancelled",
|
|
@(SKErrorPaymentInvalid): @"SKErrorPaymentInvalid",
|
|
@(SKErrorPaymentNotAllowed): @"SKErrorPaymentNotAllowed",
|
|
#if !MAC_APPSTORE
|
|
@(SKErrorStoreProductNotAvailable): @"SKErrorStoreProductNotAvailable",
|
|
@(SKErrorCloudServicePermissionDenied): @"SKErrorCloudServicePermissionDenied",
|
|
@(SKErrorCloudServiceNetworkConnectionFailed): @"SKErrorCloudServiceNetworkConnectionFailed",
|
|
#endif
|
|
#if !MAC_APPSTORE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 103000 || __TV_OS_VERSION_MAX_ALLOWED >= 103000)
|
|
@(SKErrorCloudServiceRevoked): @"SKErrorCloudServiceRevoked",
|
|
#endif
|
|
};
|
|
}
|
|
|
|
#pragma mark - Internal Methods & Events
|
|
|
|
- (id)init
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
validProducts = [[NSMutableDictionary alloc] init];
|
|
pendingTransactions = [[NSMutableDictionary alloc] init];
|
|
finishedTransactions = [[NSMutableSet alloc] init];
|
|
transactionReceipts = [[NSMutableDictionary alloc] init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
UnityPurchasing* UnityPurchasing_instance = NULL;
|
|
|
|
UnityPurchasing* UnityPurchasing_getInstance()
|
|
{
|
|
if (NULL == UnityPurchasing_instance)
|
|
{
|
|
UnityPurchasing_instance = [[UnityPurchasing alloc] init];
|
|
}
|
|
return UnityPurchasing_instance;
|
|
}
|
|
|
|
// Make a heap allocated copy of a string.
|
|
// This is suitable for passing to managed code,
|
|
// which will free the string when it is garbage collected.
|
|
// Stack allocated variables must not be returned as results
|
|
// from managed to native calls.
|
|
char* UnityPurchasingMakeHeapAllocatedStringCopy(NSString* string)
|
|
{
|
|
if (NULL == string)
|
|
{
|
|
return NULL;
|
|
}
|
|
char* res = (char*)malloc([string length] + 1);
|
|
strcpy(res, [string UTF8String]);
|
|
return res;
|
|
}
|
|
|
|
void setUnityPurchasingCallback(UnityPurchasingCallback callback)
|
|
{
|
|
[UnityPurchasing_getInstance() setCallback: callback];
|
|
}
|
|
|
|
void unityPurchasingRetrieveProducts(const char* json)
|
|
{
|
|
NSString* str = [NSString stringWithUTF8String: json];
|
|
NSArray* productDefs = [UnityPurchasing deserializeProductDefs: str];
|
|
NSMutableSet* productIds = [[NSMutableSet alloc] init];
|
|
for (ProductDefinition* product in productDefs)
|
|
{
|
|
[productIds addObject: product.storeSpecificId];
|
|
}
|
|
[UnityPurchasing_getInstance() requestProducts: productIds];
|
|
}
|
|
|
|
void unityPurchasingPurchase(const char* json, const char* developerPayload)
|
|
{
|
|
NSString* str = [NSString stringWithUTF8String: json];
|
|
ProductDefinition* product = [UnityPurchasing deserializeProductDef: str];
|
|
[UnityPurchasing_getInstance() purchaseProduct: product];
|
|
}
|
|
|
|
void unityPurchasingFinishTransaction(const char* productJSON, const char* transactionId)
|
|
{
|
|
if (transactionId == NULL)
|
|
return;
|
|
Boolean hasProduct = productJSON != NULL;
|
|
NSString* tranId = [NSString stringWithUTF8String: transactionId];
|
|
[UnityPurchasing_getInstance() finishTransaction: tranId hasProduct: hasProduct];
|
|
}
|
|
|
|
void unityPurchasingRestoreTransactions()
|
|
{
|
|
UnityPurchasingLog(@"Restore transactions");
|
|
[UnityPurchasing_getInstance() restorePurchases];
|
|
}
|
|
|
|
void unityPurchasingAddTransactionObserver()
|
|
{
|
|
UnityPurchasingLog(@"Add transaction observer");
|
|
[UnityPurchasing_getInstance() addTransactionObserver];
|
|
}
|
|
|
|
void unityPurchasingRefreshAppReceipt()
|
|
{
|
|
UnityPurchasingLog(@"Refresh app receipt");
|
|
[UnityPurchasing_getInstance() refreshReceipt];
|
|
}
|
|
|
|
char* getUnityPurchasingAppReceipt()
|
|
{
|
|
@autoreleasepool {
|
|
NSString* receipt = [UnityPurchasing_getInstance() getAppReceipt];
|
|
return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
|
|
}
|
|
}
|
|
|
|
double getUnityPurchasingAppReceiptModificationDate()
|
|
{
|
|
NSDate* receiptModificationDate = [UnityPurchasing_getInstance() getAppReceiptModificationDate];
|
|
return receiptModificationDate.timeIntervalSince1970;
|
|
}
|
|
|
|
char* getUnityPurchasingTransactionReceiptForProductId(const char *productId)
|
|
{
|
|
NSString* receipt = [UnityPurchasing_getInstance() getTransactionReceiptForProductId: [NSString stringWithUTF8String: productId]];
|
|
return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
|
|
}
|
|
|
|
BOOL getUnityPurchasingCanMakePayments()
|
|
{
|
|
return [SKPaymentQueue canMakePayments];
|
|
}
|
|
|
|
void setSimulateAskToBuy(BOOL enabled)
|
|
{
|
|
UnityPurchasingLog(@"Set simulate Ask To Buy %@", enabled ? @"true" : @"false");
|
|
UnityPurchasing_getInstance().simulateAskToBuyEnabled = enabled;
|
|
}
|
|
|
|
BOOL getSimulateAskToBuy()
|
|
{
|
|
return UnityPurchasing_getInstance().simulateAskToBuyEnabled;
|
|
}
|
|
|
|
void unityPurchasingSetApplicationUsername(const char *username)
|
|
{
|
|
if (username == NULL)
|
|
return;
|
|
UnityPurchasing_getInstance().applicationUsername = [NSString stringWithUTF8String: username];
|
|
}
|
|
|
|
void unityPurchasingFetchStorePromotionOrder(void)
|
|
{
|
|
[UnityPurchasing_getInstance() fetchStorePromotionOrder];
|
|
}
|
|
|
|
void unityPurchasingFetchStorePromotionVisibility(const char *productId)
|
|
{
|
|
NSString* prodId = [NSString stringWithUTF8String: productId];
|
|
[UnityPurchasing_getInstance() fetchStorePromotionVisibilityForProduct: prodId];
|
|
}
|
|
|
|
// Expects json in this format:
|
|
// { "products": ["storeSpecificId1", "storeSpecificId2"] }
|
|
void unityPurchasingUpdateStorePromotionOrder(const char *json)
|
|
{
|
|
NSString* str = [NSString stringWithUTF8String: json];
|
|
NSArray* productIds = [UnityPurchasing deserializeProductIdList: str];
|
|
[UnityPurchasing_getInstance() updateStorePromotionOrder: productIds];
|
|
}
|
|
|
|
void unityPurchasingUpdateStorePromotionVisibility(const char *productId, const char *visibility)
|
|
{
|
|
NSString* prodId = [NSString stringWithUTF8String: productId];
|
|
NSString* visibilityStr = [NSString stringWithUTF8String: visibility];
|
|
[UnityPurchasing_getInstance() updateStorePromotionVisibility: visibilityStr forProduct: prodId];
|
|
}
|
|
|
|
void unityPurchasingInterceptPromotionalPurchases()
|
|
{
|
|
UnityPurchasingLog(@"Intercept promotional purchases");
|
|
UnityPurchasing_getInstance().interceptPromotionalPurchases = YES;
|
|
}
|
|
|
|
void unityPurchasingContinuePromotionalPurchases()
|
|
{
|
|
UnityPurchasingLog(@"Continue promotional purchases");
|
|
[UnityPurchasing_getInstance() initiateQueuedEarlyTransactionObserverPayments];
|
|
}
|
|
|
|
void unityPurchasingPresentCodeRedemptionSheet()
|
|
{
|
|
UnityPurchasingLog(@"Present code redemption sheet");
|
|
[UnityPurchasing_getInstance() presentCodeRedemptionSheet];
|
|
}
|