React-native-iap: purchaseUpdatedListener called only once on app start, and never calls after requestSubscription call

Created on 21 Apr 2020  ·  22Comments  ·  Source: dooboolab/react-native-iap

Version of react-native-iap

4.4.6

Version of react-native

0.61.5

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

ios

Expected behavior

purchaseUpdatedListener called on each time, after requestSubscription call

Actual behavior

purchaseUpdatedListener called only once on app start, and never calls after requestSubscription call

Tested environment (Emulator? Real Device?)

Real Device

Steps to reproduce the behavior

Just follow readme and try to request subscription

📱 iOS 🕵️‍♂️ need more investigation 🙏 help wanted

Most helpful comment

Had the same issue as I didn't add the initConnection() call.
The README seems to be just updated with the bold part here:

initConnection() On iOS, it will simply call canMakePayments method and return value __which is required for the listeners to work properly.__

I think this part "On iOS, it will simply call canMakePayments" should be removed, also initConnection() should be added to the basic example

I was really convinced it was optional because it wasn't in the example

Adding the initConnection() before registering the purchase listener/s fixed this for me, so this is probably related to #1002 and #756

All 22 comments

Does anyone else having the same problem? @Strate have you called finishTransaction?

@hyochan yes, but actually this call happens after app restart

@hyochan I've tried expo-in-app-purchase package, and there is no issue like this - evrything seems to be fine

@Strate I want to see your code. Can't find any error myself :(

this issue is absolutely infuriating. Is there a prior version where this is not the case?

Does anyone else having the same problem? @Strate have you called finishTransaction?

How would this be possible as the purchase is an argument in finishTransaction? Thus you need the event listener to get the argument to pass to finishTransaction.

@Sophrinix finishTransaction expected to be called inside purchaseUpdatedListener

@hyochan sorry, but looks like I don't have an actual code right now, I've switched to expo-in-app-purchase module. Looks like @Sophrinix experience same issue.

Btw, there was nothing special in my code: register listener as soon as app started, and just follow readme.

It turns out that this is a regression.

If you use the following versions then everything works:

"react": "16.8.1",
"react-native": "^0.59.10",
"react-native-iap": "4.3.0",

The problem comes when you try to use react-native-iap 4.4.4 and a version of react-native prior to 0.60

@Sophrinix I've just tried requestPurchase in my side and everything seems to be working. Could you please share some of your code?

Also, please check again if you've called initConnection. This is critical changes in a recent update that it is now necessary to be called in iOS as in android.

I confirm that reverting to 4.3.0 works. I was going nuts with the latest version, I've been debugging for over 1h trying to find the issue.

If I may add, 4.3.4 through 4.4.3 work fine, the problem begins in 4.4.4

If I may add, 4.3.4 through 4.4.3 work fine, the problem begins in 4.4.4

Same here, Downgraded from 4.4.8 to 4.4.3 and it is now working!

Does anyone else having the same problem? @Strate have you called finishTransaction?

I am having the same issue on 4.4.0 - none of the event listeners are being called, including purchaseErrorSubscription.

Just read about downgrading, I am about to try that...

Edit: This is a serious issue because:

  • it prevents purchases being sent for server processing
  • it prevents state rollback when someone cancels the purchase.

I can also confirm that 4.3.0 is working.

I had the same issue for a few days with RN 0.62.2 and react-native-iap 4.4.8
I somehow missed the initConnection() probably because it is missing from the example in the README

Had the same issue on iOS when upgrading to 4.4.8 which made some subscriptions not complete. Downgrading to 4.4.3 resolved the issue, for now. Is initConnection() mandatory? The doc makes it seems like it isn't

Had the same issue as I didn't add the initConnection() call.
The README seems to be just updated with the bold part here:

initConnection() On iOS, it will simply call canMakePayments method and return value __which is required for the listeners to work properly.__

I think this part "On iOS, it will simply call canMakePayments" should be removed, also initConnection() should be added to the basic example

I was really convinced it was optional because it wasn't in the example

Adding the initConnection() before registering the purchase listener/s fixed this for me, so this is probably related to #1002 and #756

