Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How I can get the subscription status? #2631

Open
AlexGurski opened this issue Nov 28, 2023 · 14 comments
Open

How I can get the subscription status? #2631

AlexGurski opened this issue Nov 28, 2023 · 14 comments

Comments

@AlexGurski
Copy link

AlexGurski commented Nov 28, 2023

How I can get the subscription status?

@AlexGurski AlexGurski changed the title How I can get the subscription status How I can get the subscription status? Nov 28, 2023
@isnolan
Copy link
Contributor

isnolan commented Nov 30, 2023

You can use the availablePurchases method to get

@Vittor-Javidan
Copy link
Contributor

Vittor-Javidan commented Dec 8, 2023

@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 })

@jordibuj
Copy link

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?

@Vittor-Javidan
Copy link
Contributor

@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
Copy link
Contributor

@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([]))
  )();

@anatoolybinerals
Copy link

How I can get the subscription status in 2024?

@Vittor-Javidan
Copy link
Contributor

Vittor-Javidan commented Mar 11, 2024

@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;
          }
      }
}

@anatooly
Copy link

getAvailablePurchases

but await getAvailablePurchases() nothing return

@Vittor-Javidan
Copy link
Contributor

Vittor-Javidan commented Mar 11, 2024

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
Copy link
Contributor

getAvailablePurchases

but await getAvailablePurchases() nothing return

I updated my example to be less confusing

@anatooly
Copy link

@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 #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.

@Vittor-Javidan
Copy link
Contributor

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
Copy link
Contributor

Vittor-Javidan commented Mar 12, 2024

  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
Copy link
Contributor

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]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants