123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- #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];
- }
|