I'm on 4.4.9 and the issue described by this ticket is happening to me as well.
The listener is only invoked when started, but if I set it and then perform the purchase, it is not called.
I solved it by subscribing to the native event directly, after looking at react-native-iap/index.ts and figuring out that's what happens internally.
This can be done at any time and will persist. (Just make sure it's done only once)

import { NativeModules, NativeEventEmitter } from 'react-native';
import { Observable } from 'rxjs/Observable';

const purchaseEvent = Observable.fromEvent(
  new NativeEventEmitter(NativeModules.RNIapIos),
  'purchase-updated'
);
const purchaseSubscription = purchaseEvent.subscribe((transactionData) => {
  // Trigger server receipt validation here...
});
const errorEvent = Observable.fromEvent(
  new NativeEventEmitter(NativeModules.RNIapIos),
  'purchase-error'
);
const errorSubscription = errorEvent.subscribe((errorData) => {
  // Handle errors here...
});

A lot of people are running into errors because initConnection() is not in the README's example code. I've added it here: #1088, but if anyone could double check my code, it'd be appreciated. (I still don't have my code fully functioning so I'm not too confident about it...)

I was also encountering this issue on v5.1.1. initConnection did not solve it. The listener callbacks are never triggered on purchaseUpdate. Works fine on android though. My workaround was to use Observables like @mrozanski did.

import React, { useContext, useEffect, useState } from 'react';
import { Alert, EmitterSubscription, Platform, NativeEventEmitter,
    NativeModules } from 'react-native';
import { connect } from 'react-redux';
import RNIap, {
    InAppPurchase,
    PurchaseError,
    SubscriptionPurchase,
    purchaseErrorListener,
    purchaseUpdatedListener,
} from 'react-native-iap';
import { Observable, Subscription } from 'rxjs';

const RNIAPEvent = new NativeEventEmitter(NativeModules.RNIapIos);
const purchaseObservable = new Observable((subscriber) => {
    RNIAPEvent.addListener('purchase-updated', (event) => {
        subscriber.next(event);
    })
})

const RNIAPErrorEvent = new NativeEventEmitter(NativeModules.RNIapIos);
const purchaseErrorObservable = new Observable((subscriber) => {
    RNIAPErrorEvent.addListener('purchase-error', (event) => {
        subscriber.next(event);
    })
})


const Wrapper = ({ ...props }) => {

    let purchaseUpdateSubscription: EmitterSubscription | Subscription | null = null;
    let purchaseErrorSubscription: EmitterSubscription | Subscription | null = null;

    const validateTransaction = async (purchase: SubscriptionPurchase) => {    
        return new Promise(async (resolve) => {
              //resolve(validationResponseFromYourBackend)
        }
    }

    const handleSubEvent = async (purchase: InAppPurchase | SubscriptionPurchase) => {
        const receipt = purchase.transactionReceipt;
        const purchaseToken = purchase.purchaseToken
        if (receipt) {
            try {
                const result = await validateTransaction(purchase);
                if (result.status === 200) {
                    RNIap.acknowledgePurchaseAndroid(purchaseToken).then(() => {
                        RNIap.finishTransaction(purchase, false)
                        .then(() => giveYourUserValue())
                        .catch((e) => {
                            //
                        })
                    })
                }
            } catch (err) {
                //
            }
        }
    }


    useEffect(() => {
        RNIap.initConnection()
            .then(() => {
                RNIap.flushFailedPurchasesCachedAsPendingAndroid().catch(() => {
                    // exception
                    })
                .then(() => {
                    if (Platform.OS === 'ios') {
                        //Documented implementation has issues purchaseUpdatedListener callback
                        purchaseUpdateSubscription = purchaseObservable.subscribe((purchase) => {
                            console.log('purchase observable', purchase)
                            handleSubEvent(purchase);
                        });

                        purchaseErrorSubscription = purchaseErrorObservable.subscribe((error) => {
                            console.log('purchaseErrorListener', error);
                        })

                    } else {
                        //for android use the documented method. Callbacks work.
                        purchaseUpdateSubscription = purchaseUpdatedListener(
                            (purchase: InAppPurchase | SubscriptionPurchase) => {
                                handleSubEvent(purchase);
                            }

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

                })
            })
        return () => {
              // clear your listeners
             //eg if Subscription purchaseErrorSubscription.unsubscribe() 
            //eg if EmitterSubscription purchaseErrorSubscription.remove() 
        }
    }, []);

    return (
        <InAppSubContext.Provider
            value={{
                someValueYouWantPassedDown: 'theValue'
            }}
        >
            {props.children}
        </InAppSubContext.Provider>
    )
}

const mapState = (state) => ({ someProps: 'yeah' });

const InAppSubscriptionManager = connect(mapState)(Wrapper);

export default InAppSubscriptionManager;

You can now use this to wrap your app in your App.tsx:


import InAppSubscriptionManager from './path/to/inAppSubscriptionManager';
const App = () => {

    return (
        <Provider store={store}>
                <InAppSubscriptionManager>

                    <AppNavigator />

                </InAppSubscriptionManager>
        </Provider>
    );
}

export default App;

Glad it helped. We have this in production since June.
It did change from Observable to this at some point (we're not using typescript)

      const purchaseEvent = new NativeEventEmitter(NativeModules.RNIapIos);
      const subscription = purchaseEvent.addListener(
        'purchase-updated',
        transactionData => {
          console.log('IAP-LOG purchase-updated');
          dispatch(validateRecepit(transactionData));
        }
      );
      const errorSubscription = purchaseEvent.addListener(
        'purchase-error',
        data => {
          crashlytics().log(`Purchase error ${JSON.stringify(data)}`);
          console.log('IAP-LOG purchase-error', data);
        }
      );
    };
Was this page helpful?
0 / 5 - 0 ratings