UnityPurchasing.m 25 KB

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