React-native-iap: purchaseUpdatedListener called multiple times even after purchase

Created on 26 Oct 2020  ·  22Comments  ·  Source: dooboolab/react-native-iap

Version of react-native-iap

5.0.1

Version of react-native

0.63.3

Platforms you faced the error (IOS or Android or both?)

IOS

Expected behavior

purchaseUpdatedListener not getting called multiple times

Actual behavior

purchaseUpdatedListener getting called on startup multiple times, sometimes even in between

Tested environment (Emulator? Real Device?)

Real device (iOS 14, 13 in Test Flight and in AppStore)

Steps to reproduce the behavior

(Navigator Component)

const itemSkus = ['01', '02'];

const processNewPurchase = async (purchase) => {
  const { productId, transactionReceipt } = purchase;
  if (transactionReceipt !== undefined && transactionReceipt) {
       //backend call with fetch - validating receipt 
        if (data.ack === 'success') {
          console.log('finished');
          await finishTransaction(purchase);
      } else if (data.ack === 'failure') {
          props.setProcessing(false);
          console.log('error');
      }
    }
};


const getProductsIAP = useCallback(async () => {
  await clearProductsIOS();
  await clearTransactionIOS();

 try {
   const result = await initConnection();
   const products = await getProducts(itemSkus);
   props.setProducts(products);
   console.log('result', result);
 } catch (err) {
   console.warn(err.code, err.message);
 }

  purchaseUpdateSubscription = purchaseUpdatedListener(
    async (purchase) => {
      const receipt = purchase.transactionReceipt;
      console.log('purchaseUpdatedListener');
      if (receipt) {
        try {
          await processNewPurchase(purchase);
        } catch (ackErr) {
           console.log('ackErr', ackErr);
        }
      } else {
        console.log('purchaseUpdatedListener error: receipt');
      }
    },
  );

  purchaseErrorSubscription = purchaseErrorListener(
    (error: PurchaseError) => {
      console.log('purchaseErrorListener', error);
      console.log(JSON.stringify(error));
    },
  );

  setLoading(false);
}, []);


useEffect(() => {
  getProductsIAP();

  return () => {
    if (purchaseUpdateSubscription) {
      purchaseUpdateSubscription.remove();
      purchaseUpdateSubscription = null;
    }
    if (purchaseErrorSubscription) {
      purchaseErrorSubscription.remove();
      purchaseErrorSubscription = null;
    }
  };
}, []);

📱 iOS 🙏 help wanted

Most helpful comment

Still same problem. Looks like it´s purchaseUpdatedListener is broken. Please fix this asap. No solution found.

All 22 comments

Does the event fire the same purchase results?

Yes, it fires with the successful results/receipt. If there is no current receipt it fires too, but fails on the validation server.

Could you please check if your component is not rerendered? There were lots of issue when the component rerendered and listeners started several times occationally. Well that should be released tough but we need to reproduce your case.

Well, it's the "NavigatorContainer" means I am setting different states, updating stuff and so on. But only renders one and then "rerenders" for the update of different states, props...

But that's what is for - to unsubscribe to the listeners/to avoid this.

useEffect(() => {
  getProductsIAP();

  return () => {
    if (purchaseUpdateSubscription) {
      purchaseUpdateSubscription.remove();
      purchaseUpdateSubscription = null;
    }
    if (purchaseErrorSubscription) {
      purchaseErrorSubscription.remove();
      purchaseErrorSubscription = null;
    }
  };
}, []);

Well, it's the "NavigatorContainer" means I am setting different states, updating stuff and so on. But only renders one and then "rerenders" for the update of different states, props...

But that's what is for - to unsubscribe to the listeners/to avoid this.

useEffect(() => {
  getProductsIAP();

  return () => {
    if (purchaseUpdateSubscription) {
      purchaseUpdateSubscription.remove();
      purchaseUpdateSubscription = null;
    }
    if (purchaseErrorSubscription) {
      purchaseErrorSubscription.remove();
      purchaseErrorSubscription = null;
    }
  };
}, []);

Yes I have also mentioned this. Just shared the previous issue I remember. We need the reproduction.

Also, cleariing transaction or trying with different testflight account might help.

const getProductsIAP = useCallback(async () => {
  await clearProductsIOS();
  await clearTransactionIOS();

.....

Tested in in TestFlight, production and different devices.

Different account?

Yes, same result.

Strange. Could you also try the code snippet in our IapExample project?

I have the following behaviour.

1) User buys IAP.
2) purchaseUpdatedListener gets called.
3) User uninstalls the app.
4) User comes back to the app.
5) purchaseUpdatedListener gets triggered and goes in a loop.

I am using classes, but I don't see any re-render.

