chirag04 / react-native-in-app-utils

A react-native wrapper for handling in-app payments
MIT License
890 stars 185 forks source link

Passing finishTransaction as callback argument? #124

Open staklau opened 7 years ago

staklau commented 7 years ago

Instead of calling finishTransaction immediately, would it be possible to pass it as an argument to the callback function and only call it when all other processing is done?

- (void)paymentQueue:(SKPaymentQueue *)queue
 updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStateFailed: {
                NSString *key = RCTKeyForInstance(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];
                break;
            }
            case SKPaymentTransactionStatePurchased: {
                NSString *key = RCTKeyForInstance(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]
                                              };
                    callback(@[[NSNull null], purchase]);
                    [_callbacks removeObjectForKey:key];
                } else {
                    RCTLogWarn(@"No callback registered for transaction with state purchased.");
                }
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            }
            case SKPaymentTransactionStateRestored:
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"purchasing");
                break;
            case SKPaymentTransactionStateDeferred:
                NSLog(@"deferred");
                break;
            default:
                break;
        }
    }
}
chirag04 commented 7 years ago

Can you explain your usecase better? What do you mean by other processing?

staklau commented 7 years ago

@chirag04 When using consumable IAP items you need to make sure the items are properly processed at your server before you charge your user in the app, the common way to do this seems to be by not calling finishTransaction until you've gotten a positive response from your server.

chirag04 commented 7 years ago

Hmm. Can you not make a call to your server api before calling purchase product?

staklau commented 7 years ago

@chirag04 Then you would not be able to validate the receipt and anyone could get free stuff. This would be the way to go about it:

  1. Start the purchase product and get receipt
  2. Send receipt to server for validation and adding bought items to the database
  3. After successful response from server, call finishTransaction to complete the purchase
chirag04 commented 7 years ago

Ok. That’s ideal. What do you propose we change here?

staklau commented 7 years ago

I was thinking it would be great to use the same concept as PushNotificationIOS. There you have to "confirm" your push notification by calling PushNotificationIOS.finish() when you're done processing the notification.

Would it be possible to keep a reference to the current transaction being processed and instead of calling [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; immediately in case SKPaymentTransactionStatePurchased, you would call InAppUtils.completePurchase(); when you're done?

Something like this in Objective-C:

RCT_EXPORT_METHOD(completePurchase)
{
     [[SKPaymentQueue defaultQueue] finishTransaction:currentPurchaseTransaction];
}

And then you could use it like this in JavaScript:

           InAppUtils.purchaseProduct(productIdentifier, (error, response) => {
             if (error) {
             // handle error with no completePurchase() being called
             }
              if(response && response.productIdentifier) {
              // confirming and validating receipt with server
              confirmPurchaseWithServer(response, function(err, confirmed) {
                if (err) {
                 // handle error with no completePurchase() being called
                 }
                 if (confirmed) {
                  InAppUtils.completePurchase();
                  }
               })
              }
           });

I tried implementing this myself but I'm not well-driven in writing Native Modules.

chirag04 commented 7 years ago

we cannot do [[SKPaymentQueue defaultQueue] finishTransaction:currentPurchaseTransaction]; because there can be multiple transactions going on and there are going to be race conditions.

we can store all transactions and refer to each of them using transactionId.

Also, i don't want to make a breaking change here because not everyone is using this flow.

We can add something like purchaseConsumableProduct that takes the productId and will not call finish transaction. you will have to manually call finishTransaction from JS with the transactionId to finish that transaction.

I won't be able to work on it but happy to accept a PR.

staklau commented 7 years ago

@chirag04 I understand. Could you then please explain why the callback in case SKPaymentTransactionStatePurchased: { is not being called when I remove [[SKPaymentQueue defaultQueue] finishTransaction:transaction];? This would help get me going.

chirag04 commented 7 years ago

i see it's removed using [_callbacks removeObjectForKey:key];. you can manually remove it using removeObjectForKey

staklau commented 7 years ago

Yes but I want it to be called, I don't want to remove it. So why is it not being called when I only remove [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; from the block? It is a little counter-intuitive.

chirag04 commented 7 years ago

@staklau check you logs if you have a warning No callback registered for transaction with state purchased

staklau commented 7 years ago

@chirag04 No that's not it. The whole SKPaymentTransactionStatePurchased: { block of code is not fired when I remove the finishTransaction.

chirag04 commented 7 years ago

that's really weird. i'm not sure what's going on here.

staklau commented 7 years ago

@chirag04 I suspect the problem with finishTransaction not always firing has something to do with the order the callback and finishTransaction are being called, but I'm not sure. Anyhow, because I couldn't figure out how to call finishTransaction outside the SKPaymentTransactionStatePurchased-block, I decided to just put the request to my server directly in the objective-c code before calling finishTransaction.

Jacse commented 7 years ago

This is very relevant for me. I have used cordova-plugin-purchase before and they implement this behaviour by forcing the developer to call a finish()-method on the purchase before the purchase is cleared off the payment queue.

It looks like this:

store.when("extra chapter").approved(function(product) {
    // download the feature
    app.downloadExtraChapter().then(function() {
        product.finish();
    });
});

To elaborate I have an app where users can update their subscription status by performing an in-app purchase. Sometimes an error occurs on the server, and their subscription state has not been updated, but the transaction was processed. This leads to them trying again and be charged twice (I've even had reports of triple purchases).

The correct flow would be: 1) Perform the transaction, adding to payment queue, 2) Update server-side, if error try again 3) Only on succesful server update finish transaction and remove from payment queue

It the app is closed between any of the steps, the library should try to finish any transactions in the payment queue on app start, as described In app purchase best practices.

I really hope to see this feature implemented one way or the other. Perhaps the Cordova plugin's implementation could be consulted.