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:
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
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:
- initialize the iap connection.
- get the purchases that haven't been validated.
- add a purchase listener to validate it with the backend (and add resources or whatever)
- 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 !