|
@@ -0,0 +1,572 @@
|
|
|
+#import "UnityPurchasing.h"
|
|
|
+#if MAC_APPSTORE
|
|
|
+#import "Base64.h"
|
|
|
+#endif
|
|
|
+
|
|
|
+@implementation ProductDefinition
|
|
|
+
|
|
|
+@synthesize id;
|
|
|
+@synthesize storeSpecificId;
|
|
|
+@synthesize type;
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
+@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
|
|
|
+
|
|
|
+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 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 @"";
|
|
|
+}
|
|
|
+
|
|
|
+-(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"];
|
|
|
+ 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 found!", 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) {
|
|
|
+ if ([payment respondsToSelector:@selector(setSimulatesAskToBuyInSandbox:)]) {
|
|
|
+ UnityPurchasingLog(@"Queueing payment request with simulatesAskToBuyInSandbox enabled");
|
|
|
+ [payment performSelector:@selector(setSimulatesAskToBuyInSandbox:) withObject:@YES];
|
|
|
+ //payment.simulatesAskToBuyInSandbox = YES;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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"];
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ [self onPurchaseFailed:productDef.storeSpecificId reason:@"ItemUnavailable"];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 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];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#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 {
|
|
|
+ NSMutableDictionary* dic = [[NSMutableDictionary alloc] init];
|
|
|
+ [dic setObject:productId forKey:@"productId"];
|
|
|
+ [dic setObject:reason forKey:@"reason"];
|
|
|
+
|
|
|
+ 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:
|
|
|
+ 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];
|
|
|
+ [self onPurchaseFailed:transaction.payment.productIdentifier reason:reason];
|
|
|
+
|
|
|
+ // 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];
|
|
|
+}
|
|
|
+
|
|
|
++(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"];
|
|
|
+ }
|
|
|
+
|
|
|
+ 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];
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Internal Methods & Events
|
|
|
+
|
|
|
+- (id)init {
|
|
|
+ if ( self = [super init] ) {
|
|
|
+ validProducts = [[NSMutableDictionary alloc] init];
|
|
|
+ pendingTransactions = [[NSMutableDictionary alloc] init];
|
|
|
+ finishedTransactions = [[NSMutableSet 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(@"restoreTransactions");
|
|
|
+ [UnityPurchasing_getInstance() restorePurchases];
|
|
|
+}
|
|
|
+
|
|
|
+void unityPurchasingAddTransactionObserver() {
|
|
|
+ UnityPurchasingLog(@"addTransactionObserver");
|
|
|
+ [UnityPurchasing_getInstance() addTransactionObserver];
|
|
|
+}
|
|
|
+
|
|
|
+void unityPurchasingRefreshAppReceipt() {
|
|
|
+ UnityPurchasingLog(@"refreshAppReceipt");
|
|
|
+ [UnityPurchasing_getInstance() refreshReceipt];
|
|
|
+}
|
|
|
+
|
|
|
+char* getUnityPurchasingAppReceipt () {
|
|
|
+ NSString* receipt = [UnityPurchasing_getInstance() getAppReceipt];
|
|
|
+ return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
|
|
|
+}
|
|
|
+
|
|
|
+BOOL getUnityPurchasingCanMakePayments () {
|
|
|
+ return [SKPaymentQueue canMakePayments];
|
|
|
+}
|
|
|
+
|
|
|
+void setSimulateAskToBuy(BOOL enabled) {
|
|
|
+ UnityPurchasingLog(@"setSimulateAskToBuy %@", 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];
|
|
|
+}
|
|
|
+
|