I am on "react-native-iap": "4.4.1" and "react-native": "0.63.3".

I have tried to upgrade to 5.0.0, but I am getting the same behaviour.

Tengo el siguiente comportamiento.

  1. El usuario compra IAP.
  2. Se llama a purchaseUpdatedListener.
  3. El usuario desinstala la aplicación.
  4. El usuario vuelve a la aplicación.
  5. purchaseUpdatedListener se activa y forma un bucle.

Estoy usando clases, pero no veo ninguna repetición.

Estoy en "react-native-iap": "4.4.1" y "react-native": "0.63.3".

Intenté actualizar a 5.0.0, pero obtengo el mismo comportamiento.

Hello one question. did you find the solution to this?

Still same problem. Looks like it´s purchaseUpdatedListener is broken. Please fix this asap. No solution found.

Having same issue with this. How is it possible that it's not fixed for months? Is this not a critical issue or has someone found a workaround for this? Also if it helps, I think I only encountered this issue when I upgraded my device from iOS 13 to iOS 14. As far as I could recall it was working fine on iOS 13 but I could be mistaken.

Same issue here.
I noticed that it happens every time I save the code and the app is hot reloaded.
In this scenario the root component is unmounted, and then remounted.

This is my useIap() hook

const useIap=()=>{

const purchaseUpdateSubscription = useRef<EmitterSubscription[]>([]);
    const purchaseErrorSubscription = useRef<EmitterSubscription[]>([]);

const init = useCallback(async () => {
        console.taggedLog(logTag, 'call iap init');

        await RNIap.initConnection();

        await RNIap.flushFailedPurchasesCachedAsPendingAndroid();

        purchaseUpdateSubscription.current.push(RNIap.purchaseUpdatedListener(async purchase => {
            console.taggedLog(logTag, 'purchaseUpdatedListener', purchase);

            try {
                await finalizePurchase(purchase);
            } catch (err) {
                handleFailedPurchase();
            }
        }));

        // use array to be sure to not accidentally overwrite subscriptions and lose access to them.
        // being an array I can always iterate and call `remove` on them
        purchaseErrorSubscription.current.push(RNIap.purchaseErrorListener(error => {
            handleFailedPurchase(error);
        }));

        updateProducts();
        return clear;
    }, []);

  const clear = useCallback(() => {
        console.taggedLog(logTag, 'clearing all');
        clearPurchaseEventListeners();
    }, [clearPurchaseEventListeners]);


 const clearPurchaseEventListeners = useCallback(() => {
        console.taggedLog(logTag, 'clearing purchase event listener', purchaseUpdateSubscription.current.length, purchaseErrorSubscription.current.length);
        purchaseUpdateSubscription.current.forEach(sub => sub.remove());
        purchaseErrorSubscription.current.forEach(sub => sub.remove());
        purchaseUpdateSubscription.current = [];
        purchaseErrorSubscription.current = [];
    }, []);

return {init,clear}
}

And then i use this code in the root component

const iap=useIap();
 useEffect(() => {
        console.log("root component mounted");
        iap.init();

        return () => {
            console.log("root component unmounted");
            iap.clear();
        };
    }, []);

The root component can happen to get unmounted and remounted, the clear method is called though, and I also see the log 'clearing purchase event listener',1,1, which means it's going to clear them.

Even in this situation, the purchase purchaseUpdatedListener is still called many times, es

any solution?

Have the same issue.
It even called in different screens.
Any updates on this?

Same issue for me. It calls the past receipts when the iap initialises the next time.

I had the same problem and I realized that the same was happening because I was not assembling and disassembling the component correctly, thus making several listeners open at the same time.

componentWillUnmount() { if (this.purchaseUpdateSubscription) { this.purchaseUpdateSubscription.remove(); this.purchaseUpdateSubscription = null; } }
Every time a purchase is made and the component is disassembled it is necessary to remove the listener/subscription and add a new one when assembling the component.
This error does not occur on android, why? I don't know, but because it works on Android it makes you think that the error is in iOS and not in the code .

A possible workaround, testing I noticed that it sometimes shows the past receipts(sent to the server) so what I did is immediately store the transactionId of each notification received, so you always check the database first for if this transactionId has been processed and if so then ignore(it's a duplicate) if not then you can proceed. If the notification comes first to the App(rather than the server) then you can put some logic to control this i.e you should only receive 1 after a certain button has been clicked etc.
After testing extensively it stopped duplicating after a while, something's going on there.

Yes, there are some sketchy workarounds, but they are all pretty ....
Please just fix this issue. This is definitely a critical and project breaking bug since October 2020....

There is only one solution, is to use : Revenuecat ;)

¿alguna solución?

Revenuecat

Was this page helpful?
0 / 5 - 0 ratings