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

[iOS] [StoreKit2] getAvailablePurchases() depends strictly on getSubscriptions() ?

Open ngdbao opened this issue 1 year ago • 11 comments

Since migrated to StoreKit2. I have to make sure did call getSubscriptions() DONE, before call getAvailablePurchases(), or it will return empty []. Is this expected?

I also find out if an user was in subscribed with sku: abc When

        getSubscriptions({
          skus: [abc, xyz],
        }),

then getAvailablePurchases() works

but

if

        getSubscriptions({
          skus: [xyz], // removed abc
        }),

then getAvailablePurchases() return empty [], that causing my client (subscribed to abc) lost their subscription unexpectedly

Expected Behavior

getAvailablePurchases() should returns all available purchase (consumable, non-consumable,..) regardless of it's in array sku or not.

Environment:

  • react-native-iap: 12.15.7
  • react-native: expo 51
  • Platforms (iOS, Android, emulator, simulator, device): iOS production and sandbox

ngdbao avatar Dec 04 '24 16:12 ngdbao

same.

cbjs avatar Jan 09 '25 07:01 cbjs

Same issue . getSubscriptions({ skus: itemSKUs }). received empty array in published ios app but its properly working on testflight. Please help to fix issue.

MaheshRupnavar avatar Mar 17 '25 05:03 MaheshRupnavar

@MaheshRupnavar I am currently deprecating react-native-iap and working on https://github.com/hyochan/expo-iap as discussed in https://github.com/hyochan/react-native-iap/discussions/2754

hyochan avatar Mar 17 '25 08:03 hyochan

@hyochan Is expo-iap is done and available to use? And how we can implement in react-native-cli project

MaheshRupnavar avatar Mar 17 '25 08:03 MaheshRupnavar

@MaheshRupnavar You should install https://docs.expo.dev/bare/installing-expo-modules in order to use expo-iap in react-native-cli. I can say Expo IAP is currently in beta 🤔

hyochan avatar Mar 17 '25 09:03 hyochan

@hyochan so how we can fix this issue getSubscriptions({ skus: itemSKUs }). received empty array in published ios app but its properly working on testflight.

MaheshRupnavar avatar Mar 17 '25 09:03 MaheshRupnavar

@hyochan so how we can fix this issue getSubscriptions({ skus: itemSKUs }). received empty array in published ios app but its properly working on testflight.

This is a tough question! This must be related to iis conf rather than the codebase. You should be submitting iap products to app review and wait for few hours. If still not working, the ticket should be submitted to Apple

hyochan avatar Mar 17 '25 09:03 hyochan

@hyochan iap products approved already. but not getting subscriptions. also ticket submitted to Apple but no response yet . What can i do?

MaheshRupnavar avatar Mar 17 '25 09:03 MaheshRupnavar

@hyochan This is my code can you suggest what is wrong with this

import {useEffect, useState} from 'react';
import RNIap, {
  purchaseUpdatedListener,
  purchaseErrorListener,
  PurchaseStateAndroid,
  getSubscriptions,
  endConnection,
  flushFailedPurchasesCachedAsPendingAndroid,
  requestSubscription,
  finishTransaction,
  initConnection,
 
} from 'react-native-iap';
import {Alert, Platform} from 'react-native';


