#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 () @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 *)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]; }