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

The purchase works but (sometimes) invalid/no response is returned? #202

Open
cuttlas opened this issue Jan 12, 2019 · 27 comments
Open

The purchase works but (sometimes) invalid/no response is returned? #202

cuttlas opened this issue Jan 12, 2019 · 27 comments

Comments

@cuttlas
Copy link

cuttlas commented Jan 12, 2019

We are using this library to handle in app purchases in a production app and approximately 1 every 10 users that try to purchase a subscription report that the payment fails. However, if they try to purchase it again, the AppStore responds that they've already purchased that product (which we've checked in the AppStore console that it's true). So, apparently the purchaseProduct function always works, but it randomly doesn't return or returns an invalid response/error. Is this happening to anybody? Any solution?

This is the code we are using:

if (Platform.OS === "ios") {
      InAppUtils.purchaseProduct(
        config.inAppPurchases.ios.productId,
        (error, response) => {
          if (response && response.productIdentifier) {
            this.props.dispatch(
              makeIosSubscription({
                transactionId: response.transactionIdentifier,
                transactionReceipt: response.transactionReceipt
              })
            );
          }
        }
      );
    }

Unfortunately I cannot confirm if in those cases the callback is never called, or is called with an error/invalid response (even though the purchase was successful). We'll try to log that and I'll update this issue with more info.

Thanks

@Kouznetsov
Copy link

Kouznetsov commented Jan 18, 2019

In the same scenario, it returns us an error telling that it could not contact the app store. Even if the user has the popup telling him that he already purchased the item.

@mezod
Copy link

mezod commented Jan 22, 2019

I have the very same issue and the problem is that I have absolutely no idea of how to debug it any further.

@vafada
Copy link

vafada commented Jan 23, 2019

We are seeing the same issue.

We are seeing ESKERRORDOMAIN0 with message: Cannot connect to iTunes Store

@sonicvision
Copy link

We have been getting this issue where users reported that the payment went through fine, yet the call back never happened or an error happened. Seems like this issue to me. Can anyone update if they found a solution for this?

@cuttlas
Copy link
Author

cuttlas commented Apr 12, 2019

We ended un implementing a workaround that worked: When the purchaseProduct method returns an error, immediately call the restorePurchase method and you'll get the right receipt.

@sonicvision
Copy link

We ended un implementing a workaround that worked: When the purchaseProduct method returns an error, immediately call the restorePurchase method and you'll get the right receipt.

But, isn't this buggy. If the purchaseproduct actually returns an error, then what would happen?

@jose920405
Copy link

@cuttlas is a bad idea, because in the apple documentation its recommended don't restore purchases automatically.

don’t automatically restore purchases, especially not every time your app is launched.

https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Restoring.html

@mezod
Copy link

mezod commented Apr 12, 2019

Of course it is not an ideal solution but it is the ONLY solution we've found so far. If we do not restore purchases automatically, about 20% of responses to successful payments fail, making it a horrible UX and hard to track, since we lose the data of the payment, having no way to know which accounts have paid. So, how do you solve this issue? In what % do you get invalid/no responses during successful purchases?

@sonicvision
Copy link

20% is a big number @mezod . Is this an issue with Apple's IAP payment system or with this framework. If this was something wrong with the IAP and the number would be 20%, then I guess the world would be on fire by now. What is your opinion on this?

@mezod
Copy link

mezod commented Apr 12, 2019

Indeed it is a horrible number. We've got over 400 payments and at least 60-70 experienced issues. People writing to us like "hey, I paid, here's the proof that Apple says I paid, but your app won't grant me access". Like the worst email you can ever get !_! I don't know where's the problem, but I know of Apple developers not using this lib or even not using react-native having this issue (not at the same level though). So either most of our users have bad connectivity, or Apple fails to respond 20% of the time... We don't have this problem with Android. I'm surprised there's no documentation on the issue or more people complaining about it. It's a very sensible issue.

@sonicvision
Copy link

@mezod Can you please point me to a resource which substantiates this :- "I don't know where's the problem, but I know of Apple developers not using this lib or even not using react-native having this issue ". I was trying to look online but could not find anything like this on Stackoverflow or other places. If you have found something online, please share.

@mezod
Copy link

mezod commented Apr 12, 2019

I don't have a link, unfortunately.

@superandrew213
Copy link
Contributor

superandrew213 commented Apr 13, 2019

This is not an issue with this lib.

When a users initiates a purchase 2 things can happen:

  1. User is logged in with Apple ID and payment method has been added to iTunes and is not expired.

  2. User is not logged in or has no/expired payment method on iTunes.

For 1, purchase will go through and a transaction with state purchased will be added to the queue.

For 2, Apple will see that user cannot complete the purchase. A transaction with state failed (purchase cancelled with error code ESKERRORDOMAIN2) will be added to the queue and user gets directed to AppStore to login and/or set up their payment method. The user will then complete the purchase and be directed back to your app. A transaction with state purchased will be added to the queue. There will be a failed and purchased transaction in the queue. Not sure why Apple does this.

You will be receiving both the failed and purchased transaction when the user gets directed back to your app. This lib will pick up the first one, which will be the failed one and trigger a purchase failed error.

You can see what the error codes mean here: #15 (comment)

You need to catch ESKERRORDOMAIN2 errors and get the next transaction in the queue. This will be the purchased transaction. This is the one you need. The current version of this lib does not have the ability to get the next transaction in the queue.

Use can use my fork here. My fork adds optional promise support and helper methods.

To install: npm i --save https://github.com/superandrew213/react-native-in-app-utils#listen-for-purchase-event

You need to do something like this:
(Don't just copy and paste this code. I just quickly made it up. Make sure you understand what is happening. We can't test it in dev mode, you will only know if it works once you have released it.)

const buy = async (productId) => {
  await IAU.loadProducts([productId])

  let purchase

  try {
    purchase = await IAU.purchaseProduct(productId)
  } catch (error) {
    if (error && error.code === 'ESKERRORDOMAIN2') {
      purchase = await getPurchaseUntilFound(productId)
    }

    if (!purchase) {
      throw error
    }
  }

  if (purchase) {
    return purchase
  }

  throw new Error('Purchase failed.')
}

const getPurchaseUntilFound = async (productId) => {
  try {
    // Wait in case purchase transaction hasn't been added to queue yet
    // Sometimes it is already in the queue and sometimes it takes more time.
    await new Promise(res => setTimeout(res, 2000))
    
    const purchases = await IAU.getPurchaseTransactions()

    const purchase = purchases.find(
      item => item.productIdentifier === productId
    )

    if (purchase) return purchase

    // Keep looking until found - add a break point so it doesn't keep running
    return getPurchaseUntilFound(productId)
  } catch (error) {
    console.log(error)
  }
}

@vafada
Copy link

vafada commented Apr 15, 2019

@superandrew213 thanks for explanation!

In my case we are seeing: ESKERRORDOMAIN0 should i also be watching for that too?

Also i think Apple throws ESKERRORDOMAIN2 when the user actually cancels the payment, so how do you determine if the user actually cancelled the payment vs user going to itunes app to put payment info?

Is there a reason why your changes aren't in this repo?

Edit: i saw your PR

@sonicvision
Copy link

Same Question... Here... If the user cancels the payment we get the ESKERRORDOMAIN2. So, in that case as well, it will keep waiting and search for the next successful transaction, which would not be present. @superandrew213 @vafada

@vafada So how did you fix it? Seems like it should ESKERRORDOMAIN0 but not sure

@superandrew213
Copy link
Contributor

@sonicvision yes it's weird that Apple triggers ESKERRORDOMAIN2 when the user cancels too. Just don't let getPurchaseUntilFound in my example above run forever. Exit after 2 - 3 tries.

During the purchase process show a loader to the user. If they cancel the purchase and trigger ESKERRORDOMAIN2, getPurchaseUntilFound will run for 4 - 6 sec, before it fails. The user will see the loader for 4 - 6 sec after they cancel.

Not the best UX, but majority won't cancel and most imprtantly purchase completes successfully.

@vafada
Copy link

vafada commented May 12, 2019

@superandrew213

I've looked at your branch and we are probably going to use your fork to deal with this issue.

Why is await IAU.shouldFinishTransactions(false) required to be called before purchasing?

the only thing that does is prevent invoking the StoreKit's finishTransaction on the SKPaymentTransactionStateFailed transaction.

Does calling finishTransaction on the failed transaction prevents the SKPaymentTransactionStatePurchased transaction from coming in?

@superandrew213
Copy link
Contributor

You don't need to call IAU.shouldFinishTransactions (updated my comment above). My purchase flow is a bit more complicated and I need to manage all transactions manually.

You just need to call InAppUtils.getPurchaseTransactions. It will get (and finish) the remaining purchase transactions in the queue.

@vafada
Copy link

