UnityPurchasing.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. #import "UnityPurchasing.h"
  2. #if MAC_APPSTORE
  3. #import "Base64.h"
  4. #endif
  5. @implementation ProductDefinition
  6. @synthesize id;
  7. @synthesize storeSpecificId;
  8. @synthesize type;
  9. @end
  10. @implementation ReceiptRefresher
  11. -(id) initWithCallback:(void (^)(BOOL))callbackBlock {
  12. self.callback = callbackBlock;
  13. return [super init];
  14. }
  15. -(void) requestDidFinish:(SKRequest *)request {
  16. self.callback(true);
  17. }
  18. -(void) request:(SKRequest *)request didFailWithError:(NSError *)error {
  19. self.callback(false);
  20. }
  21. @end
  22. void UnityPurchasingLog(NSString *format, ...) {
  23. va_list args;
  24. va_start(args, format);
  25. NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
  26. va_end(args);
  27. NSLog(@"UnityIAP:%@", message);
  28. }
  29. @implementation UnityPurchasing
  30. // The max time we wait in between retrying failed SKProductRequests.
  31. static const int MAX_REQUEST_PRODUCT_RETRY_DELAY = 60;
  32. // Track our accumulated delay.
  33. int delayInSeconds = 2;
  34. -(NSString*) getAppReceipt {
  35. NSBundle* bundle = [NSBundle mainBundle];
  36. if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) {
  37. NSURL *receiptURL = [bundle appStoreReceiptURL];
  38. if ([[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) {
  39. NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
  40. #if MAC_APPSTORE
  41. // The base64EncodedStringWithOptions method was only added in OSX 10.9.
  42. NSString* result = [receipt mgb64_base64EncodedString];
  43. #else
  44. NSString* result = [receipt base64EncodedStringWithOptions:0];
  45. #endif
  46. return result;
  47. }
  48. }
  49. UnityPurchasingLog(@"No App Receipt found");
  50. return @"";
  51. }
  52. -(void) UnitySendMessage:(NSString*) subject payload:(NSString*) payload {
  53. messageCallback(subject.UTF8String, payload.UTF8String, @"".UTF8String, @"".UTF8String);
  54. }
  55. -(void) UnitySendMessage:(NSString*) subject payload:(NSString*) payload receipt:(NSString*) receipt {
  56. messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, @"".UTF8String);
  57. }
  58. -(void) UnitySendMessage:(NSString*) subject payload:(NSString*) payload receipt:(NSString*) receipt transactionId:(NSString*) transactionId {
  59. messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, transactionId.UTF8String);
  60. }
  61. -(void) setCallback:(UnityPurchasingCallback)callback {
  62. messageCallback = callback;
  63. }
  64. #if !MAC_APPSTORE
  65. -(BOOL) isiOS6OrEarlier {
  66. float version = [[[UIDevice currentDevice] systemVersion] floatValue];
  67. return version < 7;
  68. }
  69. #endif
  70. // Retrieve a receipt for the transaction, which will either
  71. // be the old style transaction receipt on <= iOS 6,
  72. // or the App Receipt in OSX and iOS 7+.
  73. -(NSString*) selectReceipt:(SKPaymentTransaction*) transaction {
  74. #if MAC_APPSTORE
  75. return [self getAppReceipt];
  76. #else
  77. if ([self isiOS6OrEarlier]) {
  78. if (nil == transaction) {
  79. return @"";
  80. }
  81. NSString* receipt;
  82. receipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding: NSUTF8StringEncoding];
  83. return receipt;
  84. } else {
  85. return [self getAppReceipt];
  86. }
  87. #endif
  88. }
  89. -(void) refreshReceipt {
  90. #if !MAC_APPSTORE
  91. if ([self isiOS6OrEarlier]) {
  92. UnityPurchasingLog(@"RefreshReceipt not supported on iOS < 7!");
  93. return;
  94. }
  95. #endif
  96. self.receiptRefresher = [[ReceiptRefresher alloc] initWithCallback:^(BOOL success) {
  97. UnityPurchasingLog(@"RefreshReceipt status %d", success);
  98. if (success) {
  99. [self UnitySendMessage:@"onAppReceiptRefreshed" payload:[self getAppReceipt]];
  100. } else {
  101. [self UnitySendMessage:@"onAppReceiptRefreshFailed" payload:nil];
  102. }
  103. }];
  104. self.refreshRequest = [[SKReceiptRefreshRequest alloc] init];
  105. self.refreshRequest.delegate = self.receiptRefresher;
  106. [self.refreshRequest start];
  107. }
  108. // Handle a new or restored purchase transaction by informing Unity.
  109. - (void)onTransactionSucceeded:(SKPaymentTransaction*)transaction {
  110. NSString* transactionId = transaction.transactionIdentifier;
  111. // This should never happen according to Apple's docs, but it does!
  112. if (nil == transactionId) {
  113. // Make something up, allowing us to identifiy the transaction when finishing it.
  114. transactionId = [[NSUUID UUID] UUIDString];
  115. UnityPurchasingLog(@"Missing transaction Identifier!");
  116. }
  117. // Item was successfully purchased or restored.
  118. if (nil == [pendingTransactions objectForKey:transactionId]) {
  119. [pendingTransactions setObject:transaction forKey:transactionId];
  120. }
  121. [self UnitySendMessage:@"OnPurchaseSucceeded" payload:transaction.payment.productIdentifier receipt:[self selectReceipt:transaction] transactionId:transactionId];
  122. }
  123. // Called back by managed code when the tranaction has been logged.
  124. -(void) finishTransaction:(NSString *)transactionIdentifier {
  125. SKPaymentTransaction* transaction = [pendingTransactions objectForKey:transactionIdentifier];
  126. if (nil != transaction) {
  127. UnityPurchasingLog(@"Finishing transaction %@", transactionIdentifier);
  128. [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
  129. [pendingTransactions removeObjectForKey:transactionIdentifier];
  130. } else {
  131. UnityPurchasingLog(@"Transaction %@ not found!", transactionIdentifier);
  132. }
  133. }
  134. // Request information about our products from Apple.
  135. -(void) requestProducts:(NSSet*)paramIds
  136. {
  137. productIds = paramIds;
  138. UnityPurchasingLog(@"Requesting %lu products", (unsigned long) [productIds count]);
  139. // Start an immediate poll.
  140. [self initiateProductPoll:0];
  141. }
  142. // Execute a product metadata retrieval request via GCD.
  143. -(void) initiateProductPoll:(int) delayInSeconds
  144. {
  145. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
  146. dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
  147. UnityPurchasingLog(@"Requesting product data...");
  148. request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIds];
  149. request.delegate = self;
  150. [request start];
  151. });
  152. }
  153. // Called by managed code when a user requests a purchase.
  154. -(void) purchaseProduct:(ProductDefinition*)productDef
  155. {
  156. // Look up our corresponding product.
  157. SKProduct* requestedProduct = [validProducts objectForKey:productDef.storeSpecificId];
  158. if (requestedProduct != nil) {
  159. UnityPurchasingLog(@"PurchaseProduct: %@", requestedProduct.productIdentifier);
  160. if ([SKPaymentQueue canMakePayments]) {
  161. SKPayment *paymentRequest = [SKPayment paymentWithProduct:requestedProduct];
  162. [[SKPaymentQueue defaultQueue] addPayment:paymentRequest];
  163. } else {
  164. UnityPurchasingLog(@"PurchaseProduct: IAP Disabled");
  165. [self onPurchaseFailed:productDef.storeSpecificId reason:@"PurchasingUnavailable"];
  166. }
  167. } else {
  168. [self onPurchaseFailed:productDef.storeSpecificId reason:@"ItemUnavailable"];
  169. }
  170. }
  171. // Initiate a request to Apple to restore previously made purchases.
  172. -(void) restorePurchases
  173. {
  174. UnityPurchasingLog(@"RestorePurchase");
  175. [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
  176. }
  177. // A transaction observer should be added at startup (by managed code)
  178. // and maintained for the life of the app, since transactions can
  179. // be delivered at any time.
  180. -(void) addTransactionObserver {
  181. SKPaymentQueue* defaultQueue = [SKPaymentQueue defaultQueue];
  182. // Detect whether an existing transaction observer is in place.
  183. // An existing observer will have processed any transactions already pending,
  184. // so when we add our own storekit will not call our updatedTransactions handler.
  185. // We workaround this by explicitly processing any existing transactions if they exist.
  186. BOOL processExistingTransactions = false;
  187. if (defaultQueue != nil && defaultQueue.transactions != nil)
  188. {
  189. if ([[defaultQueue transactions] count] > 0) {
  190. processExistingTransactions = true;
  191. }
  192. }
  193. [defaultQueue addTransactionObserver:self];
  194. if (processExistingTransactions) {
  195. [self paymentQueue:defaultQueue updatedTransactions:defaultQueue.transactions];
  196. }
  197. }
  198. #pragma mark -
  199. #pragma mark SKProductsRequestDelegate Methods
  200. // Store Kit returns a response from an SKProductsRequest.
  201. - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
  202. UnityPurchasingLog(@"Received %lu products", (unsigned long) [response.products count]);
  203. // Add the retrieved products to our set of valid products.
  204. NSDictionary* fetchedProducts = [NSDictionary dictionaryWithObjects:response.products forKeys:[response.products valueForKey:@"productIdentifier"]];
  205. [validProducts addEntriesFromDictionary:fetchedProducts];
  206. NSString* productJSON = [UnityPurchasing serializeProductMetadata:response.products];
  207. // Send the app receipt as a separate parameter to avoid JSON parsing a large string.
  208. [self UnitySendMessage:@"OnProductsRetrieved" payload:productJSON receipt:[self selectReceipt:nil] ];
  209. }
  210. #pragma mark -
  211. #pragma mark SKPaymentTransactionObserver Methods
  212. // A product metadata retrieval request failed.
  213. // We handle it by retrying at an exponentially increasing interval.
  214. - (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
  215. delayInSeconds = MIN(MAX_REQUEST_PRODUCT_RETRY_DELAY, 2 * delayInSeconds);
  216. UnityPurchasingLog(@"SKProductRequest::didFailWithError: %ld, %@. Unity Purchasing will retry in %i seconds", (long)error.code, error.description, delayInSeconds);
  217. [self initiateProductPoll:delayInSeconds];
  218. }
  219. - (void)requestDidFinish:(SKRequest *)req {
  220. request = nil;
  221. }
  222. - (void)onPurchaseFailed:(NSString*) productId reason:(NSString*)reason {
  223. NSMutableDictionary* dic = [[NSMutableDictionary alloc] init];
  224. [dic setObject:productId forKey:@"productId"];
  225. [dic setObject:reason forKey:@"reason"];
  226. NSData* data = [NSJSONSerialization dataWithJSONObject:dic options:0 error:nil];
  227. NSString* result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  228. [self UnitySendMessage:@"OnPurchaseFailed" payload:result];
  229. }
  230. - (NSString*)purchaseErrorCodeToReason:(NSInteger) errorCode {
  231. switch (errorCode) {
  232. case SKErrorPaymentCancelled:
  233. return @"UserCancelled";
  234. case SKErrorPaymentInvalid:
  235. return @"PaymentDeclined";
  236. case SKErrorPaymentNotAllowed:
  237. return @"PurchasingUnavailable";
  238. }
  239. return @"Unknown";
  240. }
  241. // The transaction status of the SKPaymentQueue is sent here.
  242. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
  243. UnityPurchasingLog(@"UpdatedTransactions");
  244. for(SKPaymentTransaction *transaction in transactions) {
  245. switch (transaction.transactionState) {
  246. case SKPaymentTransactionStatePurchasing:
  247. // Item is still in the process of being purchased
  248. break;
  249. case SKPaymentTransactionStatePurchased:
  250. case SKPaymentTransactionStateRestored: {
  251. [self onTransactionSucceeded:transaction];
  252. break;
  253. }
  254. case SKPaymentTransactionStateDeferred:
  255. UnityPurchasingLog(@"PurchaseDeferred");
  256. [self UnitySendMessage:@"onProductPurchaseDeferred" payload:transaction.payment.productIdentifier];
  257. break;
  258. case SKPaymentTransactionStateFailed: {
  259. // Purchase was either cancelled by user or an error occurred.
  260. NSString* errorCode = [NSString stringWithFormat:@"%ld",(long)transaction.error.code];
  261. UnityPurchasingLog(@"PurchaseFailed: %@", errorCode);
  262. NSString* reason = [self purchaseErrorCodeToReason:transaction.error.code];
  263. [self onPurchaseFailed:transaction.payment.productIdentifier reason:reason];
  264. // Finished transactions should be removed from the payment queue.
  265. [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  266. }
  267. break;
  268. }
  269. }
  270. }
  271. // Called when one or more transactions have been removed from the queue.
  272. - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions
  273. {
  274. // Nothing to do here.
  275. }
  276. // Called when SKPaymentQueue has finished sending restored transactions.
  277. - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
  278. UnityPurchasingLog(@"PaymentQueueRestoreCompletedTransactionsFinished");
  279. [self UnitySendMessage:@"onTransactionsRestoredSuccess" payload:@""];
  280. }
  281. // Called if an error occurred while restoring transactions.
  282. - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
  283. {
  284. UnityPurchasingLog(@"restoreCompletedTransactionsFailedWithError");
  285. // Restore was cancelled or an error occurred, so notify user.
  286. [self UnitySendMessage:@"onTransactionsRestoredFail" payload:error.localizedDescription];
  287. }
  288. +(ProductDefinition*) decodeProductDefinition:(NSDictionary*) hash
  289. {
  290. ProductDefinition* product = [[ProductDefinition alloc] init];
  291. product.id = [hash objectForKey:@"id"];
  292. product.storeSpecificId = [hash objectForKey:@"storeSpecificId"];
  293. product.type = [hash objectForKey:@"type"];
  294. return product;
  295. }
  296. + (NSArray*) deserializeProductDefs:(NSString*)json
  297. {
  298. NSData* data = [json dataUsingEncoding:NSUTF8StringEncoding];
  299. NSArray* hashes = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
  300. NSMutableArray* result = [[NSMutableArray alloc] init];
  301. for (NSDictionary* hash in hashes) {
  302. [result addObject:[self decodeProductDefinition:hash]];
  303. }
  304. return result;
  305. }
  306. + (ProductDefinition*) deserializeProductDef:(NSString*)json
  307. {
  308. NSData* data = [json dataUsingEncoding:NSUTF8StringEncoding];
  309. NSDictionary* hash = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
  310. return [self decodeProductDefinition:hash];
  311. }
  312. + (NSString*) serializeProductMetadata:(NSArray*)appleProducts
  313. {
  314. NSMutableArray* hashes = [[NSMutableArray alloc] init];
  315. for (id product in appleProducts) {
  316. if (NULL == [product productIdentifier]) {
  317. UnityPurchasingLog(@"Product is missing an identifier!");
  318. continue;
  319. }
  320. NSMutableDictionary* hash = [[NSMutableDictionary alloc] init];
  321. [hashes addObject:hash];
  322. [hash setObject:[product productIdentifier] forKey:@"storeSpecificId"];
  323. NSMutableDictionary* metadata = [[NSMutableDictionary alloc] init];
  324. [hash setObject:metadata forKey:@"metadata"];
  325. if (NULL != [product price]) {
  326. [metadata setObject:[product price] forKey:@"localizedPrice"];
  327. }
  328. if (NULL != [product priceLocale]) {
  329. NSString *currencyCode = [[product priceLocale] objectForKey:NSLocaleCurrencyCode];
  330. [metadata setObject:currencyCode forKey:@"isoCurrencyCode"];
  331. }
  332. NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
  333. [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
  334. [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
  335. [numberFormatter setLocale:[product priceLocale]];
  336. NSString *formattedString = [numberFormatter stringFromNumber:[product price]];
  337. if (NULL == formattedString) {
  338. UnityPurchasingLog(@"Unable to format a localized price");
  339. [metadata setObject:@"" forKey:@"localizedPriceString"];
  340. } else {
  341. [metadata setObject:formattedString forKey:@"localizedPriceString"];
  342. }
  343. if (NULL == [product localizedTitle]) {
  344. UnityPurchasingLog(@"No localized title for: %@. Have your products been disapproved in itunes connect?", [product productIdentifier]);
  345. [metadata setObject:@"" forKey:@"localizedTitle"];
  346. } else {
  347. [metadata setObject:[product localizedTitle] forKey:@"localizedTitle"];
  348. }
  349. if (NULL == [product localizedDescription]) {
  350. UnityPurchasingLog(@"No localized description for: %@. Have your products been disapproved in itunes connect?", [product productIdentifier]);
  351. [metadata setObject:@"" forKey:@"localizedDescription"];
  352. } else {
  353. [metadata setObject:[product localizedDescription] forKey:@"localizedDescription"];
  354. }
  355. }
  356. NSData *data = [NSJSONSerialization dataWithJSONObject:hashes options:0 error:nil];
  357. return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  358. }
  359. #pragma mark - Internal Methods & Events
  360. - (id)init {
  361. if ( self = [super init] ) {
  362. validProducts = [[NSMutableDictionary alloc] init];
  363. pendingTransactions = [[NSMutableDictionary alloc] init];
  364. }
  365. return self;
  366. }
  367. @end
  368. UnityPurchasing* UnityPurchasing_instance = NULL;
  369. UnityPurchasing* UnityPurchasing_getInstance() {
  370. if (NULL == UnityPurchasing_instance) {
  371. UnityPurchasing_instance = [[UnityPurchasing alloc] init];
  372. }
  373. return UnityPurchasing_instance;
  374. }
  375. // Make a heap allocated copy of a string.
  376. // This is suitable for passing to managed code,
  377. // which will free the string when it is garbage collected.
  378. // Stack allocated variables must not be returned as results
  379. // from managed to native calls.
  380. char* UnityPurchasingMakeHeapAllocatedStringCopy (NSString* string)
  381. {
  382. if (NULL == string) {
  383. return NULL;
  384. }
  385. char* res = (char*)malloc([string length] + 1);
  386. strcpy(res, [string UTF8String]);
  387. return res;
  388. }
  389. void setUnityPurchasingCallback(UnityPurchasingCallback callback) {
  390. [UnityPurchasing_getInstance() setCallback:callback];
  391. }
  392. void unityPurchasingRetrieveProducts(const char* json) {
  393. NSString* str = [NSString stringWithUTF8String:json];
  394. NSArray* productDefs = [UnityPurchasing deserializeProductDefs:str];
  395. NSMutableSet* productIds = [[NSMutableSet alloc] init];
  396. for (ProductDefinition* product in productDefs) {
  397. [productIds addObject:product.storeSpecificId];
  398. }
  399. [UnityPurchasing_getInstance() requestProducts:productIds];
  400. }
  401. void unityPurchasingPurchase(const char* json, const char* developerPayload) {
  402. NSString* str = [NSString stringWithUTF8String:json];
  403. ProductDefinition* product = [UnityPurchasing deserializeProductDef:str];
  404. [UnityPurchasing_getInstance() purchaseProduct:product];
  405. }
  406. void unityPurchasingFinishTransaction(const char* productJSON, const char* transactionId) {
  407. if (transactionId == NULL)
  408. return;
  409. NSString* tranId = [NSString stringWithUTF8String:transactionId];
  410. [UnityPurchasing_getInstance() finishTransaction:tranId];
  411. }
  412. void unityPurchasingRestoreTransactions() {
  413. UnityPurchasingLog(@"restoreTransactions");
  414. [UnityPurchasing_getInstance() restorePurchases];
  415. }
  416. void unityPurchasingAddTransactionObserver() {
  417. UnityPurchasingLog(@"addTransactionObserver");
  418. [UnityPurchasing_getInstance() addTransactionObserver];
  419. }
  420. void unityPurchasingRefreshAppReceipt() {
  421. UnityPurchasingLog(@"refreshAppReceipt");
  422. [UnityPurchasing_getInstance() refreshReceipt];
  423. }
  424. char* getUnityPurchasingAppReceipt () {
  425. NSString* receipt = [UnityPurchasing_getInstance() getAppReceipt];
  426. return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
  427. }
  428. BOOL getUnityPurchasingCanMakePayments () {
  429. return [SKPaymentQueue canMakePayments];
  430. }