|
- #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))callbackBlock {
- self.callback = callbackBlock;
- return [super init];
- }
-
- -(void) requestDidFinish:(SKRequest *)request {
- self.callback(true);
- }
-
- -(void) request:(SKRequest *)request didFailWithError:(NSError *)error {
- self.callback(false);
- }
-
- @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;
-
- // 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 @"";
- }
-
- -(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);
- }
-
- -(void) UnitySendMessage:(NSString*) subject payload:(NSString*) payload receipt:(NSString*) receipt {
- messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, @"".UTF8String);
- }
-
- -(void) UnitySendMessage:(NSString*) subject payload:(NSString*) payload receipt:(NSString*) receipt transactionId:(NSString*) transactionId {
- messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, transactionId.UTF8String);
- }
-
- -(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 [self getAppReceipt];
- #else
- if ([self isiOS6OrEarlier]) {
- if (nil == transaction) {
- return @"";
- }
- NSString* receipt;
- receipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding: NSUTF8StringEncoding];
-
- return receipt;
- } else {
- return [self getAppReceipt];
- }
- #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) {
- UnityPurchasingLog(@"RefreshReceipt status %d", success);
- if (success) {
- [self UnitySendMessage:@"onAppReceiptRefreshed" payload:[self getAppReceipt]];
- } else {
- [self UnitySendMessage:@"onAppReceiptRefreshFailed" payload:nil];
- }
- }];
- 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 {
- NSString* transactionId = transaction.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];
- }
-
- // Called back by managed code when the tranaction has been logged.
- -(void) finishTransaction:(NSString *)transactionIdentifier {
- SKPaymentTransaction* transaction = [pendingTransactions objectForKey:transactionIdentifier];
- if (nil != transaction) {
- 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
- }
-
- #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 receipt:[self selectReceipt:nil] ];
- }
-
-
- #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) {
- switch (transaction.transactionState) {
-
- case SKPaymentTransactionStatePurchasing:
- // Item is still in the process of being purchased
- break;
-
- case SKPaymentTransactionStatePurchased: {
- #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
- if (transaction.payment.productIdentifier != nil) {
- transactionReceipts[transaction.payment.productIdentifier] = receipt;
- }
- [self onTransactionSucceeded:transaction];
- break;
- }
- case SKPaymentTransactionStateRestored: {
- [self onTransactionSucceeded:transaction];
- break;
- }
- case SKPaymentTransactionStateDeferred:
- UnityPurchasingLog(@"PurchaseDeferred");
- [self UnitySendMessage:@"onProductPurchaseDeferred" payload:transaction.payment.productIdentifier];
- break;
- case SKPaymentTransactionStateFailed: {
- // 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];
- }
- break;
- }
- }
- }
-
- // 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];
- }
-
- - (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");
- }
- }
-
- // 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;
- }
-
- +(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];
- [metadata setObject:currencyCode forKey:@"isoCurrencyCode"];
- }
-
- #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];
- }
-
- + (NSArray*) deserializeProductIdList:(NSString*)json
- {
- NSData* data = [json dataUsingEncoding:NSUTF8StringEncoding];
- NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
- return [[dict objectForKey:@"products"] copy];
- }
-
- // 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;
- NSString* tranId = [NSString stringWithUTF8String:transactionId];
- [UnityPurchasing_getInstance() finishTransaction:tranId];
- }
-
- 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 () {
- NSString* receipt = [UnityPurchasing_getInstance() getAppReceipt];
- return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
- }
-
- 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];
- }
-
- // 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];
- }
|