No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UnityPurchasing.m 44KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201
  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. {
  15. va_list args;
  16. va_start(args, format);
  17. NSString *message = [[NSString alloc] initWithFormat: format arguments: args];
  18. va_end(args);
  19. NSLog(@"UnityIAP: %@", message);
  20. }
  21. @implementation ReceiptRefresher
  22. - (id)initWithCallback:(void (^)(BOOL, NSString*))callbackBlock
  23. {
  24. self.callback = callbackBlock;
  25. return [super init];
  26. }
  27. - (void)requestDidFinish:(SKRequest *)request
  28. {
  29. self.callback(true, NULL);
  30. }
  31. - (void)request:(SKRequest *)request didFailWithError:(NSError *)error
  32. {
  33. NSString* errorMessage = [NSString stringWithFormat: @"Error code: %ld, error description: %@", error.code, error.description];
  34. self.callback(false, errorMessage);
  35. }
  36. @end
  37. #if !MAC_APPSTORE
  38. @interface UnityPurchasing ()<UnityEarlyTransactionObserverDelegate>
  39. @end
  40. #endif
  41. @implementation UnityPurchasing
  42. // The max time we wait in between retrying failed SKProductRequests.
  43. static const int MAX_REQUEST_PRODUCT_RETRY_DELAY = 60;
  44. // The currency code for unknown locales, from https://en.wikipedia.org/wiki/ISO_4217#X_currencies
  45. static const NSString* ISO_CURRENCY_CODE_UNKNOWN = @"XXX";
  46. // Track our accumulated delay.
  47. int delayInSeconds = 2;
  48. - (NSString*)getAppReceipt
  49. {
  50. NSBundle* bundle = [NSBundle mainBundle];
  51. if ([bundle respondsToSelector: @selector(appStoreReceiptURL)])
  52. {
  53. NSURL *receiptURL = [bundle appStoreReceiptURL];
  54. if ([[NSFileManager defaultManager] fileExistsAtPath: [receiptURL path]])
  55. {
  56. NSData *receipt = [NSData dataWithContentsOfURL: receiptURL];
  57. #if MAC_APPSTORE
  58. // The base64EncodedStringWithOptions method was only added in OSX 10.9.
  59. NSString* result = [receipt mgb64_base64EncodedString];
  60. #else
  61. NSString* result = [receipt base64EncodedStringWithOptions: 0];
  62. #endif
  63. return result;
  64. }
  65. }
  66. UnityPurchasingLog(@"No App Receipt found");
  67. return @"";
  68. }
  69. - (NSDate*)getAppReceiptModificationDate
  70. {
  71. NSBundle* bundle = [NSBundle mainBundle];
  72. if ([bundle respondsToSelector: @selector(appStoreReceiptURL)])
  73. {
  74. NSURL *receiptURL = [bundle appStoreReceiptURL];
  75. if ([[NSFileManager defaultManager] fileExistsAtPath: [receiptURL path]])
  76. {
  77. NSDate *modDate = [[[NSFileManager defaultManager] attributesOfItemAtPath: [receiptURL path] error: nil] fileModificationDate];
  78. return modDate;
  79. }
  80. }
  81. UnityPurchasingLog(@"No App Receipt found");
  82. return nil;
  83. }
  84. - (NSString*)getTransactionReceiptForProductId:(NSString *)productId
  85. {
  86. NSString *result = transactionReceipts[productId];
  87. if (!result)
  88. {
  89. UnityPurchasingLog(@"No Transaction Receipt found for product %@", productId);
  90. }
  91. return result ? : @"";
  92. }
  93. - (void)UnitySendMessage:(NSString*)subject payload:(NSString*)payload
  94. {
  95. messageCallback(subject.UTF8String, payload.UTF8String, @"".UTF8String, @"".UTF8String, @"".UTF8String, false);
  96. }
  97. - (void)UnitySendMessage:(NSString*)subject payload:(NSString*)payload receipt:(NSString*)receipt
  98. {
  99. messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, @"".UTF8String, @"".UTF8String, false);
  100. }
  101. - (void)UnitySendMessage:(NSString*)subject payload:(NSString*)payload receipt:(NSString*)receipt transactionId:(NSString*)transactionId originalTransactionId:(NSString*)originalTransactionId isRestored:(Boolean)isRestored
  102. {
  103. messageCallback(subject.UTF8String, payload.UTF8String, receipt.UTF8String, transactionId.UTF8String, originalTransactionId.UTF8String, isRestored);
  104. }
  105. - (void)setCallback:(UnityPurchasingCallback)callback
  106. {
  107. messageCallback = callback;
  108. }
  109. #if !MAC_APPSTORE
  110. - (BOOL)isiOS6OrEarlier
  111. {
  112. float version = [[[UIDevice currentDevice] systemVersion] floatValue];
  113. return version < 7;
  114. }
  115. #endif
  116. // Retrieve a receipt for the transaction, which will either
  117. // be the old style transaction receipt on <= iOS 6,
  118. // or the App Receipt in OSX and iOS 7+.
  119. - (NSString*)selectReceipt:(SKPaymentTransaction*)transaction
  120. {
  121. #if MAC_APPSTORE || __is_target_os(xros)
  122. return @"";
  123. #else
  124. if ([self isiOS6OrEarlier])
  125. {
  126. if (nil == transaction)
  127. {
  128. return @"";
  129. }
  130. NSString* receipt;
  131. receipt = [[NSString alloc] initWithData: transaction.transactionReceipt encoding: NSUTF8StringEncoding];
  132. return receipt;
  133. }
  134. else
  135. {
  136. return @"";
  137. }
  138. #endif
  139. }
  140. - (void)refreshReceipt
  141. {
  142. #if !MAC_APPSTORE
  143. if ([self isiOS6OrEarlier])
  144. {
  145. UnityPurchasingLog(@"RefreshReceipt not supported on iOS < 7!");
  146. return;
  147. }
  148. #endif
  149. self.receiptRefresher = [[ReceiptRefresher alloc] initWithCallback:^(BOOL success, NSString* errorMessage) {
  150. if (success)
  151. {
  152. UnityPurchasingLog(@"RefreshReceipt status %d", success);
  153. [self UnitySendMessage: @"onAppReceiptRefreshed" payload: [self getAppReceipt]];
  154. }
  155. else
  156. {
  157. UnityPurchasingLog(@"RefreshReceipt status %d - Error message: %@", success, errorMessage);
  158. [self UnitySendMessage: @"onAppReceiptRefreshFailed" payload: errorMessage];
  159. }
  160. }];
  161. self.refreshRequest = [[SKReceiptRefreshRequest alloc] init];
  162. self.refreshRequest.delegate = self.receiptRefresher;
  163. [self.refreshRequest start];
  164. }
  165. // Handle a new or restored purchase transaction by informing Unity.
  166. - (void)onTransactionSucceeded:(SKPaymentTransaction*)transaction isRestored:(Boolean)isRestored
  167. {
  168. NSString* transactionId = transaction.transactionIdentifier;
  169. NSString* originalTransactionId = transaction.originalTransaction.transactionIdentifier;
  170. // This should never happen according to Apple's docs, but it does!
  171. if (nil == transactionId)
  172. {
  173. // Make something up, allowing us to identifiy the transaction when finishing it.
  174. transactionId = [[NSUUID UUID] UUIDString];
  175. UnityPurchasingLog(@"Missing transaction Identifier!");
  176. }
  177. // 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
  178. if ([finishedTransactions containsObject: transactionId])
  179. {
  180. [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  181. UnityPurchasingLog(@"DuplicateTransaction error with product %@ and transactionId %@", transaction.payment.productIdentifier, transactionId);
  182. [self onPurchaseFailed: transaction.payment.productIdentifier reason: @"DuplicateTransaction" errorCode: @"" errorDescription: @"Duplicate transaction occurred"];
  183. return; // EARLY RETURN
  184. }
  185. // Item was successfully purchased or restored.
  186. if (nil == [pendingTransactions objectForKey: transactionId])
  187. {
  188. [pendingTransactions setObject: transaction forKey: transactionId];
  189. }
  190. [self UnitySendMessage: @"OnPurchaseSucceeded" payload: transaction.payment.productIdentifier receipt: [self selectReceipt: transaction] transactionId: transactionId originalTransactionId: originalTransactionId isRestored: isRestored];
  191. }
  192. // Called back by managed code when the transaction has been logged.
  193. - (void)finishTransaction:(NSString *)transactionIdentifier hasProduct:(Boolean)hasProduct
  194. {
  195. SKPaymentTransaction* transaction = [pendingTransactions objectForKey: transactionIdentifier];
  196. if (nil != transaction)
  197. {
  198. if (hasProduct)
  199. {
  200. UnityPurchasingLog(@"Finishing transaction %@", transactionIdentifier);
  201. }
  202. [[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
  203. [pendingTransactions removeObjectForKey: transactionIdentifier];
  204. [finishedTransactions addObject: transactionIdentifier];
  205. }
  206. else
  207. {
  208. UnityPurchasingLog(@"Transaction %@ not pending, nothing to finish here", transactionIdentifier);
  209. }
  210. }
  211. // Request information about our products from Apple.
  212. - (void)requestProducts:(NSSet*)paramIds
  213. {
  214. productIds = paramIds;
  215. UnityPurchasingLog(@"Requesting %lu products", (unsigned long)[productIds count]);
  216. // Start an immediate poll.
  217. [self initiateProductPoll: 0];
  218. }
  219. // Execute a product metadata retrieval request via GCD.
  220. - (void)initiateProductPoll:(int)delayInSeconds
  221. {
  222. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
  223. dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
  224. UnityPurchasingLog(@"Requesting product data...");
  225. request = [[SKProductsRequest alloc] initWithProductIdentifiers: productIds];
  226. request.delegate = self;
  227. [request start];
  228. });
  229. }
  230. // Called by managed code when a user requests a purchase.
  231. - (void)purchaseProduct:(ProductDefinition*)productDef
  232. {
  233. // Look up our corresponding product.
  234. SKProduct* requestedProduct = [validProducts objectForKey: productDef.storeSpecificId];
  235. if (requestedProduct != nil)
  236. {
  237. UnityPurchasingLog(@"PurchaseProduct: %@", requestedProduct.productIdentifier);
  238. if ([SKPaymentQueue canMakePayments])
  239. {
  240. SKMutablePayment *payment = [SKMutablePayment paymentWithProduct: requestedProduct];
  241. // Modify payment request for testing ask-to-buy
  242. if (_simulateAskToBuyEnabled)
  243. {
  244. #pragma clang diagnostic push
  245. #pragma clang diagnostic ignored "-Wundeclared-selector"
  246. if ([payment respondsToSelector: @selector(setSimulatesAskToBuyInSandbox:)])
  247. {
  248. UnityPurchasingLog(@"Queueing payment request with simulatesAskToBuyInSandbox enabled");
  249. [payment performSelector: @selector(setSimulatesAskToBuyInSandbox:) withObject: @YES];
  250. //payment.simulatesAskToBuyInSandbox = YES;
  251. }
  252. #pragma clang diagnostic pop
  253. }
  254. // Modify payment request with "applicationUsername" for fraud detection
  255. if (_applicationUsername != nil)
  256. {
  257. if ([payment respondsToSelector: @selector(setApplicationUsername:)])
  258. {
  259. UnityPurchasingLog(@"Setting applicationUsername to %@", _applicationUsername);
  260. [payment performSelector: @selector(setApplicationUsername:) withObject: _applicationUsername];
  261. //payment.applicationUsername = _applicationUsername;
  262. }
  263. }
  264. [[SKPaymentQueue defaultQueue] addPayment: payment];
  265. }
  266. else
  267. {
  268. UnityPurchasingLog(@"PurchaseProduct: IAP Disabled");
  269. [self onPurchaseFailed: productDef.storeSpecificId reason: @"PurchasingUnavailable" errorCode: @"SKErrorPaymentNotAllowed" errorDescription: @"User is not authorized to make payments"];
  270. }
  271. }
  272. else
  273. {
  274. [self onPurchaseFailed: productDef.storeSpecificId reason: @"ItemUnavailable" errorCode: @"" errorDescription: @"Unity IAP could not find requested product"];
  275. }
  276. }
  277. // Initiate a request to Apple to restore previously made purchases.
  278. - (void)restorePurchases
  279. {
  280. UnityPurchasingLog(@"RestorePurchase");
  281. [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
  282. }
  283. // A transaction observer should be added at startup (by managed code)
  284. // and maintained for the life of the app, since transactions can
  285. // be delivered at any time.
  286. - (void)addTransactionObserver
  287. {
  288. SKPaymentQueue* defaultQueue = [SKPaymentQueue defaultQueue];
  289. // Detect whether an existing transaction observer is in place.
  290. // An existing observer will have processed any transactions already pending,
  291. // so when we add our own storekit will not call our updatedTransactions handler.
  292. // We workaround this by explicitly processing any existing transactions if they exist.
  293. BOOL processExistingTransactions = false;
  294. if (defaultQueue != nil && defaultQueue.transactions != nil)
  295. {
  296. if ([[defaultQueue transactions] count] > 0)
  297. {
  298. processExistingTransactions = true;
  299. }
  300. }
  301. [defaultQueue addTransactionObserver: self];
  302. if (processExistingTransactions)
  303. {
  304. [self paymentQueue: defaultQueue updatedTransactions: defaultQueue.transactions];
  305. }
  306. #if !MAC_APPSTORE
  307. UnityEarlyTransactionObserver *observer = [UnityEarlyTransactionObserver defaultObserver];
  308. if (observer)
  309. {
  310. observer.readyToReceiveTransactionUpdates = YES;
  311. if (self.interceptPromotionalPurchases)
  312. {
  313. observer.delegate = self;
  314. }
  315. else
  316. {
  317. [observer initiateQueuedPayments];
  318. }
  319. }
  320. #endif
  321. }
  322. - (void)initiateQueuedEarlyTransactionObserverPayments
  323. {
  324. #if !MAC_APPSTORE
  325. [[UnityEarlyTransactionObserver defaultObserver] initiateQueuedPayments];
  326. #endif
  327. }
  328. - (void)presentCodeRedemptionSheet
  329. {
  330. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 && TARGET_OS_TV == 0 && !MAC_APPSTORE
  331. if (@available(iOS 14, *))
  332. {
  333. [[SKPaymentQueue defaultQueue] presentCodeRedemptionSheet];
  334. }
  335. else
  336. #endif
  337. {
  338. UnityPurchasingLog(@"Offer Code redemption is available on iOS and iPadOS 14 and later");
  339. }
  340. }
  341. #if !MAC_APPSTORE
  342. #pragma mark -
  343. #pragma mark UnityEarlyTransactionObserverDelegate Methods
  344. - (void)promotionalPurchaseAttempted:(SKPayment *)payment
  345. {
  346. UnityPurchasingLog(@"Promotional purchase attempted");
  347. [self UnitySendMessage: @"onPromotionalPurchaseAttempted" payload: payment.productIdentifier];
  348. }
  349. #endif
  350. #pragma mark -
  351. #pragma mark SKProductsRequestDelegate Methods
  352. // Store Kit returns a response from an SKProductsRequest.
  353. - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
  354. {
  355. UnityPurchasingLog(@"Received %lu products and %lu invalid products", (unsigned long)[response.products count], (unsigned long)[response.invalidProductIdentifiers count]);
  356. // Add the retrieved products to our set of valid products.
  357. NSDictionary* fetchedProducts = [NSDictionary dictionaryWithObjects: response.products forKeys: [response.products valueForKey: @"productIdentifier"]];
  358. [validProducts addEntriesFromDictionary: fetchedProducts];
  359. NSString* productJSON = [UnityPurchasing serializeProductMetadata: response.products];
  360. // Send the app receipt as a separate parameter to avoid JSON parsing a large string.
  361. [self UnitySendMessage: @"OnProductsRetrieved" payload: productJSON];
  362. }
  363. #pragma mark -
  364. #pragma mark SKPaymentTransactionObserver Methods
  365. // A product metadata retrieval request failed.
  366. // We handle it by retrying at an exponentially increasing interval.
  367. - (void)request:(SKRequest *)request didFailWithError:(NSError *)error
  368. {
  369. delayInSeconds = MIN(MAX_REQUEST_PRODUCT_RETRY_DELAY, 2 * delayInSeconds);
  370. UnityPurchasingLog(@"SKProductRequest::didFailWithError: %ld, %@. Unity Purchasing will retry in %i seconds", (long)error.code, error.description, delayInSeconds);
  371. [self initiateProductPoll: delayInSeconds];
  372. }
  373. - (void)requestDidFinish:(SKRequest *)req
  374. {
  375. request = nil;
  376. }
  377. - (void)onPurchaseFailed:(NSString*)productId reason:(NSString*)reason errorCode:(NSString*)errorCode errorDescription:(NSString*)errorDescription
  378. {
  379. NSMutableDictionary* dic = [[NSMutableDictionary alloc] init];
  380. [dic setObject: productId forKey: @"productId"];
  381. [dic setObject: reason forKey: @"reason"];
  382. [dic setObject: errorCode forKey: @"storeSpecificErrorCode"];
  383. [dic setObject: errorDescription forKey: @"message"];
  384. NSData* data = [NSJSONSerialization dataWithJSONObject: dic options: 0 error: nil];
  385. NSString* result = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
  386. [self UnitySendMessage: @"OnPurchaseFailed" payload: result];
  387. }
  388. - (NSString*)purchaseErrorCodeToReason:(NSInteger)errorCode
  389. {
  390. switch (errorCode)
  391. {
  392. case SKErrorPaymentCancelled:
  393. return @"UserCancelled";
  394. case SKErrorPaymentInvalid:
  395. return @"PaymentDeclined";
  396. case SKErrorPaymentNotAllowed:
  397. return @"PurchasingUnavailable";
  398. }
  399. return @"Unknown";
  400. }
  401. // The transaction status of the SKPaymentQueue is sent here.
  402. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
  403. {
  404. UnityPurchasingLog(@"UpdatedTransactions");
  405. for (SKPaymentTransaction *transaction in transactions)
  406. {
  407. [self handleTransaction: transaction];
  408. }
  409. }
  410. // Called when one or more transactions have been removed from the queue.
  411. - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions
  412. {
  413. // Nothing to do here.
  414. }
  415. // Called when SKPaymentQueue has finished sending restored transactions.
  416. - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
  417. {
  418. UnityPurchasingLog(@"PaymentQueueRestoreCompletedTransactionsFinished");
  419. [self UnitySendMessage: @"onTransactionsRestoredSuccess" payload: @""];
  420. }
  421. // Called if an error occurred while restoring transactions.
  422. - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
  423. {
  424. UnityPurchasingLog(@"restoreCompletedTransactionsFailedWithError");
  425. // Restore was cancelled or an error occurred, so notify user.
  426. [self UnitySendMessage: @"onTransactionsRestoredFail" payload: error.localizedDescription];
  427. }
  428. // Called when an entitlement was revoked.
  429. - (void)paymentQueue:(SKPaymentQueue *)queue didRevokeEntitlementsForProductIdentifiers:(NSArray *)productIdentifiers
  430. {
  431. UnityPurchasingLog(@"didRevokeEntitlementsForProductIdentifiers");
  432. NSString* productIdsJSON = [UnityPurchasing serializeProductIdList: productIdentifiers];
  433. [self UnitySendMessage: @"onEntitlementsRevoked" payload: productIdsJSON];
  434. }
  435. - (void)handleTransaction:(SKPaymentTransaction *)transaction
  436. {
  437. if (transaction.payment.productIdentifier == nil)
  438. {
  439. return;
  440. }
  441. SKProduct* product = [validProducts objectForKey: transaction.payment.productIdentifier];
  442. switch (transaction.transactionState)
  443. {
  444. case SKPaymentTransactionStatePurchasing:
  445. // Item is still in the process of being purchased
  446. break;
  447. case SKPaymentTransactionStatePurchased:
  448. [self handleTransactionPurchased: transaction forProduct: product];
  449. break;
  450. case SKPaymentTransactionStateRestored:
  451. [self handleTransactionRestored: transaction forProduct: product];
  452. break;
  453. case SKPaymentTransactionStateDeferred:
  454. [self handleTransactionDeferred: transaction forProduct: product];
  455. break;
  456. case SKPaymentTransactionStateFailed:
  457. [self handleTransactionFailed: transaction];
  458. break;
  459. }
  460. }
  461. - (void)handleTransactionPurchased:(SKPaymentTransaction*)transaction forProduct:(SKProduct*)product
  462. {
  463. #if MAC_APPSTORE || __is_target_os(xros)
  464. // There is no transactionReceipt on Mac
  465. NSString* receipt = @"";
  466. #else
  467. // The transactionReceipt field is deprecated, but is being used here to validate Ask-To-Buy purchases
  468. NSString* receipt = [transaction.transactionReceipt base64EncodedStringWithOptions: 0];
  469. #endif
  470. transactionReceipts[transaction.payment.productIdentifier] = receipt;
  471. if (product != nil)
  472. {
  473. [self onTransactionSucceeded: transaction isRestored: false];
  474. }
  475. }
  476. - (void)handleTransactionRestored:(SKPaymentTransaction*)transaction forProduct:(SKProduct*)product
  477. {
  478. if (product != nil)
  479. {
  480. [self onTransactionSucceeded: transaction isRestored: true];
  481. }
  482. }
  483. - (void)handleTransactionDeferred:(SKPaymentTransaction*)transaction forProduct:(SKProduct*)product
  484. {
  485. if (product != nil)
  486. {
  487. UnityPurchasingLog(@"PurchaseDeferred");
  488. [self UnitySendMessage: @"onProductPurchaseDeferred" payload: transaction.payment.productIdentifier];
  489. }
  490. }
  491. - (void)handleTransactionFailed:(SKPaymentTransaction*)transaction
  492. {
  493. // Purchase was either cancelled by user or an error occurred.
  494. NSString* errorCode = [NSString stringWithFormat: @"%ld", (long)transaction.error.code];
  495. UnityPurchasingLog(@"PurchaseFailed: %@", errorCode);
  496. NSString* reason = [self purchaseErrorCodeToReason: transaction.error.code];
  497. NSString* errorCodeString = [UnityPurchasing storeKitErrorCodeNames][@(transaction.error.code)];
  498. if (errorCodeString == nil)
  499. {
  500. errorCodeString = [NSString stringWithFormat: @"%ld", transaction.error.code];
  501. }
  502. NSString* errorDescription = [NSString stringWithFormat: @"APPLE_%@", transaction.error.localizedDescription];
  503. [self onPurchaseFailed: transaction.payment.productIdentifier reason: reason errorCode: errorCodeString errorDescription: errorDescription];
  504. // Finished transactions should be removed from the payment queue.
  505. [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  506. }
  507. - (void)fetchStorePromotionOrder
  508. {
  509. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 && !__is_target_os(xros)
  510. if (@available(iOS 11.0, *))
  511. {
  512. [[SKProductStorePromotionController defaultController] fetchStorePromotionOrderWithCompletionHandler:^(NSArray<SKProduct *> * _Nonnull storePromotionOrder, NSError * _Nullable error) {
  513. if (error)
  514. {
  515. UnityPurchasingLog(@"Error in fetchStorePromotionOrder: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
  516. [self UnitySendMessage: @"onFetchStorePromotionOrderFailed" payload: nil];
  517. }
  518. else
  519. {
  520. UnityPurchasingLog(@"Fetched %lu store-promotion ordered products", (unsigned long)[storePromotionOrder count]);
  521. NSString *productIdsJSON = [UnityPurchasing serializeSKProductIdList: storePromotionOrder];
  522. [self UnitySendMessage: @"onFetchStorePromotionOrderSucceeded" payload: productIdsJSON];
  523. }
  524. }];
  525. }
  526. else
  527. #endif
  528. {
  529. UnityPurchasingLog(@"Fetch store promotion order is only available on iOS and tvOS 11 or later");
  530. [self UnitySendMessage: @"onFetchStorePromotionOrderFailed" payload: nil];
  531. }
  532. }
  533. - (void)updateStorePromotionOrder:(NSArray*)productIds
  534. {
  535. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 && !__is_target_os(xros)
  536. if (@available(iOS 11_0, *))
  537. {
  538. NSMutableArray* products = [[NSMutableArray alloc] init];
  539. for (NSString* productId in productIds)
  540. {
  541. SKProduct* product = [validProducts objectForKey: productId];
  542. if (product)
  543. [products addObject: product];
  544. }
  545. SKProductStorePromotionController* controller = [SKProductStorePromotionController defaultController];
  546. [controller updateStorePromotionOrder: products completionHandler:^(NSError* error) {
  547. if (error)
  548. UnityPurchasingLog(@"Error in updateStorePromotionOrder: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
  549. }];
  550. }
  551. else
  552. #endif
  553. {
  554. UnityPurchasingLog(@"Update store promotion order is only available on iOS and tvOS 11 or later");
  555. }
  556. }
  557. - (void)fetchStorePromotionVisibilityForProduct:(NSString*)productId
  558. {
  559. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 && !__is_target_os(xros)
  560. if (@available(iOS 11.0, macOS 11.0, tvOS 11.0, *))
  561. {
  562. SKProduct *product = [validProducts objectForKey: productId];
  563. [[SKProductStorePromotionController defaultController]
  564. fetchStorePromotionVisibilityForProduct: product completionHandler:^(SKProductStorePromotionVisibility storePromotionVisibility, NSError * _Nullable error) {
  565. if (error)
  566. {
  567. UnityPurchasingLog(@"Error in fetchStorePromotionVisibilityForProduct: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
  568. [self UnitySendMessage: @"onFetchStorePromotionVisibilityFailed" payload: nil];
  569. }
  570. else
  571. {
  572. NSString *visibility = [UnityPurchasing getStringForStorePromotionVisibility: storePromotionVisibility];
  573. UnityPurchasingLog(@"Fetched Store Promotion Visibility for %@", product.productIdentifier);
  574. NSString *payload = [UnityPurchasing serializeVisibilityResultForProduct: productId withVisiblity: visibility];
  575. [self UnitySendMessage: @"onFetchStorePromotionVisibilitySucceeded" payload: payload];
  576. }
  577. }
  578. ];
  579. }
  580. else
  581. #endif
  582. {
  583. UnityPurchasingLog(@"Fetch store promotion visibility is only available on iOS, macOS and tvOS 11 or later");
  584. [self UnitySendMessage: @"onFetchStorePromotionVisibilityFailed" payload: nil];
  585. }
  586. }
  587. // visibility should be one of "Default", "Hide", or "Show"
  588. - (void)updateStorePromotionVisibility:(NSString*)visibility forProduct:(NSString*)productId
  589. {
  590. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 && !__is_target_os(xros)
  591. if (@available(iOS 11_0, *))
  592. {
  593. SKProduct *product = [validProducts objectForKey: productId];
  594. if (!product)
  595. {
  596. UnityPurchasingLog(@"updateStorePromotionVisibility unable to find product %@", productId);
  597. return;
  598. }
  599. SKProductStorePromotionVisibility v = SKProductStorePromotionVisibilityDefault;
  600. if ([visibility isEqualToString: @"Hide"])
  601. v = SKProductStorePromotionVisibilityHide;
  602. else if ([visibility isEqualToString: @"Show"])
  603. v = SKProductStorePromotionVisibilityShow;
  604. SKProductStorePromotionController* controller = [SKProductStorePromotionController defaultController];
  605. [controller updateStorePromotionVisibility: v forProduct: product completionHandler:^(NSError* error) {
  606. if (error)
  607. UnityPurchasingLog(@"Error in updateStorePromotionVisibility: %@ - %@ - %@", [error code], [error domain], [error localizedDescription]);
  608. }];
  609. }
  610. else
  611. #endif
  612. {
  613. UnityPurchasingLog(@"Update store promotion visibility is only available on iOS and tvOS 11 or later");
  614. }
  615. }
  616. - (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product
  617. {
  618. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
  619. if (@available(iOS 11_0, *))
  620. {
  621. // Just defer to the early transaction observer. This should have no effect, just return whatever the observer returns.
  622. return [[UnityEarlyTransactionObserver defaultObserver] paymentQueue: queue shouldAddStorePayment: payment forProduct: product];
  623. }
  624. #endif
  625. return YES;
  626. }
  627. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 && !__is_target_os(xros)
  628. + (NSString*)getStringForStorePromotionVisibility:(SKProductStorePromotionVisibility)storePromotionVisibility
  629. API_AVAILABLE(macos(11.0), ios(11.0), tvos(11.0))
  630. {
  631. switch (storePromotionVisibility)
  632. {
  633. case SKProductStorePromotionVisibilityShow:
  634. return @"Show";
  635. case SKProductStorePromotionVisibilityHide:
  636. return @"Hide";
  637. case SKProductStorePromotionVisibilityDefault:
  638. return @"Default";
  639. default:
  640. return @"Default";
  641. }
  642. }
  643. #endif
  644. + (ProductDefinition*)decodeProductDefinition:(NSDictionary*)hash
  645. {
  646. ProductDefinition* product = [[ProductDefinition alloc] init];
  647. product.id = [hash objectForKey: @"id"];
  648. product.storeSpecificId = [hash objectForKey: @"storeSpecificId"];
  649. product.type = [hash objectForKey: @"type"];
  650. return product;
  651. }
  652. + (NSArray*)deserializeProductDefs:(NSString*)json
  653. {
  654. NSData* data = [json dataUsingEncoding: NSUTF8StringEncoding];
  655. NSArray* hashes = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil];
  656. NSMutableArray* result = [[NSMutableArray alloc] init];
  657. for (NSDictionary* hash in hashes)
  658. {
  659. [result addObject: [self decodeProductDefinition: hash]];
  660. }
  661. return result;
  662. }
  663. + (ProductDefinition*)deserializeProductDef:(NSString*)json
  664. {
  665. NSData* data = [json dataUsingEncoding: NSUTF8StringEncoding];
  666. NSDictionary* hash = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil];
  667. return [self decodeProductDefinition: hash];
  668. }
  669. + (NSString*)serializeProductMetadata:(NSArray*)appleProducts
  670. {
  671. NSMutableArray* hashes = [[NSMutableArray alloc] init];
  672. for (id product in appleProducts)
  673. {
  674. if (NULL == [product productIdentifier])
  675. {
  676. UnityPurchasingLog(@"Product is missing an identifier!");
  677. continue;
  678. }
  679. NSMutableDictionary* hash = [[NSMutableDictionary alloc] init];
  680. [hashes addObject: hash];
  681. [hash setObject: [product productIdentifier] forKey: @"storeSpecificId"];
  682. NSMutableDictionary* metadata = [[NSMutableDictionary alloc] init];
  683. [hash setObject: metadata forKey: @"metadata"];
  684. if (NULL != [product price])
  685. {
  686. [metadata setObject: [product price] forKey: @"localizedPrice"];
  687. }
  688. if (NULL != [product priceLocale])
  689. {
  690. NSString *currencyCode = [[product priceLocale] objectForKey: NSLocaleCurrencyCode];
  691. // NSLocaleCurrencyCode has been seen to return nil. Avoid crashing and report the issue to the log. E.g. https://developer.apple.com/forums/thread/119838
  692. if (currencyCode != nil)
  693. {
  694. [metadata setObject: currencyCode forKey: @"isoCurrencyCode"];
  695. }
  696. else
  697. {
  698. UnityPurchasingLog(@"Error: unable to determine localized currency code for product {%@}, [SKProduct priceLocale] identifier {%@}. NSLocaleCurrencyCode {%@} is nil. Using ISO Unknown Currency code, instead: {%@}.", [product productIdentifier], [[product priceLocale] localeIdentifier], currencyCode, ISO_CURRENCY_CODE_UNKNOWN);
  699. [metadata setObject: ISO_CURRENCY_CODE_UNKNOWN forKey: @"isoCurrencyCode"];
  700. }
  701. }
  702. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 || __TV_OS_VERSION_MAX_ALLOWED >= 140000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
  703. if (@available(iOS 14, macOS 11.0, tvOS 14, *))
  704. {
  705. [product isFamilyShareable] ? [metadata setObject: @"true" forKey: @"isFamilyShareable"] : [metadata setObject: @"false" forKey: @"isFamilyShareable"];
  706. }
  707. #endif
  708. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 || __TV_OS_VERSION_MAX_ALLOWED >= 110000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
  709. if ((@available(iOS 11_2, macOS 10_13_2, tvOS 11_2, *)) && (nil != [product introductoryPrice]))
  710. {
  711. [metadata setObject: [[product introductoryPrice] price] forKey: @"introductoryPrice"];
  712. if (nil != [[product introductoryPrice] priceLocale])
  713. {
  714. NSString *currencyCode = [[[product introductoryPrice] priceLocale] objectForKey: NSLocaleCurrencyCode];
  715. [metadata setObject: currencyCode forKey: @"introductoryPriceLocale"];
  716. }
  717. else
  718. {
  719. [metadata setObject: @"" forKey: @"introductoryPriceLocale"];
  720. }
  721. if (nil != [[product introductoryPrice] numberOfPeriods])
  722. {
  723. NSNumber *numberOfPeriods = [NSNumber numberWithInt: [[product introductoryPrice] numberOfPeriods]];
  724. [metadata setObject: numberOfPeriods forKey: @"introductoryPriceNumberOfPeriods"];
  725. }
  726. else
  727. {
  728. [metadata setObject: @"" forKey: @"introductoryPriceNumberOfPeriods"];
  729. }
  730. if (nil != [[product introductoryPrice] subscriptionPeriod])
  731. {
  732. if (nil != [[[product introductoryPrice] subscriptionPeriod] numberOfUnits])
  733. {
  734. NSNumber *numberOfUnits = [NSNumber numberWithInt: [[[product introductoryPrice] subscriptionPeriod] numberOfUnits]];
  735. [metadata setObject: numberOfUnits forKey: @"numberOfUnits"];
  736. }
  737. else
  738. {
  739. [metadata setObject: @"" forKey: @"numberOfUnits"];
  740. }
  741. if (nil != [[[product introductoryPrice] subscriptionPeriod] unit])
  742. {
  743. NSNumber *unit = [NSNumber numberWithInt: [[[product introductoryPrice] subscriptionPeriod] unit]];
  744. [metadata setObject: unit forKey: @"unit"];
  745. }
  746. else
  747. {
  748. [metadata setObject: @"" forKey: @"unit"];
  749. }
  750. }
  751. else
  752. {
  753. [metadata setObject: @"" forKey: @"numberOfUnits"];
  754. [metadata setObject: @"" forKey: @"unit"];
  755. }
  756. }
  757. else
  758. {
  759. [metadata setObject: @"" forKey: @"introductoryPrice"];
  760. [metadata setObject: @"" forKey: @"introductoryPriceLocale"];
  761. [metadata setObject: @"" forKey: @"introductoryPriceNumberOfPeriods"];
  762. [metadata setObject: @"" forKey: @"numberOfUnits"];
  763. [metadata setObject: @"" forKey: @"unit"];
  764. }
  765. if ((@available(iOS 11_2, macOS 10_13_2, tvOS 11_2, *)) && (nil != [product subscriptionPeriod]))
  766. {
  767. if (nil != [[product subscriptionPeriod] numberOfUnits])
  768. {
  769. NSNumber *numberOfUnits = [NSNumber numberWithInt: [[product subscriptionPeriod] numberOfUnits]];
  770. [metadata setObject: numberOfUnits forKey: @"subscriptionNumberOfUnits"];
  771. }
  772. else
  773. {
  774. [metadata setObject: @"" forKey: @"subscriptionNumberOfUnits"];
  775. }
  776. if (nil != [[product subscriptionPeriod] unit])
  777. {
  778. NSNumber *unit = [NSNumber numberWithInt: [[product subscriptionPeriod] unit]];
  779. [metadata setObject: unit forKey: @"subscriptionPeriodUnit"];
  780. }
  781. else
  782. {
  783. [metadata setObject: @"" forKey: @"subscriptionPeriodUnit"];
  784. }
  785. }
  786. else
  787. {
  788. [metadata setObject: @"" forKey: @"subscriptionNumberOfUnits"];
  789. [metadata setObject: @"" forKey: @"subscriptionPeriodUnit"];
  790. }
  791. #else
  792. [metadata setObject: @"" forKey: @"introductoryPrice"];
  793. [metadata setObject: @"" forKey: @"introductoryPriceLocale"];
  794. [metadata setObject: @"" forKey: @"introductoryPriceNumberOfPeriods"];
  795. [metadata setObject: @"" forKey: @"numberOfUnits"];
  796. [metadata setObject: @"" forKey: @"unit"];
  797. [metadata setObject: @"" forKey: @"subscriptionNumberOfUnits"];
  798. [metadata setObject: @"" forKey: @"subscriptionPeriodUnit"];
  799. #endif
  800. NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
  801. [numberFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4];
  802. [numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle];
  803. [numberFormatter setLocale: [product priceLocale]];
  804. NSString *formattedString = [numberFormatter stringFromNumber: [product price]];
  805. if (NULL == formattedString)
  806. {
  807. UnityPurchasingLog(@"Unable to format a localized price");
  808. [metadata setObject: @"" forKey: @"localizedPriceString"];
  809. }
  810. else
  811. {
  812. [metadata setObject: formattedString forKey: @"localizedPriceString"];
  813. }
  814. if (NULL == [product localizedTitle])
  815. {
  816. UnityPurchasingLog(@"No localized title for: %@. Have your products been disapproved in itunes connect?", [product productIdentifier]);
  817. [metadata setObject: @"" forKey: @"localizedTitle"];
  818. }
  819. else
  820. {
  821. [metadata setObject: [product localizedTitle] forKey: @"localizedTitle"];
  822. }
  823. if (NULL == [product localizedDescription])
  824. {
  825. UnityPurchasingLog(@"No localized description for: %@. Have your products been disapproved in itunes connect?", [product productIdentifier]);
  826. [metadata setObject: @"" forKey: @"localizedDescription"];
  827. }
  828. else
  829. {
  830. [metadata setObject: [product localizedDescription] forKey: @"localizedDescription"];
  831. }
  832. }
  833. NSData *data = [NSJSONSerialization dataWithJSONObject: hashes options: 0 error: nil];
  834. return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
  835. }
  836. + (NSString*)serializeSKProductIdList:(NSArray<SKProduct *> *)products
  837. {
  838. NSMutableArray *productIds = [NSMutableArray arrayWithCapacity: products.count];
  839. for (SKProduct *product in products)
  840. {
  841. [productIds addObject: product.productIdentifier];
  842. }
  843. return [UnityPurchasing serializeProductIdList: products];
  844. }
  845. + (NSString*)serializeProductIdList:(NSArray*)products
  846. {
  847. NSData *data = [NSJSONSerialization dataWithJSONObject: products options: 0 error: nil];
  848. return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
  849. }
  850. + (NSArray*)deserializeProductIdList:(NSString*)json
  851. {
  852. NSData* data = [json dataUsingEncoding: NSUTF8StringEncoding];
  853. NSDictionary* dict = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil];
  854. return [[dict objectForKey: @"products"] copy];
  855. }
  856. + (NSString*)serializeVisibilityResultForProduct:(NSString *)productId withVisiblity:(NSString *)visibility
  857. {
  858. NSDictionary *result = @{@"productId": productId, @"visibility": visibility};
  859. NSData *data = [NSJSONSerialization dataWithJSONObject: result options: 0 error: nil];
  860. return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
  861. }
  862. // Note: this will need to be updated if Apple ever adds more StoreKit error codes.
  863. + (NSDictionary<NSNumber *, NSString *> *)storeKitErrorCodeNames
  864. {
  865. return @{
  866. @0: @"SKErrorUnknown",
  867. @1: @"SKErrorClientInvalid",
  868. @2: @"SKErrorPaymentCancelled",
  869. @3: @"SKErrorPaymentInvalid",
  870. @4: @"SKErrorPaymentNotAllowed",
  871. @5: @"SKErrorStoreProductNotAvailable",
  872. @6: @"SKErrorCloudServicePermissionDenied",
  873. @7: @"SKErrorCloudServiceNetworkConnectionFailed",
  874. @8: @"SKErrorCloudServiceRevoked",
  875. @9: @"SKErrorPrivacyAcknowledgementRequired",
  876. @10: @"SKErrorUnauthorizedRequestData",
  877. @11: @"SKErrorInvalidOfferIdentifier",
  878. @12: @"SKErrorInvalidSignature",
  879. @13: @"SKErrorMissingOfferParams",
  880. @14: @"SKErrorInvalidOfferPrice",
  881. @15: @"SKErrorOverlayCancelled",
  882. @16: @"SKErrorOverlayInvalidConfiguration",
  883. @17: @"SKErrorOverlayTimeout",
  884. @18: @"SKErrorIneligibleForOffer",
  885. @19: @"SKErrorUnsupportedPlatform",
  886. @20: @"SKErrorOverlayPresentedInBackgroundScene",
  887. };
  888. }
  889. #pragma mark - Internal Methods & Events
  890. - (id)init
  891. {
  892. if (self = [super init])
  893. {
  894. validProducts = [[NSMutableDictionary alloc] init];
  895. pendingTransactions = [[NSMutableDictionary alloc] init];
  896. finishedTransactions = [[NSMutableSet alloc] init];
  897. transactionReceipts = [[NSMutableDictionary alloc] init];
  898. }
  899. return self;
  900. }
  901. @end
  902. UnityPurchasing* UnityPurchasing_instance = NULL;
  903. UnityPurchasing* UnityPurchasing_getInstance()
  904. {
  905. if (NULL == UnityPurchasing_instance)
  906. {
  907. UnityPurchasing_instance = [[UnityPurchasing alloc] init];
  908. }
  909. return UnityPurchasing_instance;
  910. }
  911. // Make a heap allocated copy of a string.
  912. // This is suitable for passing to managed code,
  913. // which will free the string when it is garbage collected.
  914. // Stack allocated variables must not be returned as results
  915. // from managed to native calls.
  916. char* UnityPurchasingMakeHeapAllocatedStringCopy(NSString* string)
  917. {
  918. if (NULL == string)
  919. {
  920. return NULL;
  921. }
  922. char* res = (char*)malloc([string length] + 1);
  923. strcpy(res, [string UTF8String]);
  924. return res;
  925. }
  926. void setUnityPurchasingCallback(UnityPurchasingCallback callback)
  927. {
  928. [UnityPurchasing_getInstance() setCallback: callback];
  929. }
  930. void unityPurchasingRetrieveProducts(const char* json)
  931. {
  932. NSString* str = [NSString stringWithUTF8String: json];
  933. NSArray* productDefs = [UnityPurchasing deserializeProductDefs: str];
  934. NSMutableSet* productIds = [[NSMutableSet alloc] init];
  935. for (ProductDefinition* product in productDefs)
  936. {
  937. [productIds addObject: product.storeSpecificId];
  938. }
  939. [UnityPurchasing_getInstance() requestProducts: productIds];
  940. }
  941. void unityPurchasingPurchase(const char* json, const char* developerPayload)
  942. {
  943. NSString* str = [NSString stringWithUTF8String: json];
  944. ProductDefinition* product = [UnityPurchasing deserializeProductDef: str];
  945. [UnityPurchasing_getInstance() purchaseProduct: product];
  946. }
  947. void unityPurchasingFinishTransaction(const char* productJSON, const char* transactionId)
  948. {
  949. if (transactionId == NULL)
  950. return;
  951. Boolean hasProduct = productJSON != NULL;
  952. NSString* tranId = [NSString stringWithUTF8String: transactionId];
  953. [UnityPurchasing_getInstance() finishTransaction: tranId hasProduct: hasProduct];
  954. }
  955. void unityPurchasingRestoreTransactions()
  956. {
  957. UnityPurchasingLog(@"Restore transactions");
  958. [UnityPurchasing_getInstance() restorePurchases];
  959. }
  960. void unityPurchasingAddTransactionObserver()
  961. {
  962. UnityPurchasingLog(@"Add transaction observer");
  963. [UnityPurchasing_getInstance() addTransactionObserver];
  964. }
  965. void unityPurchasingRefreshAppReceipt()
  966. {
  967. UnityPurchasingLog(@"Refresh app receipt");
  968. [UnityPurchasing_getInstance() refreshReceipt];
  969. }
  970. char* getUnityPurchasingAppReceipt()
  971. {
  972. @autoreleasepool {
  973. NSString* receipt = [UnityPurchasing_getInstance() getAppReceipt];
  974. return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
  975. }
  976. }
  977. double getUnityPurchasingAppReceiptModificationDate()
  978. {
  979. NSDate* receiptModificationDate = [UnityPurchasing_getInstance() getAppReceiptModificationDate];
  980. return receiptModificationDate.timeIntervalSince1970;
  981. }
  982. char* getUnityPurchasingTransactionReceiptForProductId(const char *productId)
  983. {
  984. NSString* receipt = [UnityPurchasing_getInstance() getTransactionReceiptForProductId: [NSString stringWithUTF8String: productId]];
  985. return UnityPurchasingMakeHeapAllocatedStringCopy(receipt);
  986. }
  987. BOOL getUnityPurchasingCanMakePayments()
  988. {
  989. return [SKPaymentQueue canMakePayments];
  990. }
  991. void setSimulateAskToBuy(BOOL enabled)
  992. {
  993. UnityPurchasingLog(@"Set simulate Ask To Buy %@", enabled ? @"true" : @"false");
  994. UnityPurchasing_getInstance().simulateAskToBuyEnabled = enabled;
  995. }
  996. BOOL getSimulateAskToBuy()
  997. {
  998. return UnityPurchasing_getInstance().simulateAskToBuyEnabled;
  999. }
  1000. void unityPurchasingSetApplicationUsername(const char *username)
  1001. {
  1002. if (username == NULL)
  1003. return;
  1004. UnityPurchasing_getInstance().applicationUsername = [NSString stringWithUTF8String: username];
  1005. }
  1006. void unityPurchasingFetchStorePromotionOrder(void)
  1007. {
  1008. [UnityPurchasing_getInstance() fetchStorePromotionOrder];
  1009. }
  1010. void unityPurchasingFetchStorePromotionVisibility(const char *productId)
  1011. {
  1012. NSString* prodId = [NSString stringWithUTF8String: productId];
  1013. [UnityPurchasing_getInstance() fetchStorePromotionVisibilityForProduct: prodId];
  1014. }
  1015. // Expects json in this format:
  1016. // { "products": ["storeSpecificId1", "storeSpecificId2"] }
  1017. void unityPurchasingUpdateStorePromotionOrder(const char *json)
  1018. {
  1019. NSString* str = [NSString stringWithUTF8String: json];
  1020. NSArray* productIds = [UnityPurchasing deserializeProductIdList: str];
  1021. [UnityPurchasing_getInstance() updateStorePromotionOrder: productIds];
  1022. }
  1023. void unityPurchasingUpdateStorePromotionVisibility(const char *productId, const char *visibility)
  1024. {
  1025. NSString* prodId = [NSString stringWithUTF8String: productId];
  1026. NSString* visibilityStr = [NSString stringWithUTF8String: visibility];
  1027. [UnityPurchasing_getInstance() updateStorePromotionVisibility: visibilityStr forProduct: prodId];
  1028. }
  1029. void unityPurchasingInterceptPromotionalPurchases()
  1030. {
  1031. UnityPurchasingLog(@"Intercept promotional purchases");
  1032. UnityPurchasing_getInstance().interceptPromotionalPurchases = YES;
  1033. }
  1034. void unityPurchasingContinuePromotionalPurchases()
  1035. {
  1036. UnityPurchasingLog(@"Continue promotional purchases");
  1037. [UnityPurchasing_getInstance() initiateQueuedEarlyTransactionObserverPayments];
  1038. }
  1039. void unityPurchasingPresentCodeRedemptionSheet()
  1040. {
  1041. UnityPurchasingLog(@"Present code redemption sheet");
  1042. [UnityPurchasing_getInstance() presentCodeRedemptionSheet];
  1043. }