I am using expo-in-app-purchases to do my purchases.
when I do a test subscription first time processNewPurchase(purchase)
only calls once (correct behaviour). But after 5 minutes (since it is a test subscription) subscription got cancelled and again try to do the subscription processNewPurchase(purchase)
calls twice and so on, (next time it is thrice).
InAppPurchases.setPurchaseListener(
({ responseCode, results, errorCode }) => {
// Purchase was successful
if (responseCode === InAppPurchases.IAPResponseCode.OK) {
results.forEach(async (purchase) => {
if (!purchase.acknowledged) {
await processNewPurchase(purchase)
// finish the transaction on platform's end
InAppPurchases.finishTransactionAsync(purchase, true)
}
})
// handle particular error codes
} else if (
responseCode === InAppPurchases.IAPResponseCode.USER_CANCELED
) {
console.log('User canceled the transaction')
} else if (responseCode === InAppPurchases.IAPResponseCode.DEFERRED) {
console.log(
'User does not have permissions to buy but requested parental approval (iOS only)'
)
} else {
console.warn(
`Something went wrong with the purchase. Received errorCode ${errorCode}`
)
}
setProcessing(false)
}
)
since setPurchaseListener
should be in global state , I use that in App.js
as <IAPManagerWrapped>
export default function App() {
return (
<ErrorBoundary onError={errorHandler}>
<Auth>
<IAPManagerWrapped>
<Provider store={store}>
<NavigationContainer ref={navigationRef}>
<StatusBar barStyle='dark-content' />
<Main navigationRef={navigationRef} />
</NavigationContainer>
</Provider>
</IAPManagerWrapped>
</Auth>
</ErrorBoundary>
)
}
In my purchase screen (PurchaseScreen.js), I am using useIAP() hook to get the product details.
const { getProducts } = useIap()
const [subscription, setSubscription] = useState([])
useEffect(() => {
getProducts().then((results) => {
console.log(results)
if (results && results.length > 0) {
const sub = results.find((o) => o.productId === subscribeId)
if (sub) {
setSubscription(sub)
}
}
})
return () => {}
}, [])
What is the reason for calling processNewPurchase(purchase)
inside setPurchaseListener
multiple times ?
Thank you.
I came across the same problem with consumable products. After spending a lot of time on debugging, I noticed that the purchase handler is being called only once; But on android it contains more than 1 products including the previous purchases in the list.
As mentioned in the official docs here
On Android, it will return both finished and unfinished purchases, hence the array return type. This is because the Google Play Billing API detects purchase updates but doesn't differentiate which item was just purchased, therefore there's no good way to tell but in general it will be whichever purchase has acknowledged set to false, so those are the ones that you have to handle in the response. Consumed items will not be returned however, so if you consume an item that record will be gone and no longer appear in the results array when a new purchase is made.
Though it states that
Consumed items will not be returned however, so if you consume an item that record will be gone and no longer appear in the results array when a new purchase is made.
But I was still getting the previously purchased item in the list even though it was acknowledged on the server side and consumed on the client side. But still when in the same session (App was not restarted and a new purchase was made), it was showing as acknowledged: false
in the array list. Therefore our check in the code !purchase.acknowledged
was not filtering out the previously purchased item(s).
results.forEach(async (purchase) => {
if (!purchase.acknowledged) {
await processNewPurchase(purchase)
// finish the transaction on platform's end
InAppPurchases.finishTransactionAsync(purchase, true)
}
})
and as we can see from the code, our processNewPurchase
function is inside the loop so it is being called multiple times depending on the array length.
So the problem is that the library does not mark the acknowledged/consumed products in the same session correctly and returns them back as non acknowledged/consumed items in the next purchase (don't know if its fault on Google Side or inside the library), and our loop which was supposed to run only once, runs more than once resulting in calling our success code multiple times.
Fortunately, in the array, when it returns the previous purchases, it returns the associated unique purchaseToken with each purchase, and we can use them to know which purchase has already been processed and which has not been processed yet. Here is a sudo code of my implementation to solve this issue.
1. Send all the purchases to server
2. Filter the unprocessed purchase from the incoming purchases. [recorded in step 3]
3. Process the filtered unprocessed purchase and keep the record of the processed purchase to help filtering the unprocessed purchase next time.
Purchase Listener gets called every time if we call getPurchaseHistoryAsync
So it is not called only when a new purchase is made. To make sure we grant correct entitlements, a verification of the purchase from Google Servers is necessary.