diff --git a/InAppUtils/InAppUtils.m b/InAppUtils/InAppUtils.m index eadf7e9..fb7b137 100644 --- a/InAppUtils/InAppUtils.m +++ b/InAppUtils/InAppUtils.m @@ -19,20 +19,24 @@ - (instancetype)init return self; } ++ (BOOL)requiresMainQueueSetup { + return NO; +} + - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } -RCT_EXPORT_MODULE() +RCT_EXPORT_MODULE(); -- (void)paymentQueue:(SKPaymentQueue *)queue - updatedTransactions:(NSArray *)transactions +- (void) paymentQueue:(SKPaymentQueue *)queue + updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStateFailed: { - NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); + NSString *key = transaction.payment.productIdentifier; RCTResponseSenderBlock callback = _callbacks[key]; if (callback) { callback(@[RCTJSErrorFromNSError(transaction.error)]); @@ -44,15 +48,10 @@ - (void)paymentQueue:(SKPaymentQueue *)queue break; } case SKPaymentTransactionStatePurchased: { - NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); + NSString *key = transaction.payment.productIdentifier; RCTResponseSenderBlock callback = _callbacks[key]; if (callback) { - NSDictionary *purchase = @{ - @"transactionDate": @(transaction.transactionDate.timeIntervalSince1970 * 1000), - @"transactionIdentifier": transaction.transactionIdentifier, - @"productIdentifier": transaction.payment.productIdentifier, - @"transactionReceipt": [[transaction transactionReceipt] base64EncodedStringWithOptions:0] - }; + NSDictionary *purchase = [self getPurchaseData:transaction]; callback(@[[NSNull null], purchase]); [_callbacks removeObjectForKey:key]; } else { @@ -91,7 +90,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue if(product) { SKPayment *payment = [SKPayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; - _callbacks[RCTKeyForInstance(payment.productIdentifier)] = callback; + _callbacks[payment.productIdentifier] = callback; } else { callback(@[@"invalid_product"]); } @@ -175,7 +174,7 @@ -(SKPaymentTransaction *) getTransactionByID:(NSString *) transactionID { - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { - NSString *key = RCTKeyForInstance(@"restoreRequest"); + NSString *key = @"restoreRequest"; RCTResponseSenderBlock callback = _callbacks[key]; if (callback) { switch (error.code) @@ -187,7 +186,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue callback(@[@"restore_failed"]); break; } - + [_callbacks removeObjectForKey:key]; } else { RCTLogWarn(@"No callback registered for restore product request."); @@ -196,25 +195,14 @@ - (void)paymentQueue:(SKPaymentQueue *)queue - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { - NSString *key = RCTKeyForInstance(@"restoreRequest"); + NSString *key = @"restoreRequest"; RCTResponseSenderBlock callback = _callbacks[key]; if (callback) { NSMutableArray *productsArrayForJS = [NSMutableArray array]; for(SKPaymentTransaction *transaction in queue.transactions){ if(transaction.transactionState == SKPaymentTransactionStateRestored) { - NSMutableDictionary *purchase = [NSMutableDictionary dictionaryWithDictionary: @{ - @"transactionDate": @(transaction.transactionDate.timeIntervalSince1970 * 1000), - @"transactionIdentifier": transaction.transactionIdentifier, - @"productIdentifier": transaction.payment.productIdentifier, - @"transactionReceipt": [[transaction transactionReceipt] base64EncodedStringWithOptions:0] - }]; - - SKPaymentTransaction *originalTransaction = transaction.originalTransaction; - if (originalTransaction) { - purchase[@"originalTransactionDate"] = @(originalTransaction.transactionDate.timeIntervalSince1970 * 1000); - purchase[@"originalTransactionIdentifier"] = originalTransaction.transactionIdentifier; - } + NSDictionary *purchase = [self getPurchaseData:transaction]; [productsArrayForJS addObject:purchase]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; @@ -230,38 +218,45 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue RCT_EXPORT_METHOD(restorePurchases:(RCTResponseSenderBlock)callback) { NSString *restoreRequest = @"restoreRequest"; - _callbacks[RCTKeyForInstance(restoreRequest)] = callback; + _callbacks[restoreRequest] = callback; [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } RCT_EXPORT_METHOD(loadProducts:(NSArray *)productIdentifiers callback:(RCTResponseSenderBlock)callback) { - if([SKPaymentQueue canMakePayments]){ - SKProductsRequest *productsRequest = [[SKProductsRequest alloc] - initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; - productsRequest.delegate = self; - _callbacks[RCTKeyForInstance(productsRequest)] = callback; - [productsRequest start]; - } else { - callback(@[@"not_available"]); - } + SKProductsRequest *productsRequest = [[SKProductsRequest alloc] + initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; + productsRequest.delegate = self; + _callbacks[RCTKeyForInstance(productsRequest)] = callback; + [productsRequest start]; } -RCT_EXPORT_METHOD(canMakePayments: (RCTResponseSenderBlock)callback) +RCT_EXPORT_METHOD(canMakePayments:(RCTResponseSenderBlock)callback) { BOOL canMakePayments = [SKPaymentQueue canMakePayments]; callback(@[@(canMakePayments)]); } RCT_EXPORT_METHOD(receiptData:(RCTResponseSenderBlock)callback) +{ + NSString *receipt = [self grandUnifiedReceipt]; + if (receipt == nil) { + callback(@[@"not_available"]); + } else { + callback(@[[NSNull null], receipt]); + } +} + +// Fetch Grand Unified Receipt +- (NSString *)grandUnifiedReceipt { NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl]; if (!receiptData) { - callback(@[@"not_available"]); + return nil; } else { - callback(@[[NSNull null], [receiptData base64EncodedStringWithOptions:0]]); + return [receiptData base64EncodedStringWithOptions:0]; } } @@ -304,6 +299,24 @@ - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{ } } +- (NSDictionary *)getPurchaseData:(SKPaymentTransaction *)transaction { + NSString *receipt = [self grandUnifiedReceipt]; + NSMutableDictionary *purchase = [NSMutableDictionary dictionaryWithDictionary: @{ + @"transactionDate": @(transaction.transactionDate.timeIntervalSince1970 * 1000), + @"transactionIdentifier": transaction.transactionIdentifier, + @"productIdentifier": transaction.payment.productIdentifier, + @"transactionReceipt": receipt ? receipt : [NSNull null] + }]; + // originalTransaction is available for restore purchase and purchase of cancelled/expired subscriptions + SKPaymentTransaction *originalTransaction = transaction.originalTransaction; + if (originalTransaction) { + purchase[@"originalTransactionDate"] = @(originalTransaction.transactionDate.timeIntervalSince1970 * 1000); + purchase[@"originalTransactionIdentifier"] = originalTransaction.transactionIdentifier; + } + + return purchase; +} + - (void)dealloc { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; diff --git a/Readme.md b/Readme.md index ae116ae..0dec0dc 100644 --- a/Readme.md +++ b/Readme.md @@ -1,16 +1,15 @@ # `react-native-in-app-utils` -A react-native wrapper for handling in-app purchases. +A react-native wrapper for handling in-app purchases in iOS. -# Reason to fork -- Added features Finishing the transaction manually from RN side and looping through transactions in queue. - -# Breaking Change +## Breaking Change -- Due to a major breaking change in RN 0.40+, Use v5.x of this lib when installing from npm. +- Due to a major breaking change in RN 0.40+, use version 5 or higher of this lib when installing from npm. +## Reason to fork +- Added features Finishing the transaction manually from RN side and looping through transactions in queue. -# Notes +## Notes - You need an Apple Developer account to use in-app purchases. @@ -18,13 +17,11 @@ A react-native wrapper for handling in-app purchases. - You have to test your in-app purchases on a real device, in-app purchases will always fail on the Simulator. -### Add it to your project - -1. Make sure you have `rnpm` installed: `npm install rnpm -g` +## Installation -2. Install with rnpm: `rnpm install react-native-in-app-utils` +1. Install with react-native cli `react-native install react-native-in-app-utils` -3. Whenever you want to use it within React code now you just have to do: `var InAppUtils = require('NativeModules').InAppUtils;` +2. Whenever you want to use it within React code now you just have to do: `var InAppUtils = require('NativeModules').InAppUtils;` or for ES6: ``` @@ -40,10 +37,11 @@ const { InAppUtils } = NativeModules You have to load the products first to get the correctly internationalized name and price in the correct currency. ```javascript -var products = [ +const identifiers = [ 'com.xyz.abc', ]; -InAppUtils.loadProducts(products, (error, products) => { +InAppUtils.loadProducts(identifiers, (error, products) => { + console.log(products); //update store here. }); ``` @@ -64,6 +62,18 @@ InAppUtils.loadProducts(products, (error, products) => { **Troubleshooting:** If you do not get back your product(s) then there's a good chance that something in your iTunes Connect or Xcode is not properly configured. Take a look at this [StackOverflow Answer](http://stackoverflow.com/a/11707704/293280) to determine what might be the issue(s). +### Checking if payments are allowed + +```javascript +InAppUtils.canMakePayments((canMakePayments) => { + if(!canMakePayments) { + Alert.alert('Not Allowed', 'This device is not allowed to make purchases. Please check restrictions on device'); + } +}) +``` + +**NOTE:** canMakePayments may return false because of country limitation or parental contol/restriction setup on the device. + ### Buy product ```javascript @@ -102,6 +112,8 @@ InAppUtils.loopThroughTransactions((response) => { **NOTE:** Call `loadProducts` prior to calling `purchaseProduct`, otherwise this will return `invalid_product`. If you're calling them right after each other, you will need to call `purchaseProduct` inside of the `loadProducts` callback to ensure it has had a chance to complete its call. +**NOTE:** Call `canMakePurchases` prior to calling `purchaseProduct` to ensure that the user is allowed to make a purchase. It is generally a good idea to inform the user that they are not allowed to make purchases from their account and what they can do about it instead of a cryptic error message from iTunes. + **NOTE:** `purchaseProductForUser(productIdentifier, username, callback)` is also available. https://stackoverflow.com/questions/29255568/is-there-any-way-to-know-purchase-made-by-which-itunes-account-ios/29280858#29280858 @@ -109,11 +121,14 @@ https://stackoverflow.com/questions/29255568/is-there-any-way-to-know-purchase-m | Field | Type | Description | | --------------------- | ------ | -------------------------------------------------- | +| originalTransactionDate | number | The original transaction date (ms since epoch) | +| originalTransactionIdentifier | string | The original transaction identifier | | transactionDate | number | The transaction date (ms since epoch) | | transactionIdentifier | string | The transaction identifier | | productIdentifier | string | The product identifier | | transactionReceipt | string | The transaction receipt as a base64 encoded string | +**NOTE:** `originalTransactionDate` and `originalTransactionIdentifier` are only available for subscriptions that were previously cancelled or expired. ### Restore payments diff --git a/package.json b/package.json index 9e0f046..e10c615 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-in-app-utils", - "version": "5.6.0", + "version": "6.1.0", "description": "A react-native wrapper for handling in-app payments.", "author": { "name": "Chirag Jain", diff --git a/react-native-in-app-utils.podspec b/react-native-in-app-utils.podspec index ce26c4a..035d531 100644 --- a/react-native-in-app-utils.podspec +++ b/react-native-in-app-utils.podspec @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.summary = pjson["description"] s.license = pjson["license"] s.author = { "Chirag Jain" => "jain_chirag04@yahoo.com" } - s.platform = :ios, "7.0" - s.source = { :git => "https://github.com/chirag04/react-native-in-app-utils", :tag => "#{s.version}" } + s.platform = :ios, "8.0" + s.source = { :git => "https://github.com/chirag04/react-native-in-app-utils.git", :tag => "v#{s.version}" } s.source_files = 'InAppUtils/*.{h,m}' s.dependency 'React'