const useInAppPurchase = (connected: any, itemSKUs: any) => {
  const [connectionErrorMsg, setConnectionErrorMsg] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  // const {  getSubscriptions } = useIAP();
  const [successfulProductBought, setSuccessfulProductBought] = useState<any>(
    {},
  );
  const [successFlag, setSuccessFlag] = useState(false);
  const startBuy = async (productIdToBuy: any) => {
    try {
      console.log('Items SKUs from the db>>>>', itemSKUs);
      setIsLoading(true);
  
      let response: any;
      try {
        response = await getSubscriptions({ skus: itemSKUs });
      } catch (err: any) {
        console.log('Error in getSubs>>>>>', err);
       
        setIsLoading(false);
        return; // Stop execution if fetching subscriptions fails
      }
  
      console.log('getSubscriptions response>>>>>', response);
      console.log('productIdToBuy>>>>>>', productIdToBuy);
  
      const filteredResponse = response.find(
        (subs: any) => subs.productId === productIdToBuy
      );
  
      if (!filteredResponse) {
        console.log("No matching subscription found.");
        
        setIsLoading(false);
        return;
      }
  
      console.log('Filtered Response:', filteredResponse);
      
      if (Platform.OS === 'ios') {
        console.log('In iOS');
  
        try {
          const purchase = await requestSubscription({ sku: filteredResponse.productId });
          setIsLoading(false);
  
          if (purchase) {
            console.log('Subscription successful:', purchase?.transactionId);
            setSuccessfulProductBought(purchase);
            setSuccessFlag(true);
          } else {
            setIsLoading(false);
       
            console.log('Subscription failed or canceled');
          }
        } catch (error) {
          setIsLoading(false);
           //FIXME: Remove alert
          // Alert.alert("Subscription Alert", `Error in Purchase subscriptions ${error}`, [
          //   { text: "Retry", onPress: () => startBuy(productIdToBuy) },
          //   { text: "Cancel", style: "cancel" }
          // ]);
          console.error('Error purchasing subscription:', error);
        }
      } else {
        //FOR ANDROID
        setIsLoading(false);
        purchaseFullApp(filteredResponse);
      }
    } catch (e) {
      console.error('Error:' + e);
      setIsLoading(false);
      setConnectionErrorMsg('Please check your internet connection!');
    }
  };
  
  useEffect(() => {
    console.log('in useeffect');
    // Purchase update listener
    const purchaseUpdateSubscription = purchaseUpdatedListener(
      async purchase => {
        console.log('Purchase updated:', purchase?.transactionId);

        try {
          Platform.OS === 'android' && (await handlePurchaseUpdate(purchase));
        } catch (error) {
          console.error('Error handling purchase update:', error);
        }
      },
    );
  
    const purchaseErrorSubscription = purchaseErrorListener(error => {
      console.error('Purchase error:', error);
    });
    return () => {
      purchaseUpdateSubscription.remove();
      purchaseErrorSubscription.remove();
      endConnection();
    };
  }, []);

  const purchaseFullApp = async (productToBuy: any) => {
    if (connectionErrorMsg !== '') {
      setConnectionErrorMsg('');
    }
    if (!connected) {
      setConnectionErrorMsg('Please check your internet connection');
    } else if (productToBuy?.productId) {
      try {
        console.log('IAP initiated', productToBuy.productId);
        await flushFailedPurchasesCachedAsPendingAndroid();
        console.log('After flusing failed purchase');
        let subsDetails = {
          sku: productToBuy.productId,
          ...(productToBuy?.subscriptionOfferDetails?.[0]?.offerToken && {
            subscriptionOffers: [
              {
                sku: productToBuy.productId,
                offerToken: productToBuy.subscriptionOfferDetails[0].offerToken,
              },
            ],
          }),
        };
        console.log('after subsDetails>>>>', subsDetails);
        console.log('connection before subscription>>>>', connected);
        await requestSubscription(subsDetails)
          .catch(e => {
            // await RNIap.requestPurchase({sku}).catch((e)=>{
            console.log('android subscriptionn error>>>>', e);
          })
          .then(purchase => {
            if (purchase) {
              console.log('purchase success' + JSON.stringify(purchase));
              console.log('connection after purchase>>>>', connected);
              // setSuccessFlag(true);
            }
          });
      } catch (e) {
        console.error(e);
      }
    }
  };
  // Handle purchase update
  const handlePurchaseUpdate = async (purchase: any) => {
    console.log('purchase state' + purchase.purchaseStateAndroid);
    switch (purchase.purchaseStateAndroid) {
      case PurchaseStateAndroid.PURCHASED:
        console.log('Purchase state is PURCHASED, finishing transaction');
        await finishTransactionAndroid(purchase);
        break;
      case PurchaseStateAndroid.PENDING:
        console.log('Purchase state is PENDING, waiting for completion');
        break;
      case 0: // Unspecified
        console.log('Purchase state is UNSPECIFIED, retrying...');
        // Retry mechanism (e.g., polling the server or rechecking the state)
        setTimeout(() => {
          console.log('Retrying purchase update check...');
          handlePurchaseUpdate(purchase); // Re-check the purchase state
        }, 5000); // Retry after 5 seconds
        break;
      default:
        console.log('Unknown purchase state:', purchase.purchaseStateAndroid);
    }
  };

  // Finish transaction
  const finishTransactionAndroid = async (purchase: any) => {
    try {
      console.log('Finishing transaction for purchase:', purchase);
      await finishTransaction({purchase, isConsumable: false}).catch(e => {
        console.log('error in the finish transaction', e);
      }); // false for non-consumable items
      console.log('Transaction finished successfully:');
      console.log('finishtrasaction >>> successFlag >>>>>' + successFlag);
      setSuccessfulProductBought(purchase);
      setSuccessFlag(true);
    } catch (error) {
      console.error('Error finishing transaction:', error);
    }
  };
  return {
    connectionErrorMsg,
    successfulProductBought,
    startBuy,
    successFlag,
    isLoading,
  };
};
export default useInAppPurchase;

MaheshRupnavar avatar Mar 17 '25 09:03 MaheshRupnavar

Did you check https://github.com/hyochan/react-native-iap/issues/2208?

hyochan avatar Mar 17 '25 14:03 hyochan

@ngdbao thats the expected behavior

see: https://github.com/hyochan/react-native-iap/blob/d669bbc41dc865b4184cbe5e6e21607607b4a5d6/ios/RNIapIosSk2.swift#L665-L667

the product id (in your case, abc) should exists in the productStore (the internal cache):

https://github.com/hyochan/react-native-iap/blob/main/ios/RNIapIosSk2.swift#L466-L488

before it gets added to the array for returned values for getAvailablePurchases.

the call to

getSubscriptions({
          skus: [abc, xyz],
        }),

adds abc and xyz in the productStore

I think the checks for

if await productStore.getProduct(productID: transaction.productID) != nil {
                            addTransaction(transaction: transaction, verification: verification)
                        }

in the getAvailableItems method

should be removed, Android doesn't do the same check, and the method should just add the transaction to the array even if the product id that was already purchased doesn't exists in the productStore

The worked around it so call

getSubscriptions({
          skus: <all possible skus>,
        }),

before calling getAvailablePurchases

or patch the function RNIapIosSk2.getAvailablePurchases and remove all the checks for

if await productStore.getProduct(productID: transaction.productID) != nil {
                            addTransaction(transaction: transaction, verification: verification)
                        }

and just do

 addTransaction(transaction: transaction, verification: verification)

vafada avatar Apr 02 '25 00:04 vafada

I’m closing all issues reported in versions below 14, as the library now supports the new architecture with NitroModules and has been completely revamped.

FYI, getSubscriptions is no longer available and this is replaced with fetchProducts with type subs.

hyochan avatar Sep 30 '25 18:09 hyochan