Skip to content

Instantly share code, notes, and snippets.

@acominotto
Created May 22, 2024 08:01
Show Gist options
  • Save acominotto/601beb97d09609ef15a99b6f8cb92e4c to your computer and use it in GitHub Desktop.
Save acominotto/601beb97d09609ef15a99b6f8cb92e4c to your computer and use it in GitHub Desktop.
Set up with

How to validate payments for ios / android in the backend with react native iap and expo.

Set up react-native-iap

in your project :

npm install react-native-iap

then add react-native-iap to your plugins in app.config.(js|ts) or app.json

Okay, it's now installed, let's set up our products in the appstore / playstore:

1. Appstore

appstoreconnect > your app > monetization > in-app purchase

Set it up and don't forget to put a screenshot, otherwise the product won't be in the "Ready to submit" status that is necessary for it to be visible in the getProducts for react-native-iap.

If you didn't do it already, don't forget to sign the paid app agreement and fill all the documents necessary in (necessary for the products to be fetchable too):

appstoreconnect > business > your company > Paid applications agreement

And set up a shared secret (there should be a better way to validate apple's receipts but I couldn't make it work... They have their own way of doing things that I am afraid I will never understand)

appstoreconnect > your app > App information > App-Specific Shared Secret

To the sandbox! (testing your products in a development environment)

appstoreconnect > user and access > sandbox

set up a new user (it must be a new one that doesn't exist... TIP: adding +XXX to your gmail address still routes it to your gmail address e.g. tim.cook+sandbox@gmail.com - not sure he has gmail though...)

Okay you should be set up, let's go for android

2. Playstore

google play console > your app > in-app purchases

Set up your product

Now let's add your account as a tester

google play console > settings (not your app's, your company account's) > license tester

and add your account or all the testers accounts so that they can benefit from the product without paying

Ok now we need to set up a service account so that we can call google's billing api.

to do that we will need a project in the google cloud platform (don't worry it shoudn't cost you a dime).

select or create a new project in the gcp.

in this project, go to service accounts and create a new service account

name it however you prefer (eg. iap-validator)

create a json key and save it, copy the email.

Now invite this email to your google play console in:

google play console > users and permissions

All set up !!!!!!!! (took me 1 day to get to that result with the how awful the documentation is on the internet...).

Okay, now we can start setting our purchase on the smartphone:

there are 4 things to do:

  1. initialize the iap connection.
  2. get the purchases that haven't been validated.
  3. add a purchase listener to validate it with the backend (and add resources or whatever)
  4. add the purchase buttons

so let's initialize:

(I use on all the example this kind of import: import * as iap from 'react-native-iap')

await iap.initConnection()

DONE (should be in your root _layout for expo router or your entry point).

Now get the purchases:

export const iapSyncPurchases = async () => {
  try {
    await iap.flushFailedPurchasesCachedAsPendingAndroid()
  } catch (e) {
    console.log('Error flushing failed purchases: ', e)
  }
  const purchases = await iap.getAvailablePurchases()
  console.log('purchases: ', purchases)
  
  if (purchase) {
    await callBackend(purchase)
    await iap.finishTransaction({ purchase, isConsumable: true })
  }
}

This is an actual example, you do what you want with it. I call it at the start of my application, once the context has been loaded.

Now purchase listener:

export const useIapPurchases = () => {
  useEffect(() => {
    const sub = iap.purchaseUpdatedListener(async purchase => {
      if (purchase.transactionReceipt) {
        if (purchase) {
          await callBackend(purchase)
          await iap.finishTransaction({ purchase, isConsumable: true })
        }
        console.log('Transaction receipt:', purchase.transactionReceipt)
      }
    })
    return () => sub.remove()
  }, [])
}

notice how I call the same function as the backend.

And now for the purchase button:

const sku = Platform.select(REMOVE_ADS_SKU) as string

export const getWithoutAds = async () => {
  console.log('sku: ', sku)
  const res = await iap.getProducts({ skus: [sku] })
  console.log('res: ', res)
  return res[0]
}
export const requestPurchase = async () => {
  await getWithoutAds()
  await iap.requestPurchase(
    Platform.select({
      ios: {
        sku: sku,
        andDangerouslyFinishTransactionAutomaticallyIOS: false
      },
      android: {
        skus: [sku]
      }
    } as any)
  )
}

I call the get products before the requestPurchase to be certain that they're loaded and this can be optimized.

if you call requestPurchase on a Pressable, it should now make the stuff purchase sequence to launch on your smartphone.

Let's move on to the backend.

I used two services:

googleapis and google-auth-library

npm i google-auth-library googleapis

and node-apple-receipt-verify

npm i node-apple-receipt-verify

This last service might be using some deprecated api, but as I mentionned earlier, I don't want to have to deal with apple's way of doing things. I hope it's maintainted in the longer run ahah.

ok, for apple: easy

import appleReceiptVerify from 'node-apple-receipt-verify'

appleReceiptVerify.config({
  verbose: process.env.NODE_ENV !== 'production',
  environment: [process.env.APPLE_IS_SANDBOX === 'true' ? 'sandbox' : 'production'],
  secret: process.env.APPLE_SHARED_SECRET as string
})

const verifyTransaction = async (receipt: string) => {
    return new Promise<appleReceiptVerify.PurchasedProducts[]>((resolve, rej) => {
      appleReceiptVerify.validate({ receipt }, (err, puchases) => {
        if (err) {
          rej(err)
        } else {
          resolve(puchases)
        }
      })
    })
  }

the shared secret is the string we created in the appstoreconnect.

done ! so we can validate that a purchase was made for apple and use it however we want to update the db etc.

and android (now comes in the service account json key)

const client_email = process.env.GOOGLE_BILLING_SERVICE_ACCOUNT_CLIENT_EMAIL
const client_id = process.env.GOOGLE_BILLING_SERVICE_ACCOUNT_CLIENT_ID
const private_key = process.env.GOOGLE_BILLING_SERVICE_ACCOUNT_PRIVATE_KEY

const androidPublisher = new AndroidPublisherApi.Androidpublisher({
    auth: new GoogleAuth({
        credentials: {
        client_email,
        client_id,
        private_key
        },
        scopes: ['https://www.googleapis.com/auth/androidpublisher']
    })
})

const verifyPurchaseToken = async (token: string) => {
      const response = await androidPublisher.purchases.products.get({
        packageName: PROD_APPLICATION_ID,
        productId: REMOVE_ADS_SKU.android,
        token
      })
      return response.data
    }

REMOVE_ADS_SKU is from the shared package, as I use a monorepo. and simply is the sku (productId) for both platforms.

so now all you need to do is add an endpoint that you use to validate both types of purchase !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment