diff --git a/InAppUtils.xcodeproj/project.pbxproj b/InAppUtils.xcodeproj/project.pbxproj index 2d99fb0..dad2eb5 100644 --- a/InAppUtils.xcodeproj/project.pbxproj +++ b/InAppUtils.xcodeproj/project.pbxproj @@ -306,8 +306,6 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/node_modules/react-native/React/**", - "$(SRCROOT)/../react-native/React/**", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -321,8 +319,6 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/node_modules/react-native/React/**", - "$(SRCROOT)/../react-native/React/**", ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/InAppUtils/InAppUtils.h b/InAppUtils/InAppUtils.h index 7e1f805..a3a95ff 100644 --- a/InAppUtils/InAppUtils.h +++ b/InAppUtils/InAppUtils.h @@ -1,8 +1,8 @@ #import #import -#import "RCTBridgeModule.h" +#import @interface InAppUtils : NSObject -@end \ No newline at end of file +@end diff --git a/InAppUtils/InAppUtils.m b/InAppUtils/InAppUtils.m index f545cb4..021bd34 100644 --- a/InAppUtils/InAppUtils.m +++ b/InAppUtils/InAppUtils.m @@ -1,7 +1,7 @@ #import "InAppUtils.h" #import -#import "RCTLog.h" -#import "RCTUtils.h" +#import +#import #import "SKProduct+StringPrice.h" @implementation InAppUtils @@ -59,8 +59,10 @@ - (void)paymentQueue:(SKPaymentQueue *)queue NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier); RCTResponseSenderBlock callback = _callbacks[key]; NSDictionary *purchase = @{ - @"transactionIdentifier": transaction.transactionIdentifier, - @"productIdentifier": transaction.payment.productIdentifier + @"transactionDate": @(transaction.transactionDate.timeIntervalSince1970 * 1000), + @"transactionIdentifier": transaction.transactionIdentifier, + @"productIdentifier": transaction.payment.productIdentifier, + @"transactionReceipt": [[transaction transactionReceipt] base64EncodedStringWithOptions:0] }; if (callback) { callback(@[[NSNull null], purchase]); @@ -92,8 +94,22 @@ - (void)paymentQueue:(SKPaymentQueue *)queue } } +RCT_EXPORT_METHOD(purchaseProductForUser:(NSString *)productIdentifier + username:(NSString *)username + callback:(RCTResponseSenderBlock)callback) +{ + [self doPurchaseProduct:productIdentifier username:username callback:callback]; +} + RCT_EXPORT_METHOD(purchaseProduct:(NSString *)productIdentifier callback:(RCTResponseSenderBlock)callback) +{ + [self doPurchaseProduct:productIdentifier username:nil callback:callback]; +} + +- (void) doPurchaseProduct:(NSString *)productIdentifier + username:(NSString *)username + callback:(RCTResponseSenderBlock)callback { SKProduct *product; for(SKProduct *p in products) @@ -104,7 +120,10 @@ - (void)paymentQueue:(SKPaymentQueue *)queue } } if(product) { - SKPayment *payment = [SKPayment paymentWithProduct:product]; + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + if(username) { + payment.applicationUsername = username; + } [[SKPaymentQueue defaultQueue] addPayment:payment]; _callbacks[RCTKeyForInstance(payment.productIdentifier)] = callback; } else { @@ -118,7 +137,16 @@ - (void)paymentQueue:(SKPaymentQueue *)queue NSString *key = RCTKeyForInstance(@"restoreRequest"); RCTResponseSenderBlock callback = _callbacks[key]; if (callback) { - callback(@[@"restore_failed"]); + switch (error.code) + { + case SKErrorPaymentCancelled: + callback(@[@"user_cancelled"]); + break; + default: + callback(@[@"restore_failed"]); + break; + } + [_callbacks removeObjectForKey:key]; } else { RCTLogWarn(@"No callback registered for restore product request."); @@ -133,10 +161,19 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue NSMutableArray *productsArrayForJS = [NSMutableArray array]; for(SKPaymentTransaction *transaction in queue.transactions){ if(transaction.transactionState == SKPaymentTransactionStateRestored) { - NSDictionary *purchase = @{ - @"transactionIdentifier": transaction.transactionIdentifier, - @"productIdentifier": transaction.payment.productIdentifier - }; + 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; + } + [productsArrayForJS addObject:purchase]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } @@ -155,18 +192,32 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } +RCT_EXPORT_METHOD(restorePurchasesForUser:(NSString *)username + callback:(RCTResponseSenderBlock)callback) +{ + NSString *restoreRequest = @"restoreRequest"; + _callbacks[RCTKeyForInstance(restoreRequest)] = callback; + if(!username) { + callback(@[@"username_required"]); + return; + } + [[SKPaymentQueue defaultQueue] restoreCompletedTransactionsWithApplicationUsername:username]; +} + 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) +{ + BOOL canMakePayments = [SKPaymentQueue canMakePayments]; + callback(@[@(canMakePayments)]); } RCT_EXPORT_METHOD(receiptData:(RCTResponseSenderBlock)callback) @@ -204,7 +255,8 @@ - (void)productsRequest:(SKProductsRequest *)request @"currencySymbol": [item.priceLocale objectForKey:NSLocaleCurrencySymbol], @"currencyCode": [item.priceLocale objectForKey:NSLocaleCurrencyCode], @"priceString": item.priceString, - @"downloadable": item.downloadable ? @"true" : @"false" , + @"countryCode": [item.priceLocale objectForKey: NSLocaleCountryCode], + @"downloadable": item.downloadable ? @"true" : @"false" , @"description": item.localizedDescription ? item.localizedDescription : @"", @"title": item.localizedTitle ? item.localizedTitle : @"", }; @@ -218,6 +270,16 @@ - (void)productsRequest:(SKProductsRequest *)request } } +// SKProductsRequestDelegate network error +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{ + NSString *key = RCTKeyForInstance(request); + RCTResponseSenderBlock callback = _callbacks[key]; + if(callback) { + callback(@[RCTJSErrorFromNSError(error)]); + [_callbacks removeObjectForKey:key]; + } +} + - (void)dealloc { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; diff --git a/Readme.md b/Readme.md index 394329c..46b5757 100644 --- a/Readme.md +++ b/Readme.md @@ -2,11 +2,16 @@ A react-native wrapper for handling in-app purchases. +# Breaking Change + +- Due to a major breaking change in RN 0.40+, Use v5.x of this lib when installing from npm. + + # Notes - You need an Apple Developer account to use in-app purchases. -- You have to set up your in-app purchases in iTunes Connect first. Follow this [tutorial](http://stackoverflow.com/questions/19556336/how-do-you-add-an-in-app-purchase-to-an-ios-application) for an easy explanation. +- You have to set up your in-app purchases in iTunes Connect first. Follow steps 1-13 in this [tutorial](http://stackoverflow.com/questions/19556336/how-do-you-add-an-in-app-purchase-to-an-ios-application) for an easy explanation. - You have to test your in-app purchases on a real device, in-app purchases will always fail on the Simulator. @@ -16,12 +21,13 @@ A react-native wrapper for handling in-app purchases. 2. Install with rnpm: `rnpm 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;` - or for ES6: - ``` - import { NativeModules } from 'react-native' - import { InAppUtils } from 'NativeModules' - ``` +3. Whenever you want to use it within React code now you just have to do: `var InAppUtils = require('NativeModules').InAppUtils;` + or for ES6: + +``` +import { NativeModules } from 'react-native' +const { InAppUtils } = NativeModules +``` ## API @@ -39,7 +45,7 @@ InAppUtils.loadProducts(products, (error, products) => { }); ``` -**Response fields:** +**Response:** An array of product objects with the following fields: | Field | Type | Description | | -------------- | ------- | ------------------------------------------- | @@ -48,20 +54,33 @@ InAppUtils.loadProducts(products, (error, products) => { | currencySymbol | string | The currency symbol, i.e. "$" or "SEK" | | currencyCode | string | The currency code, i.e. "USD" of "SEK" | | priceString | string | Localised string of price, i.e. "$1,234.00" | +| countryCode | string | Country code of the price, i.e. "GB" or "FR"| | downloadable | boolean | Whether the purchase is downloadable | | description | string | Description string | | title | string | Title string | **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 var productIdentifier = 'com.xyz.abc'; InAppUtils.purchaseProduct(productIdentifier, (error, response) => { - // NOTE for v3.0: User can cancel the payment which will be availble as error object here. + // NOTE for v3.0: User can cancel the payment which will be available as error object here. if(response && response.productIdentifier) { - AlertIOS.alert('Purchase Successful', 'Your Transaction ID is ' + response.transactionIdentifier); + Alert.alert('Purchase Successful', 'Your Transaction ID is ' + response.transactionIdentifier); //unlock store here. } }); @@ -69,34 +88,57 @@ InAppUtils.purchaseProduct(productIdentifier, (error, 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. -**Response fields:** +**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 + +**Response:** A transaction object with the following fields: -| Field | Type | Description | -| --------------------- | ------ | -------------------------- | -| transactionIdentifier | string | The transaction identifier | -| productIdentifier | string | The product identifier | +| Field | Type | Description | +| --------------------- | ------ | -------------------------------------------------- | +| 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 | ### Restore payments ```javascript -InAppUtils.restorePurchases((error, response)=> { +InAppUtils.restorePurchases((error, response) => { if(error) { - AlertIOS.alert('itunes Error', 'Could not connect to itunes store.'); + Alert.alert('itunes Error', 'Could not connect to itunes store.'); } else { - AlertIOS.alert('Restore Successful', 'Successfully restores all your purchases.'); - //unlock store here again. + Alert.alert('Restore Successful', 'Successfully restores all your purchases.'); + + if (response.length === 0) { + Alert.alert('No Purchases', "We didn't find any purchases to restore."); + return; + } + + response.forEach((purchase) => { + if (purchase.productIdentifier === 'com.xyz.abc') { + // Handle purchased product. + } + }); } }); ``` -**Response:** An array of transactions with the following fields: +**NOTE:** `restorePurchasesForUser(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 -| Field | Type | Description | -| --------------------- | ------ | -------------------------- | -| originalTransactionIdentifier | string | The original transaction identifier | -| transactionIdentifier | string | The transaction identifier | -| productIdentifier | string | The product identifier | +**Response:** An array of transaction objects with the following fields: + +| 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 | ### Receipts @@ -106,7 +148,7 @@ iTunes receipts are associated to the users iTunes account and can be retrieved ```javascript InAppUtils.receiptData((error, receiptData)=> { if(error) { - AlertIOS.alert('itunes Error', 'Receipt not found.'); + Alert.alert('itunes Error', 'Receipt not found.'); } else { //send to validation server } @@ -115,6 +157,23 @@ InAppUtils.receiptData((error, receiptData)=> { **Response:** The receipt as a base64 encoded string. +### Can make payments + +Check if in-app purchases are enabled/disabled. + +```javascript +InAppUtils.canMakePayments((enabled) => { + if(enabled) { + Alert.alert('IAP enabled'); + } else { + Alert.alert('IAP disabled'); + } +}); +``` + +**Response:** The enabled boolean flag. + + ## Testing To test your in-app purchases, you have to *run the app on an actual device*. Using the iOS Simulator, they will always fail as the simulator cannot connect to the iTunes Store. However, you can do certain tasks like using `loadProducts` without the need to run on a real device. @@ -124,3 +183,38 @@ To test your in-app purchases, you have to *run the app on an actual device*. Us 2. Run your app on an actual iOS device. To do so, first [run the react-native server on the local network](https://facebook.github.io/react-native/docs/runningondevice.html) instead of localhost. Then connect your iDevice to your Mac via USB and [select it from the list of available devices and simulators](https://i.imgur.com/6ifsu8Q.jpg) in the very top bar. (Next to the build and stop buttons) 3. Open the app and buy something with your Sandbox Tester Apple Account! + +## Monthly Subscriptions + +You can check if the receipt is still valid using [iap-receipt-validator](https://github.com/sibelius/iap-receipt-validator) package + +```jsx +import iapReceiptValidator from 'iap-receipt-validator'; + +const password = 'b212549818ff42ecb65aa45c'; // Shared Secret from iTunes connect +const production = false; // use sandbox or production url for validation +const validateReceipt = iapReceiptValidator(password, production); + +async validate(receiptData) { + try { + const validationData = await validateReceipt(receiptData); + + // check if Auto-Renewable Subscription is still valid + // validationData['latest_receipt_info'][0].expires_date > today + } catch(err) { + console.log(err.valid, err.error, err.message) + } +} +``` + +This works on both react native and backend server, you should setup a cron job that run everyday to check if the receipt is still valid + +## Free trial period for in-app-purchase +There is nothing to set up related to this library. +Instead, If you want to set up a free trial period for in-app-purchase, you have to set it up at +iTunes Connect > your app > your in-app-purchase > free trial period (say 3-days or any period you can find from the pulldown menu) + +The flow we know at this point seems to be (auto-renewal case): +1. FIRST, user have to 'purchase' no matter the free trial period is set or not. +2. If the app is configured to have a free trial period, THEN user can use the app in that free trial period without being charged. +3. When the free trial period is over, Apple's system will start to auto-renew user's purchase, therefore user can continue to use the app, but user will be charged from that point on. diff --git a/package.json b/package.json index ae44395..d128aed 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "react-native-in-app-utils", - "version": "4.0.0", + "version": "6.0.0", "description": "A react-native wrapper for handling in-app payments.", "author": { "name": "Chirag Jain", "email": "jain_chirag04@yahoo.com", "url": "http://chiragjain.tumblr.com" }, + "license": "MIT", "repository": { "type": "git", "url": "git@github.com:chirag04/react-native-in-app-utils.git" diff --git a/react-native-in-app-utils.podspec b/react-native-in-app-utils.podspec new file mode 100644 index 0000000..ce26c4a --- /dev/null +++ b/react-native-in-app-utils.podspec @@ -0,0 +1,18 @@ +require 'json' +pjson = JSON.parse(File.read('package.json')) + +Pod::Spec.new do |s| + + s.name = pjson["name"] + s.version = pjson["version"] + s.homepage = "https://github.com/chirag04/react-native-in-app-utils" + 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.source_files = 'InAppUtils/*.{h,m}' + + s.dependency 'React' + +end