您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 

835 行
34 KiB

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