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

How I can get the subscription status?

Open AlexGurski opened this issue 7 months ago • 14 comments

How I can get the subscription status?

AlexGurski avatar Nov 28 '23 11:11 AlexGurski

You can use the availablePurchases method to get

isnolan avatar Nov 30 '23 01:11 isnolan

@yhostc getAvailablePurchases returns the all purchases being IAP or Subscriptions, right? If the user subscription expires, will it remains the old subscription inside the array returned by "getAvailablePurchases" method?

Edit: I found the "onlyIncludeActiveItems" on docs. This solves the issue of not getting a expired subscribe, right? example: await getAvailablePurchases({ onlyIncludeActiveItems: true })

Vittor-Javidan avatar Dec 08 '23 21:12 Vittor-Javidan

I'm finding this quite confusing.

getAvailablePurchases is only documented as a function returned by the useIAP hook. Nothing is said in the docs about what it does. But according to its type, It doesn't accept any parameter.

Internally it calls a getAvailablePurchases function accepting an object with several config parameters like onlyIncludeActiveItems (which has a default value of true). That function is also exported, although no reference is written to it in the docs. So I'm not sure if it's intended to be used (maybe for backwards compatibility?) but if it is, it should return only active items unless specified false explicitely.

Finally, there is this section in the troubleshooting page. I don't get if this is a warning for iOS only but it leads me to think that there is no way of knowing for certain if a user has an active subscription from the app. Because getAvailablePurchases is going to return an empty array anyway... or maybe not?

If you try the newly published IapExample app based on react native 0.72, you can see some confusing behaviour. Just buy a subscription and quit the app. You have now an active subscription. Open the app again and go to the Subscriptions page so the finishTransaction function is called, and quit again. Now open the app again and go the getAvailablePurchases, it will return empty. Now go to Subscriptions page and get the list of subscriptions. Don't do anything else. Now go back to the available purchases page. It will return your current active subscription. Really confusing. Looks like getAvailablePurchases returns different things depending on getSubscriptions having been called previously.

Can anyone in the know shed some light on this?

jordibuj avatar Dec 10 '23 16:12 jordibuj

@jordibuj After reading the implementation, both functions has the same name, but they are not the same.

The one from the getAvailablePurchases from useIAP is calling the getAvailablePurchases that you can import direcly form reac-native-iap import. But the useIAP hook instead return directly the value, its being setted inside setAvailablePurchases from useIAP hook.

look the useIAP definition:

import {useCallback} from 'react';

import {
  finishTransaction as iapFinishTransaction,
  getAvailablePurchases as iapGetAvailablePurchases,
  getProducts as iapGetProducts,
  getPurchaseHistory as iapGetPurchaseHistory,
  getSubscriptions as iapGetSubscriptions,
  requestPurchase as iapRequestPurchase,
  requestSubscription as iapRequestSubscription,
} from '../iap';
import type {PurchaseError} from '../purchaseError';
import type {Product, Purchase, PurchaseResult, Subscription} from '../types';

import {useIAPContext} from './withIAPContext';

type IAP_STATUS = {
  connected: boolean;
  products: Product[];
  promotedProductsIOS: Product[];
  subscriptions: Subscription[];
  purchaseHistory: Purchase[];
  availablePurchases: Purchase[];
  currentPurchase?: Purchase;
  currentPurchaseError?: PurchaseError;
  initConnectionError?: Error;
  finishTransaction: ({
    purchase,
    isConsumable,
    developerPayloadAndroid,
  }: {
    purchase: Purchase;
    isConsumable?: boolean;
    developerPayloadAndroid?: string;
  }) => Promise<string | boolean | PurchaseResult | void>;
  getAvailablePurchases: () => Promise<void>;
  getPurchaseHistory: () => Promise<void>;
  getProducts: ({skus}: {skus: string[]}) => Promise<void>;
  getSubscriptions: ({skus}: {skus: string[]}) => Promise<void>;
  requestPurchase: typeof iapRequestPurchase;
  requestSubscription: typeof iapRequestSubscription;
};

export const useIAP = (): IAP_STATUS => {
  const {
    connected,
    products,
    promotedProductsIOS,
    subscriptions,
    purchaseHistory,
    availablePurchases,
    currentPurchase,
    currentPurchaseError,
    initConnectionError,
    setProducts,
    setSubscriptions,
    setAvailablePurchases,
    setPurchaseHistory,
    setCurrentPurchase,
    setCurrentPurchaseError,
  } = useIAPContext();

  const getProducts = useCallback(
    async ({skus}: {skus: string[]}): Promise<void> => {
      setProducts(await iapGetProducts({skus}));
    },
    [setProducts],
  );

  const getSubscriptions = useCallback(
    async ({skus}: {skus: string[]}): Promise<void> => {
      setSubscriptions(await iapGetSubscriptions({skus}));
    },
    [setSubscriptions],
  );

  const getAvailablePurchases = useCallback(async (): Promise<void> => {
    setAvailablePurchases(await iapGetAvailablePurchases());
  }, [setAvailablePurchases]);

  const getPurchaseHistory = useCallback(async (): Promise<void> => {
    setPurchaseHistory(await iapGetPurchaseHistory());
  }, [setPurchaseHistory]);

  const finishTransaction = useCallback(
    async ({
      purchase,
      isConsumable,
      developerPayloadAndroid,
    }: {
      purchase: Purchase;
      isConsumable?: boolean;
      developerPayloadAndroid?: string;
    }): Promise<string | boolean | PurchaseResult | void> => {
      try {
        return await iapFinishTransaction({
          purchase,
          isConsumable,
          developerPayloadAndroid,
        });
      } catch (err) {
        throw err;
      } finally {
        if (purchase.productId === currentPurchase?.productId) {
          setCurrentPurchase(undefined);
        }

        if (purchase.productId === currentPurchaseError?.productId) {
          setCurrentPurchaseError(undefined);
        }
      }
    },
    [
      currentPurchase?.productId,
      currentPurchaseError?.productId,
      setCurrentPurchase,
      setCurrentPurchaseError,
    ],
  );

  return {
    connected,
    products,
    promotedProductsIOS,
    subscriptions,
    purchaseHistory,
    availablePurchases,
    currentPurchase,
    currentPurchaseError,
    initConnectionError,
    finishTransaction,
    getProducts,
    getSubscriptions,
    getAvailablePurchases,
    getPurchaseHistory,
    requestPurchase: iapRequestPurchase,
    requestSubscription: iapRequestSubscription,
  };
};

on the second import, notice the getAvailablePurchases as iapGetAvailablePurchases,

If you go to the definition of getAvailablePurchases , will be the same file as the one that you can import directly.

From this, i don't know whats happening actually, but maybe the visualization is changing because you seeing the value through availablePurchases state from useIAP, which always will have the value of empty array on first render, since its value depends on a async function call, the true getAvailablePurchases function.

Not sure if that answer the confusion, but was what i could find.

Vittor-Javidan avatar Dec 11 '23 20:12 Vittor-Javidan

@yhostc getAvailablePurchases returns the all purchases being IAP or Subscriptions, right? If the user subscription expires, will it remains the old subscription inside the array returned by "getAvailablePurchases" method?

Edit: I found the "onlyIncludeActiveItems" on docs. This solves the issue of not getting a expired subscribe, right? example: await getAvailablePurchases({ onlyIncludeActiveItems: true })

For update if someone in the future see this about my doubt:

I found my answer. When importing the props don't say nothing because JSDocs of the function is not properly configured. But the description of the props are there in the definition: https://github.com/dooboolab-community/react-native-iap/blob/e67a52e/src/iap.ts#L464

It says is IOs Only this parameter. I think i will need to test buying my own subscription on my app and let the time pass to see how this function behave on android by default, since is being hard to find this info on other issues or docs.

@param {alsoPublishToEventListener}:boolean When `true`, every element will also be pushed to the purchaseUpdated listener.
Note that this is only for backaward compatiblity. It won't publish to transactionUpdated (Storekit2) Defaults to `false`
@param {onlyIncludeActiveItems}:boolean. (IOS Sk2 only). Defaults to true, meaning that it will return the transaction if suscription has not expired. 
@See https://developer.apple.com/documentation/storekit/transaction/3851204-currententitlements for details
 *
 */
export const getAvailablePurchases = ({
  alsoPublishToEventListener = false,
  automaticallyFinishRestoredTransactions = false,
  onlyIncludeActiveItems = true,
}: {
  alsoPublishToEventListener?: boolean;
  automaticallyFinishRestoredTransactions?: boolean;
  onlyIncludeActiveItems?: boolean;
} = {}): Promise<Purchase[]> =>
  (
    Platform.select({
      ios: async () => {
        if (isIosStorekit2()) {
          return Promise.resolve(
            (
              await RNIapIosSk2.getAvailableItems(
                alsoPublishToEventListener,
                onlyIncludeActiveItems,
              )
            ).map(transactionSk2ToPurchaseMap),
          );
        } else {
          return RNIapIos.getAvailableItems(
            automaticallyFinishRestoredTransactions,
          );
        }
      },
      android: async () => {
        if (RNIapAmazonModule) {
          return await RNIapAmazonModule.getAvailableItems();
        }

        const products = await RNIapModule.getAvailableItemsByType(
          ANDROID_ITEM_TYPE_IAP,
        );

        const subscriptions = await RNIapModule.getAvailableItemsByType(
          ANDROID_ITEM_TYPE_SUBSCRIPTION,
        );

        return products.concat(subscriptions);
      },
    }) || (() => Promise.resolve([]))
  )();

Vittor-Javidan avatar Dec 14 '23 04:12 Vittor-Javidan

How I can get the subscription status in 2024?

anatoolybinerals avatar Mar 11 '24 15:03 anatoolybinerals

@anatoolybinerals

1: Retrieve all purchases from the user. You gonna receive an array of purchases (Be sure to be connected to the store). 2: You iterate this array looking for the productId of the desired subscription. If exists, it means, the user is a subscriber. (Do what you must do, and disconnect from store if you done)

Example:

import { getAvailablePurchases } from 'react-native-iap'

const PREMIUM_PLAN_SKU = 'my_premium_main_id_defined_on_the_store'

async function verifyPremium() {
      const purchases = await getAvailablePurchases();
      for (let i = 0; i < purchases.length; i++) {
          if (purchases[i].productId === PREMIUM_PLAN_SKU) {
              // If user has premium, this block of scope will be reached.
              break;
          }
      }
}

Vittor-Javidan avatar Mar 11 '24 19:03 Vittor-Javidan

getAvailablePurchases

but await getAvailablePurchases() nothing return

anatooly avatar Mar 11 '24 20:03 anatooly

getAvailablePurchases

but await getAvailablePurchases() nothing return

Oh, that's a nice question. And you half correct, as I am half correct as well. That's because we thinking about distincts "getAvailablePurchases" functions. And I should leave this explicit.

The first getAvailablePurchases function comes from useIAP. This one are meant to be used inside react components when they are mounted. So instead returning the purchases, it will store the purchases on availablePurchases state returned by useIAP hook as well.

The second getAvailablePurchases is the one you can import directly from react-native-iap library, like import { getAvailablePurchases } from 'react-native-iap'. This one will return the available purchases directly to a constant if you use await keyword (like on my previous example). This way is intent to be used inside Classes of services.

In resume:

  • Methods from useIAP will return the values directly to their respective states.
  • Methods directed imported to the file, will return the values from their call, instead being a Promise<void> type.

I don't know why, but I suppose that must be for flexibility purposes, in case you want to create a logic that is not connected to a hook, not forcing you to use inside components.

Vittor-Javidan avatar Mar 11 '24 21:03 Vittor-Javidan

getAvailablePurchases

but await getAvailablePurchases() nothing return

I updated my example to be less confusing

Vittor-Javidan avatar Mar 11 '24 22:03 Vittor-Javidan

@Vittor-Javidan thank you for all your replies, much appreciated. But it's method work?

Example your code:

import { getAvailablePurchases } from 'react-native-iap'

function verifyPremium() {
  // setup({ storekitMode: 'STOREKIT1_MODE' })
  getAvailablePurchases().then(console.log).catch(console.log)
}

=> [Error: An unknown error occurred] may be this issue https://github.com/dooboolab-community/react-native-iap/issues/2685

import { getAvailablePurchases } from 'react-native-iap'

function verifyPremium() {
  setup({ storekitMode: 'STOREKIT2_MODE' })
  getAvailablePurchases().then(console.log).catch(console.log)
}

=> []

may be I need special way for buy subscription or finishTransaction that package return.

const currentPurchase = await requestSubscription(request)
const askResult = await finishTransaction({
  purchase,
  isConsumable: true,
})

Thanks.

anatooly avatar Mar 12 '24 17:03 anatooly

When you call getAvailablePurchases, what you doing is retrieving the purchases already made in the past. In that case, sometimes you want to validate if the user is premium before the app starts. In this situation you don't necessary require a component to be mounted.

If your goal is retrieve the available subscriptions to allow the user to buy them, the code will be another. Because you will need to have a component that will represent your store inside your app.

Inside that component you gonna at least need:

1: A hook to connect and disconnect the store. 2: A hook to get your subscription, products and refresh the component when they are get. 3: A hook to listen for purchases errors 4: A hook to listen for new purchases and finish them properly 5: A function to buy the selected subscription or product.

In that case, all you need are inside the useIAP hook. Only the functions initConnection, endConnection you gonna still need to import from react-native-iap.

Vittor-Javidan avatar Mar 12 '24 18:03 Vittor-Javidan

  const {
    connected, // Checks if the store is connected
    products, // When you call `getProducts`, the products will be store on this state.
    getProducts, // Fetch the products available to be bought and store them on `products`
    requestPurchase, // Request the desired product (This shoulbe called requestProduct, since purchases are being used for both subscriptions and products.
    subscriptions, // When you call `getSubscriptions`, the subscriptions will be store on this state.
    getSubscriptions, // Fetch all subscriptions available to be bought, and store them on `subscriptions`
    requestSubscription, // Request the desired subscription
    availablePurchases, // When you call `getAvailablePurchases`, the available already made purchases will be stored on this state
    getAvailablePurchases, // Fetch all purchases already made and store them on `availablePurchases`.
    currentPurchase, // Current purchase being made by the user. This updates automatically.
    currentPurchaseError, // If the current purchase goes wrong, this will update automatically.
  } = useIAP();

Vittor-Javidan avatar Mar 12 '24 18:03 Vittor-Javidan

The current purchase and purchase error hooks are already given on documentation. For example:

  useEffect(() => {
    if (currentPurchase === false) {
      finishTransaction({ purchase: currentPurchase, isConsumable: false });
    }
  }, [currentPurchase]);

  useEffect(() => {
    if (currentPurchaseError?.message) {
      // Handle the error
    }
  }, [currentPurchaseError]);

Vittor-Javidan avatar Mar 12 '24 18:03 Vittor-Javidan