How to test Google's In-App Purchase
Tags: reactnative, javascript, inapppurachase
Published: 2022-04-13 21:32:05

Here's the cheatsheet on how to test Google's In-App Purchase. 

While I was trying to implement IAP I had to go through a lot of trial and errors. 

Here's what you need to know to test Google IAP:

## Google Play Store Setup
To be able to test Google IAP you need to have a few things setup prior you begin your testing.

### You need to publish an initial version of your app on play store in production
To start testing IAP you need the base version of your app to be published on play store, this allows google to fetch all the details related to your app when IAP is triggered from your app. If this is not done and you try to initiate IAP from the app, it'll throw an error saying `The item you are trying to purchase couldn't be found.` That being said you need an initial version of your app to be published, note that the initial version of app doesn't necessarily have to have IAP implementation, just a base version of app will work.

## Deciding on how to sign your app
When you'll be publishing your initial version of app you'll need to sign your APK before publishing. There are two options for this:

- You can create and manage keystore and sign your app on your own
- Let google create, manage and sign your app

Note that you can opt for either of the approach and cannot switch between once the app is published. 

The difference between the 2 is that in the first one you upload a signed APK on play store and in the other one you upload AAB (Android App Bundle) on play store and google signs it and creates an apk for you. 

Both options have pros and cons specifically for testing iap, lets discuss that

### Creating, managing and signing your own app
To do this you need to generate a keystore from Android Studio and use that to sign your app's APK before every release.

Now to test IAP you need to have a signed APK, otherwise you will not be able to initiate the purchase flow

The main advantage with this approach is having an initial version of app published on play store you can just create new signed APK's, install them on your device and test it.

There's no disadvantage to this approach per se, but the only reason I'd not recommend you to manage keystore is if you loose the keystore you will not be able to update any app that's was signed by that keystore ever.

This is where google signing keys comes into place

### Using Play App Signing
This is a simpler approach where google create, manages and signs your app's IAP and you don't have to worry about loosing your keystore ever. Now to do this instead of uploading an APK you upload an AAB instead. Google will take care of everything from that point.

Note that every-time you create a new release it take a few hours before google publishes to the users

This approach is the best and I'd recommend this, but when it comes to testing IAP this becomes a pain. Since you don't have access to the keystore the app is signed with, every-time you need a signed APK you need to upload a released AAB on play store and test the new app.

## Testing Environments
Now that you've uploaded an initial version of your app without IAP, you can start implementing IAP on the next version of your app. Now you need to create In-App products and Subscriptions on Google Play Console. To test the app with actual payment processing you need to release it either in production or one of the testing environments. Ideally you want to release it in a test environment so can test it and handle all the edge cases.

There are multiple options for creating a test env, For the purpose of this post. I'm gonna explain only the **Closed Testing** environment. For this you need to create a new track and upload you APK or AAB depending on how you decided to sign your app. Once that's done you can roll it out to tested. The list of tester can be managed in the track itself.

With this you'll be able to download the new version of your app and test it in a isolated env.

## Publishing the final version
Once you've tested your IAP implementation and handled all the edge cases you can release the app to production. This can be done from the track you used for closed testing. It takes a couple of hours to roll out the final version to your users

## Bonus
I wrote a nice hook in react-native you can use

```javascript
import * as RNIap from 'react-native-iap'
import * as Sentry from '@sentry/react-native'
import { Alert } from 'react-native'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'

import { useIap } from 'components/iap/IAPManager'

const useInAppPurchase = (itemIds, type = 'product', isPurchased = false) => {
  const itemIdsRef = useRef(itemIds)
  const { getProducts, getSubscriptions, setProcessing } = useIap()
  const [products, setProducts] = useState([])
  const [subscriptions, setSubscriptions] = useState([])

  const requestPurchase = async (itemId, onPurchaseStart, onPurchaseEnd) => {
    try {
      setProcessing(true)
      await onPurchaseStart?.()
      await RNIap.requestPurchase(itemId, false)
      await onPurchaseEnd?.()
    } catch (err) {
      setProcessing(false)
      // eslint-disable-next-line no-console
      console.error('IAP Hook', err.code, err.message)
      Sentry.captureException(err)
    }
  }

  const requestSubscription = async (itemId, onSubscriptionStart, onSubscriptionEnd) => {
    try {
      setProcessing(true)
      await onSubscriptionStart?.()
      await RNIap.requestSubscription(itemId, false)
      await onSubscriptionEnd?.()
    } catch (err) {
      setProcessing(false)
      // eslint-disable-next-line no-console
      console.error('IAP Hook', err.code, err.message)
      Sentry.captureException(err)
    }
  }

  useLayoutEffect(() => {
    itemIdsRef.current = itemIds
  })

  useEffect(() => {
    const fetchProductDetails = async (ids) => {
      setProducts(await getProducts(ids))
    }

    const fetchSubscriptionDetails = async (ids) => {
      setSubscriptions(await getSubscriptions(ids))
    }

    if (type === 'product' && !isPurchased && itemIdsRef.current?.some((item) => !!item)) {
      fetchProductDetails(itemIdsRef.current)
    }

    if (type === 'subscription' && !isPurchased && itemIdsRef.current?.some((item) => !!item)) {
      fetchSubscriptionDetails(itemIdsRef.current)
    }
  }, [getProducts, getSubscriptions, isPurchased, type])

  return {
    products,
    subscriptions,
    requestPurchase,
    requestSubscription
  }
}

export default useInAppPurchase
```

Usage:
```javascript
const { products, requestPurchase } = useInAppPurchase(productIds, 'product', isPurchased)
const { subscriptions, requestSubscription } = useInAppPurchase(subscriptionIds, 'subscription', isSubscribed)

requestPurchase(purchaseId)
requestSubscription(subscriptionId)
```