vafada commented May 15, 2019

@superandrew213

Here's my pseudo code now, can you comment on it?

let shouldClearCurrentTransaction = true;

  try {
    await InAppUtils.shouldFinishTransactions(false);
    const response = await InAppUtils.purchaseProductForUser(productId, userId);
    const receiptData = await InAppUtils.receiptData();

    // send receiptData to backend

  } catch (error) {
    // user might be in StoreKit flow: https://forums.developer.apple.com/thread/6431#14831
    // Lets give StoreKit some time (10 seconds) to put the success transaction in queue
    // loop 5 times and sleep for 2 seconds per iteration
    let purchaseInStoreKitFlow = false;
    for (let i = 0; i < 5; i++) {

      await new Promise(res => setTimeout(res, 2000))

      // InAppUtils.getPurchaseTransactions finishes the transactions so we dont need to
      // manually do it
      shouldClearCurrentTransaction = false;
      const purchases = await InAppUtils.getPurchaseTransactions();

      const purchase = purchases.find(item => item.productIdentifier === productId);

      if (purchase) {
        purchaseInStoreKitFlow = true;
        const receiptData = await InAppUtils.receiptData();

            // send receiptData to backend

        // get out of the loop
        break;
      }
    }

    if (!purchaseInStoreKitFlow) {
      // Real error and not a StoreKit flow.
    

      if (error.code !== 'ESKERRORDOMAIN2') {
       // alert user
      }
      throw error;
    }
  } finally {
    await InAppUtils.clearCompletedTransactions();
    if (shouldClearCurrentTransaction) {
      await InAppUtils.finishCurrentTransaction();
    }
    await InAppUtils.shouldFinishTransactions(true);
  }

Notes:

  1. I have to track shouldClearCurrentTransaction since InAppUtils.getPurchaseTransactions finishes SKPaymentTransactionStatePurchased transactions and i'm afraid calling InAppUtils.finishCurrentTransaction would throw an error since is going to finished an already finished transaction.

  2. I need to do await InAppUtils.shouldFinishTransactions(false);

without that, I will be receiving both the failed and purchased transaction when the user gets directed back to your app and both transactions will get finished thus calling InAppUtils.getPurchaseTransactions would return an empty array since the purchased transaction has already been finished and removed from the queue

  1. we call clearCompletedTransactions since the failed transaction will still be in the queue due to await InAppUtils.shouldFinishTransactions(false);

Does all logic make sense?

@superandrew213
Copy link
Contributor

Looks ok. I would try/catch what you have in your catch block.

Make sure normal purchases go through without any issues. Then release and see if it lowers your complains.

@sonicvision
Copy link

I am glad to report that I implemented the code recommended by @superandrew213 and now we have started seeing that the code is saving customers who paid but did not get upgraded. Instead it upgrades them. Though, the only error code we see is ESKERRORDOMAIN0, which is also expected, as this is an unknown error.

@wouterds
Copy link

wouterds commented Sep 25, 2019

We're seeing the same issue.

  1. User wants to pay & initialises purchase process
  2. Fills payment info and confirms purchase
  3. Actually gets charged & receives invoice from Apple BUT the SDK errors with ESKERRORDOMAIN0
  4. Because of the error we didn't receive the receipt info and we think the purchase failed
  5. Users asking for refunds & why they didn't get what they paid for

@sonicvision
Copy link

Yup.. this has brought down our errors of this kind a lot. I have put a tracking in our app, and I see that it is surely saving some users.

@unmec
Copy link

unmec commented Sep 27, 2019

Observed a similar issue with sandbox account with RN 0.60.5
No callback fired, no error thrown, but the payment went through.

let product = 'xxx';
InAppUtils.purchaseProduct(product, async (err, res) => {
  // nothing here, codes never get executed
});

Edit
Upgraded to 6.0.2 then it worked. So do we need to upgrade for all existing projects?

@wouterds
Copy link

wouterds commented Oct 1, 2019

It's unrelated to 6.0.2 @unmec, it's not 10/10 times reproducible. That's the worst part about it. It happens a lot, but not for everyone.

@wildseansy
Copy link

wildseansy commented Jan 10, 2020

I'm getting this and it's causing a lot of angry customers, especially in the restore purchase flow. I will likely switch to https://github.com/dooboolab/react-native-iap.

They describe redesigning the library to not use a promise or callback flow, and instead use an event pattern. I believe this is what is resulting in some of the issues we're seeing here, not properly handing events in the transaction queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants