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 all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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
140 changes: 116 additions & 24 deletions InAppUtils/InAppUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,31 @@ @implementation InAppUtils
{
NSArray *products;
NSMutableDictionary *_callbacks;
BOOL hasPurchaseCompletedListeners;
SKPaymentTransaction *currentTransaction;
BOOL shouldFinishTransactions;
}

- (instancetype)init
{
if ((self = [super init])) {
hasPurchaseCompletedListeners = NO;
shouldFinishTransactions = YES;
_callbacks = [[NSMutableDictionary alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}


- (void)startObserving {
hasPurchaseCompletedListeners = YES;
}

- (void)stopObserving {
hasPurchaseCompletedListeners = NO;
}

+ (BOOL)requiresMainQueueSetup {
return NO;
}
Expand All @@ -30,44 +44,64 @@ - (dispatch_queue_t)methodQueue

RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents
{
return @[@"purchaseCompleted"];
}

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

- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStateFailed: {
NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier);
NSLog(@"purchase failed");
NSString *key = transaction.payment.productIdentifier;
RCTResponseSenderBlock callback = _callbacks[key];
if (callback) {
callback(@[RCTJSErrorFromNSError(transaction.error)]);
[_callbacks removeObjectForKey:key];
} else {
RCTLogWarn(@"No callback registered for transaction with state failed.");
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self finishTransaction:transaction];
break;
}
case SKPaymentTransactionStatePurchased: {
NSString *key = RCTKeyForInstance(transaction.payment.productIdentifier);
NSLog(@"purchased");
currentTransaction = transaction;
NSString *key = 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 {
RCTLogWarn(@"No callback registered for transaction with state purchased.");
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
if (hasPurchaseCompletedListeners) {
[self sendEventWithName:@"purchaseCompleted" body:purchase];
}
if (!callback && !hasPurchaseCompletedListeners) {
RCTLogWarn(@"No callback or listener registered for transaction with state purchased.");
}
[self finishTransaction:transaction];
break;
}
case SKPaymentTransactionStateRestored:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(@"purchase restored");
[self finishTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"purchasing");
break;
case SKPaymentTransactionStateDeferred:
NSLog(@"deferred");
NSLog(@"purchase deferred");
break;
default:
break;
Expand Down Expand Up @@ -107,25 +141,33 @@ - (void) doPurchaseProduct:(NSString *)productIdentifier
payment.applicationUsername = username;
}
[[SKPaymentQueue defaultQueue] addPayment:payment];
_callbacks[RCTKeyForInstance(payment.productIdentifier)] = callback;
_callbacks[payment.productIdentifier] = callback;
} else {
callback(@[@"invalid_product"]);
callback(@[RCTMakeError(@"invalid_product", nil, nil)]);
}
}

- (void) finishTransaction:(SKPaymentTransaction *)transaction
{
if (shouldFinishTransactions) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(@"transaction finished");
}
}

- (void)paymentQueue:(SKPaymentQueue *)queue
restoreCompletedTransactionsFailedWithError:(NSError *)error
{
NSString *key = RCTKeyForInstance(@"restoreRequest");
NSString *key = @"restoreRequest";
RCTResponseSenderBlock callback = _callbacks[key];
if (callback) {
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;
}

Expand All @@ -137,7 +179,7 @@ - (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];
Expand All @@ -147,7 +189,7 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
NSDictionary *purchase = [self getPurchaseData:transaction];

[productsArrayForJS addObject:purchase];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self finishTransaction:transaction];
}
}
callback(@[[NSNull null], productsArrayForJS]);
Expand All @@ -160,17 +202,17 @@ - (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(restorePurchasesForUser:(NSString *)username
callback:(RCTResponseSenderBlock)callback)
callback:(RCTResponseSenderBlock)callback)
{
NSString *restoreRequest = @"restoreRequest";
_callbacks[RCTKeyForInstance(restoreRequest)] = callback;
_callbacks[restoreRequest] = callback;
if(!username) {
callback(@[@"username_required"]);
callback(@[RCTMakeError(@"username_required", nil, nil)]);
return;
}
[[SKPaymentQueue defaultQueue] restoreCompletedTransactionsWithApplicationUsername:username];
Expand All @@ -189,14 +231,14 @@ - (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)
{
NSString *receipt = [self grandUnifiedReceipt];
if (receipt == nil) {
callback(@[@"not_available"]);
callback(@[RCTMakeError(@"receipt_not_available", nil, nil)]);
} else {
callback(@[[NSNull null], receipt]);
}
Expand All @@ -214,6 +256,51 @@ - (NSString *)grandUnifiedReceipt
}
}

RCT_EXPORT_METHOD(shouldFinishTransactions:(BOOL)finishTransactions
callback:(RCTResponseSenderBlock)callback) {
shouldFinishTransactions = finishTransactions;
callback(@[[NSNull null]]);
}

RCT_EXPORT_METHOD(getPurchaseTransactions:(RCTResponseSenderBlock)callback) {
NSArray *transactions = [[SKPaymentQueue defaultQueue] transactions];
NSMutableArray *purchasedTransactions = [NSMutableArray array];
for (int k = 0; k < transactions.count; k++) {
SKPaymentTransaction *transaction = transactions[k];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
NSDictionary *purchase = [self getPurchaseData:transaction];
[purchasedTransactions addObject:purchase];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
callback(@[[NSNull null], purchasedTransactions]);
}

RCT_EXPORT_METHOD(finishCurrentTransaction:(RCTResponseSenderBlock)callback) {
if (currentTransaction) {
[[SKPaymentQueue defaultQueue] finishTransaction:currentTransaction];
currentTransaction = nil;
NSLog(@"current transaction cleared");
}
callback(@[[NSNull null]]);
}

// Clears all transactions that are not in purchasing state
RCT_EXPORT_METHOD(clearCompletedTransactions:(RCTResponseSenderBlock)callback) {
NSArray *pendingTrans = [[SKPaymentQueue defaultQueue] transactions];
int transactionsCleared = 0;
for (int k = 0; k < pendingTrans.count; k++) {
SKPaymentTransaction *transaction = pendingTrans[k];
// Transactions in purchasing state cannot be cleared
if (transaction.transactionState != SKPaymentTransactionStatePurchasing) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
transactionsCleared++;
}
}
NSLog(@"cleared %i transactions", transactionsCleared);
callback(@[[NSNull null]]);
}

// SKProductsRequestDelegate protocol method
- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response
Expand All @@ -224,7 +311,7 @@ - (void)productsRequest:(SKProductsRequest *)request
products = [NSMutableArray arrayWithArray:response.products];
NSMutableArray *productsArrayForJS = [NSMutableArray array];
for(SKProduct *item in response.products) {
NSDictionary *product = @{
NSMutableDictionary *product = [NSMutableDictionary dictionaryWithDictionary:@{
@"identifier": item.productIdentifier,
@"price": item.price,
@"currencySymbol": [item.priceLocale objectForKey:NSLocaleCurrencySymbol],
Expand All @@ -234,7 +321,12 @@ - (void)productsRequest:(SKProductsRequest *)request
@"downloadable": item.isDownloadable ? @"true" : @"false" ,
@"description": item.localizedDescription ? item.localizedDescription : @"",
@"title": item.localizedTitle ? item.localizedTitle : @"",
};
}];
if (@available(iOS 11.2, *)) {
if (item.introductoryPrice) {
product[@"introPrice"] = @(item.introductoryPrice.price.floatValue) ?: @"";
}
}
[productsArrayForJS addObject:product];
}
callback(@[[NSNull null], productsArrayForJS]);
Expand Down
Loading