Skip to content
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

Listen for purchase events + optional promise support + other updates #128

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d2bb4ed
Add PurchaseCompleted event
superandrew213 Oct 10, 2017
bac9483
Add promise option
superandrew213 Oct 10, 2017
8b93ee6
Add listener
superandrew213 Oct 10, 2017
19765eb
Update readme
superandrew213 Oct 10, 2017
97fef8b
Update Readme.md
superandrew213 Oct 11, 2017
dad0ce4
Merge remote-tracking branch 'chirag04/master' into listen-for-purcha…
superandrew213 Dec 29, 2017
ffa363f
Improve promisify function
superandrew213 Jan 2, 2018
6a0c3bd
Revert "Improve promisify function"
superandrew213 Jan 2, 2018
a4879fb
Add error arg to canMakePayments
superandrew213 Jan 2, 2018
08a7bd4
Use error objects
superandrew213 Jan 3, 2018
b131fa9
Update readme
superandrew213 Jan 3, 2018
895456d
Update readme
superandrew213 Jan 3, 2018
01ea503
Merge branch 'master' into listen-for-purchase-event
superandrew213 Jan 16, 2018
2edc791
Improve code
superandrew213 Jan 20, 2018
138c183
Rename event to purchaseCompleted
superandrew213 Jan 23, 2018
bc48565
Update readme
superandrew213 Jan 23, 2018
78ea07a
Add store payment only if purchaseCompleted listener has been added
superandrew213 Jan 23, 2018
b15792e
Default hasPurchaseCompletedListeners to NO
superandrew213 Jan 23, 2018
5ff369b
Merge remote-tracking branch 'chirag04/master' into listen-for-purcha…
superandrew213 Mar 16, 2018
cc34229
Add promise support for all methods
superandrew213 Jun 26, 2018
b506624
Merge remote-tracking branch 'upstream/master' into listen-for-purcha…
superandrew213 Aug 1, 2018
138c6d3
Fix for RN56
superandrew213 Nov 26, 2018
eac175b
Merge remote-tracking branch 'upstream/master' into listen-for-purcha…
superandrew213 Nov 26, 2018
80fe9ef
Add helpers to give more control
superandrew213 Dec 5, 2018
4960c56
Remove reference
superandrew213 Dec 6, 2018
c8cb54c
Trigger purchase event even if callback registered
superandrew213 Dec 30, 2018
1eea062
Merge branch 'listen-for-purchase-event' of https://github.com/supera…
superandrew213 Dec 30, 2018
252464b
Merge remote-tracking branch 'upstream/master' into listen-for-purcha…
superandrew213 Mar 4, 2019
9f2f0af
Add getPurchaseTransactions
superandrew213 Mar 4, 2019
a171c6f
Use appStoreReceiptURL to get receipt data
superandrew213 Jun 17, 2019
04b1523
Revert "Use appStoreReceiptURL to get receipt data"
superandrew213 Sep 25, 2019
2e08e5a
Start using Grand Unified Receipt format and remove usage of deprecat…
superandrew213 Sep 25, 2019
77afb0f
Merge remote-tracking branch 'upstream/master' into listen-for-purcha…
superandrew213 Sep 25, 2019
8679bae
Keep errors consistent
superandrew213 Sep 25, 2019
65d2b69
Add introPrice if available
superandrew213 Jun 19, 2020
a13ff6e
Fix iOS14 issue with productIdentifier
superandrew213 Sep 21, 2020
794d32f
Revert "Fix iOS14 issue with productIdentifier"
superandrew213 Sep 24, 2020
8d33665
Fix iOS14 issue with callbacks
superandrew213 Sep 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion InAppUtils/InAppUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
#import <StoreKit/StoreKit.h>

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface InAppUtils : NSObject <RCTBridgeModule, SKProductsRequestDelegate, SKPaymentTransactionObserver>
@interface InAppUtils : RCTEventEmitter <RCTBridgeModule, SKProductsRequestDelegate, SKPaymentTransactionObserver>

@end
41 changes: 32 additions & 9 deletions InAppUtils/InAppUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ @implementation InAppUtils
{
NSArray *products;
NSMutableDictionary *_callbacks;
bool hasPurchaseCompletedListeners;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we default to NO?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

- (instancetype)init
Expand All @@ -19,13 +20,33 @@ - (instancetype)init
return self;
}

-(void)startObserving {
hasPurchaseCompletedListeners = YES;
}

-(void)stopObserving {
hasPurchaseCompletedListeners = NO;
}

- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}

RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents
{
return @[@"PurchaseCompleted"];
Copy link
Owner

@chirag04 chirag04 Jan 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: purchaseCompleted maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

// Transactions initiated from App Store
- (BOOL)paymentQueue:(SKPaymentQueue *)queue
shouldAddStorePayment:(SKPayment *)payment
forProduct:(SKProduct *)product {
return true;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use hasPurchaseCompletedListeners?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions
{
Expand All @@ -46,10 +67,12 @@ - (void)paymentQueue:(SKPaymentQueue *)queue
case SKPaymentTransactionStatePurchased: {
NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier);
RCTResponseSenderBlock callback = _callbacks[key];
NSDictionary *purchase = [self getPurchaseData:transaction];
if (callback) {
NSDictionary *purchase = [self getPurchaseData:transaction];
callback(@[[NSNull null], purchase]);
[_callbacks removeObjectForKey:key];
} else if (hasPurchaseCompletedListeners) {
[self sendEventWithName:@"PurchaseCompleted" body:purchase];
} else {
RCTLogWarn(@"No callback registered for transaction with state purchased.");
}
Expand Down Expand Up @@ -105,7 +128,7 @@ - (void) doPurchaseProduct:(NSString *)productIdentifier
[[SKPaymentQueue defaultQueue] addPayment:payment];
_callbacks[RCTKeyForInstance(payment.productIdentifier)] = callback;
} else {
callback(@[@"invalid_product"]);
callback(@[RCTMakeError(@"invalid_product", nil, nil)]);
}
}

Expand All @@ -118,13 +141,13 @@ - (void)paymentQueue:(SKPaymentQueue *)queue
switch (error.code)
{
case SKErrorPaymentCancelled:
callback(@[@"user_cancelled"]);
callback(@[RCTMakeError(@"user_cancelled", nil, nil)]);
break;
default:
callback(@[@"restore_failed"]);
callback(@[RCTJSErrorFromNSError(error)]);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to be a breaking change. I think we can avoid making a breaking change with this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still leaving this for now in case we do make other breaking changes, then we might as well add this too.

break;
}

[_callbacks removeObjectForKey:key];
} else {
RCTLogWarn(@"No callback registered for restore product request.");
Expand Down Expand Up @@ -166,7 +189,7 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
NSString *restoreRequest = @"restoreRequest";
_callbacks[RCTKeyForInstance(restoreRequest)] = callback;
if(!username) {
callback(@[@"username_required"]);
callback(@[RCTMakeError(@"username_required", nil, nil)]);
return;
}
[[SKPaymentQueue defaultQueue] restoreCompletedTransactionsWithApplicationUsername:username];
Expand All @@ -185,15 +208,15 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
RCT_EXPORT_METHOD(canMakePayments: (RCTResponseSenderBlock)callback)
{
BOOL canMakePayments = [SKPaymentQueue canMakePayments];
callback(@[@(canMakePayments)]);
callback(@[[NSNull null], @(canMakePayments)]);
}

RCT_EXPORT_METHOD(receiptData:(RCTResponseSenderBlock)callback)
{
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
if (!receiptData) {
callback(@[@"not_available"]);
callback(@[RCTMakeError(@"receipt_not_available", nil, nil)]);
} else {
callback(@[[NSNull null], [receiptData base64EncodedStringWithOptions:0]]);
}
Expand Down Expand Up @@ -252,7 +275,7 @@ - (NSDictionary *)getPurchaseData:(SKPaymentTransaction *)transaction {
purchase[@"originalTransactionDate"] = @(originalTransaction.transactionDate.timeIntervalSince1970 * 1000);
purchase[@"originalTransactionIdentifier"] = originalTransaction.transactionIdentifier;
}

return purchase;
}

Expand Down
54 changes: 45 additions & 9 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@ A react-native wrapper for handling in-app purchases.

### Add it to your project

1.Install with react-native cli `react-native install react-native-in-app-utils`
1. Install: `npm i --save react-native-in-app-utils`

2. Whenever you want to use it within React code now you just have to do: `var InAppUtils = require('NativeModules').InAppUtils;`
or for ES6:
2. Link: `react-native link react-native-in-app-utils`

3. Use:

```
import { NativeModules } from 'react-native'
const { InAppUtils } = NativeModules
var InAppUtils = require('react-native-in-app-utils');
```

or

```
// ES6
import InAppUtils from 'react-native-in-app-utils';
```


Expand Down Expand Up @@ -62,8 +69,8 @@ InAppUtils.loadProducts(products, (error, products) => {
### Checking if payments are allowed

```javascript
InAppUtils.canMakePayments((canMakePayments) => {
if(!canMakePayments) {
InAppUtils.canMakePayments((error, enabled) => {
if(!enabled) {
Alert.alert('Not Allowed', 'This device is not allowed to make purchases. Please check restrictions on device');
}
})
Expand Down Expand Up @@ -112,7 +119,7 @@ InAppUtils.restorePurchases((error, response) => {
Alert.alert('itunes Error', 'Could not connect to itunes store.');
} else {
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;
Expand Down Expand Up @@ -163,7 +170,7 @@ InAppUtils.receiptData((error, receiptData)=> {
Check if in-app purchases are enabled/disabled.

```javascript
InAppUtils.canMakePayments((enabled) => {
InAppUtils.canMakePayments((error, enabled) => {
if(enabled) {
Alert.alert('IAP enabled');
} else {
Expand All @@ -174,6 +181,35 @@ InAppUtils.canMakePayments((enabled) => {

**Response:** The enabled boolean flag.

### Listen for purchase events

Can be used for purchases initiated from the App Store or subscription renewals.

```javascript
import InAppUtils from 'react-native-in-app-utils';

const listener = InAppUtils.addListener('PurchaseCompleted', purchase => {
if(purchase && purchase.productIdentifier) {
Alert.alert('Purchase Successful', 'Your Transaction ID is ' + purchase.transactionIdentifier);
//unlock store here.
}
});
```

to remove listener:

```javascript
listener.remove();
```

**Response:** A transaction object with the following fields:

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

## Testing

Expand Down
43 changes: 43 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
NativeEventEmitter,
NativeModules,
Platform,
} from 'react-native';

const { InAppUtils } = NativeModules;

const InAppUtilsEmitter = new NativeEventEmitter(InAppUtils);

const promisify = fn => (...args) => new Promise((resolve, reject) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about using RCTPromiseResolveBlock on objective-c side? it's going to be a breaking change tho. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I prefer using promises but others might still want to use callbacks. Doing it js side gives you the option to use both. Are there any advantages using RCTPromiseResolveBlock?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote to just standardize on Promises, using RCTPromiseResolveBlock. Better to have a single, simple API than two options.

fn(...args, (err, res) => err ? reject(err) : resolve(res));
});

const IAU = Platform.select({
ios: {
loadProducts: (products, cb) => cb
? InAppUtils.loadProducts(products, cb)
: promisify(InAppUtils.loadProducts)(products),

canMakePayments: cb => cb
Copy link

@joserocha3 joserocha3 Jan 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@superandrew213 - If canMakePayments returns true, your promisfy function will reject the promise. You can create a special function just for canMakePayments like this one:

const promisifyBool = fn => (...args) => new Promise((resolve, reject) => {
    fn(...args, res => res === true || false ? resolve(res) : reject(res))
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this:

const promisify = fn => (...args) => new Promise((resolve, reject) => {
  fn(...args, (err, res) => {
    if (err !== undefined && err instanceof Error) reject(err);
    // If only one argument is given and it's not an error
    if (err !== undefined && res === undefined) resolve(err);
    resolve(res);
  });
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thanks for catching that!

Copy link

@joserocha3 joserocha3 Jan 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@superandrew213 just realized that err is not always of type Error. When calling restorePurchases if the user cancels, err comes back as a string "restore_failed". Seems the rejection types are bit inconsistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ... it would be best if we enforce an error type. We can use callback(@[RCTMakeError(@"restore_failed", nil, nil)]); where restore_failed would be the error message. This will be a breaking change though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have to do a breaking change, lets keep it simple and just change canMakePayments to canMakePayments(err, res). @chirag04 what do you think? It would also be good to use error objects too.

? InAppUtils.canMakePayments(cb)
: promisify(InAppUtils.canMakePayments)(),

purchaseProduct: (productIdentifier, cb) => cb
? InAppUtils.purchaseProduct(productIdentifier, cb)
: promisify(InAppUtils.purchaseProduct)(productIdentifier),

restorePurchases: cb => cb
? InAppUtils.restorePurchases(cb)
: promisify(InAppUtils.restorePurchases)(),

receiptData: cb => cb
? InAppUtils.receiptData(cb)
: promisify(InAppUtils.receiptData)(),

addListener: InAppUtilsEmitter.addListener,
},

android: {},
});

export default IAU;