react-native-iap icon indicating copy to clipboard operation
react-native-iap copied to clipboard

Request Subscription Not appearing - How to deal with pending transactions?

Open ToshKoevoets opened this issue 2 years ago • 2 comments

It seems that for Apple, as long as there are pending transactions, requestSubscription won't always appear.

Pending transactions appear to be triggered by Apple for every subscription cycle, and probably for other transactions as well.

In TestFlight, this occurs often since every 5 minutes (default settings) a transaction cycle happens. However, it seems there are sometimes even more, or perhaps they accumulate. It's very hard to understand what's going on.

I assume Apple expects us to handle these. However, we run server checks instead of in-app checks.

Running ClearTransactionIOS after connection has been established will break the in-app payment. I don't fully understand why, because it seems to run before creating a new purchase request.

We solved it for now by occasionally clearing the iOS transactions only when a user is already active and we don't have any subscription buttons rendered. That solves it for now.

I've read in some threads that it only happens to TestFlight users. However, testing them, it's hard to understand what only happens to testers and what also happens to real users. If we have unstable TestFlight apps, we are not comfortable releasing them to the public.

The documentation also doesn't explain this. Why is Apple creating all those transactions and what's the correct way of dealing with this?

ToshKoevoets avatar Jan 05 '24 14:01 ToshKoevoets

Having the same problem but couldn't find the right place the put "clearTransactionIOS". Can I put it right before requestSubscription ? .

metinaltinbas avatar Apr 24 '24 09:04 metinaltinbas

You can use 'purchaseUpdatedListener' which will receive all pending transactions which is not cleared yet & after validating the transaction receipt from backend you can clear/finish the transaction in the Frontend.


const persistSubscription = async(purchaseInfo:RNIap.SubscriptionPurchase,isRestoreSubscription=false) => {
    try{
      let purchasePayload = await getPurchasePayload(purchaseInfo,purchaseInfo.productId);
    
      const response = await ServiceManager.checkPaymentStatus(purchasePayload);
      if(response.is_acknowledged){
        console.log(`🎉 Purchase acknowledged for ${purchaseInfo.transactionId}`)
        const finishTransactionResult = await RNIap.finishTransaction({purchase:purchaseInfo,isConsumable:false});
        const isIOSTransactionFinished = Platform.OS === "ios" && finishTransactionResult;
        const isAndroidTransactionFinished = Platform.OS === "android" && (finishTransactionResult as RNIap.PurchaseResult)?.responseCode == 0;

        //If transaction finished, successfully 
        if(isIOSTransactionFinished || isAndroidTransactionFinished){
          await AsyncStorage.removeItem('purchase_payload');
          await AsyncStorage.setItem("purchase_inprogress","true");
          await AsyncStorage.setItem('plan_purchased', 'true');
          navigation.replace('PurchaseSuccessScreen');
          setShowLoader(false);
        }
   
      }else{
        console.log(`❌ Purchase not acknowledged for ${purchaseInfo.transactionId}`);
        await AsyncStorage.removeItem('purchase_payload');
        await RNIap.finishTransaction({purchase:purchaseInfo,isConsumable:false});
        setShowLoader(false);
      }
    }catch(error:any){ 
      if(error.name === "AxiosError"){
        console.log(typeof error.response.status);
        if(error.response && error.response.status >= 400 && error.response.status < 500){
          console.log(`✅ Finishing transcations for ${purchaseInfo.transactionId}`)
          await AsyncStorage.removeItem('purchase_payload');
          await RNIap.finishTransaction({purchase:purchaseInfo,isConsumable:false});
        }
      }
    }
  } 

useEffect(() => {
    let purchaseListener: EmitterSubscription;
    const initIAP = async () => {
      try {
        await RNIap.initConnection();
        if(Platform.OS === 'android'){
          await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
        }
        purchaseListener = RNIap.purchaseUpdatedListener(
          async (
            purchase: RNIap.SubscriptionPurchase | RNIap.ProductPurchase
          ) => {
              console.log(`Listener received for ${purchase.transactionId}`)
              setShowLoader(true);
              await persistSubscription(purchase);
          }
        );
      } catch (error) {
        console.log(`❌ Error while initializing IAP connection`)
      }
    };

    initIAP();

    return () => {
      if (purchaseListener) {
        purchaseListener.remove();
      }
    };
  }, []);

If it's not a valid transaction our backend return 4xx code based on which we clear/finish off the transaction. Else, if it's a valid transaction and not processed earlier in our system. Our system will process it and return acknowledge key as true.

Hope it helps :-)

mashish584 avatar Apr 29 '24 08:04 mashish584