Description
My first time implementing in-app purchase so I might have a bit less knowledge about the whole lifecycle of the IAP. I only have in-app products to buy virtual coins that the user can buy as many times as they like hence isConsumable
will be true for my case.
My implementation if IAP can be summed up to three changes:
App.tsx
the entry point of the app which has my navigator etc I did initConnection
Home.tsx
which is the first screen that the App.tsx loads in any case i.e. auth or non-auth users is where I added purchaseUpdatedListener
and this is also where finishTransaction
happens.BuyCoins.tsx
screen is where I getProducts
as well as requestPurchase
when a buy button is pressed.For all the three points above here is the code:
App.tsx
import {
clearTransactionIOS,
endConnection,
flushFailedPurchasesCachedAsPendingAndroid,
initConnection,
withIAPContext,
} from 'react-native-iap';
useEffect(() => {
const initIAP = async () => {
try {
await initConnection();
if (Platform.OS == 'android') {
await flushFailedPurchasesCachedAsPendingAndroid();
} else {
await clearTransactionIOS();
}
} catch (error) {
console.log('Error initializing IAP: ', error);
}
};
initIAP();
return () => {
endConnection();
};
}, []);
Home.tsx
useEffect(() => {
const subscriptionListener = purchaseUpdatedListener(
async (purchase: Purchase) => {
if (currentPurchase) {
//TODO: Send the receipt to the server
await finishTransaction({
purchase: currentPurchase,
isConsumable: true,
});
console.log(
'currentPurchase @ subscriptionListener: ',
currentPurchase.transactionReceipt,
);
}
},
);
return () => {
subscriptionListener.remove();
};
}, []);
BuyCoins.tsx
const handleProductPurchase = async (sku: Sku) => {
try {
const params =
Platform.OS === 'android'
? { skus: [sku] }
: { sku, andDangerouslyFinishTransactionAutomaticallyIOS: false };
await requestPurchase(params);
} catch (error) {
errorLog({ message: 'handleProductPurchase', error });
setIsLoading(false);
}
};
Expected Behavior
When I purchase it goes through successfully but in finish payment, if I try to print the transactionReceipt
I get:
{"orderId":"ABC123","packageName":"com.MYAPP.TEST","productId":"passtest","purchaseTime":1234,"purchaseState":0,"purchaseToken":"safkhbvkhvbkfhbvfehbv","quantity":1,"acknowledged":false}
The "acknowledged":false
should be true as it's a consumable product and as soon as the user buys any product it should return true
because I did call it after await finishTransaction...
Logs
Here are the results of the logs I have in the above code:
LOG purchase.purchaseStateAndroid: 1
LOG Transaction finished and consumed: {"orderId":"GPA.1234","packageName":"com.myapp","productId":"coins_pack_1","purchaseTime":1234,"purchaseState":0,"purchaseToken":"THE_PURCHASE_TOKEN","quantity":1,"acknowledged":false}
LOG Error finishing transaction: [Error: An unknown or unexpected error has occurred. Please try again later.]
LOG Error finishing transaction: [Error: An unknown or unexpected error has occurred. Please try again later.]
LOG Error finishing transaction: [Error: An unknown or unexpected error has occurred. Please try again later.]
LOG Error finishing transaction: [Error: Purchase failed with code: 8]
Environment:
[Optional] Additional Context
I sometimes get error in finishTransaction [Error: Purchase failed with code: 8]
In your purchaseUpdatedListener
, you’re using currentPurchase
from the useIAP
hook instead of the purchase
parameter passed to the listener.
This can lead to stale or incorrect data, causing acknowledged: false
.
First of all, to use the purchase parameter from the listener directly instead of currentPurchase:
useEffect(() => {
const subscriptionListener = purchaseUpdatedListener(
async (purchase: Purchase) => { // Use the incoming purchase parameter
try {
if (purchase.purchaseStateAndroid === PurchaseStateAndroid.PURCHASED) {
await finishTransaction({
purchase, // Use the purchase from listener
isConsumable: true,
purchaseToken: purchase.purchaseToken,
productId: purchase.productId,
});
}
} catch (error) {
console.log('FinishTransaction error:', error);
}
}
);
return () => subscriptionListener.remove();
}, [finishTransaction]); // Add finishTransaction as dependency
Remove redundant platform checks and simplify the purchase logic:
const handleProductPurchase = async (sku: Sku) => {
try {
await requestPurchase({
sku,
andDangerouslyFinishTransactionAutomaticallyIOS: false,
});
} catch (error) {
console.log('Purchase error:', error);
}
};
After finishing the transaction, check if the purchase is acknowledged:
if (Platform.OS === 'android' && !purchase.acknowledged) {
console.warn('Purchase not acknowledged!');
}