Skip to content

Merged repo #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 53 additions & 40 deletions InAppUtils/InAppUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)]);
Expand All @@ -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 {
Expand Down Expand Up @@ -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"]);
}
Expand Down Expand Up @@ -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)
Expand All @@ -187,7 +186,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue
callback(@[@"restore_failed"]);
break;
}

[_callbacks removeObjectForKey:key];
} else {
RCTLogWarn(@"No callback registered for restore product request.");
Expand All @@ -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];
Expand All @@ -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];
}
}

Expand Down Expand Up @@ -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];
Expand Down
43 changes: 29 additions & 14 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
# `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.

- 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.

### 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:

```
Expand All @@ -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.
});
```
Expand All @@ -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
Expand Down Expand Up @@ -102,18 +112,23 @@ 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

**Response:** A transaction object 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 |

**NOTE:** `originalTransactionDate` and `originalTransactionIdentifier` are only available for subscriptions that were previously cancelled or expired.

### Restore payments

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions react-native-in-app-utils.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down