1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201 |
- #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 || __is_target_os(xros)
- 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 and %lu invalid products", (unsigned long)[response.products count], (unsigned long)[response.invalidProductIdentifiers 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 || __is_target_os(xros)
- // 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 = [NSString stringWithFormat: @"%ld", transaction.error.code];
- }
- 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 && !__is_target_os(xros)
- 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 && !__is_target_os(xros)
- 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 && !__is_target_os(xros)
- 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 && !__is_target_os(xros)
- 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 && !__is_target_os(xros)
- + (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 || __TV_OS_VERSION_MAX_ALLOWED >= 140000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
- if (@available(iOS 14, macOS 11.0, tvOS 14, *))
- {
- [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 @{
- @0: @"SKErrorUnknown",
- @1: @"SKErrorClientInvalid",
- @2: @"SKErrorPaymentCancelled",
- @3: @"SKErrorPaymentInvalid",
- @4: @"SKErrorPaymentNotAllowed",
- @5: @"SKErrorStoreProductNotAvailable",
- @6: @"SKErrorCloudServicePermissionDenied",
- @7: @"SKErrorCloudServiceNetworkConnectionFailed",
- @8: @"SKErrorCloudServiceRevoked",
- @9: @"SKErrorPrivacyAcknowledgementRequired",
- @10: @"SKErrorUnauthorizedRequestData",
- @11: @"SKErrorInvalidOfferIdentifier",
- @12: @"SKErrorInvalidSignature",
- @13: @"SKErrorMissingOfferParams",
- @14: @"SKErrorInvalidOfferPrice",
- @15: @"SKErrorOverlayCancelled",
- @16: @"SKErrorOverlayInvalidConfiguration",
- @17: @"SKErrorOverlayTimeout",
- @18: @"SKErrorIneligibleForOffer",
- @19: @"SKErrorUnsupportedPlatform",
- @20: @"SKErrorOverlayPresentedInBackgroundScene",
- };
- }
-
- #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];
- }
|