react-native-iap
react-native-iap copied to clipboard
[iOS] [StoreKit2] getAvailablePurchases() depends strictly on getSubscriptions() ?
Since migrated to StoreKit2. I have to make sure did call getSubscriptions() DONE, before call getAvailablePurchases(), or it will return empty []. Is this expected?
I also find out if an user was in subscribed with sku: abc When
getSubscriptions({
skus: [abc, xyz],
}),
then getAvailablePurchases() works
but
if
getSubscriptions({
skus: [xyz], // removed abc
}),
then getAvailablePurchases() return empty [], that causing my client (subscribed to abc) lost their subscription unexpectedly
Expected Behavior
getAvailablePurchases() should returns all available purchase (consumable, non-consumable,..) regardless of it's in array sku or not.
Environment:
- react-native-iap: 12.15.7
- react-native: expo 51
- Platforms (iOS, Android, emulator, simulator, device): iOS production and sandbox
same.
Same issue . getSubscriptions({ skus: itemSKUs }). received empty array in published ios app but its properly working on testflight. Please help to fix issue.
@MaheshRupnavar I am currently deprecating react-native-iap and working on https://github.com/hyochan/expo-iap as discussed in https://github.com/hyochan/react-native-iap/discussions/2754
@hyochan Is expo-iap is done and available to use? And how we can implement in react-native-cli project
@MaheshRupnavar You should install https://docs.expo.dev/bare/installing-expo-modules in order to use expo-iap in react-native-cli. I can say Expo IAP is currently in beta 🤔
@hyochan so how we can fix this issue getSubscriptions({ skus: itemSKUs }). received empty array in published ios app but its properly working on testflight.
@hyochan so how we can fix this issue getSubscriptions({ skus: itemSKUs }). received empty array in published ios app but its properly working on testflight.
This is a tough question! This must be related to iis conf rather than the codebase. You should be submitting iap products to app review and wait for few hours. If still not working, the ticket should be submitted to Apple
@hyochan iap products approved already. but not getting subscriptions. also ticket submitted to Apple but no response yet . What can i do?
@hyochan This is my code can you suggest what is wrong with this
import {useEffect, useState} from 'react';
import RNIap, {
purchaseUpdatedListener,
purchaseErrorListener,
PurchaseStateAndroid,
getSubscriptions,
endConnection,
flushFailedPurchasesCachedAsPendingAndroid,
requestSubscription,
finishTransaction,
initConnection,
} from 'react-native-iap';
import {Alert, Platform} from 'react-native';
const useInAppPurchase = (connected: any, itemSKUs: any) => {
const [connectionErrorMsg, setConnectionErrorMsg] = useState('');
const [isLoading, setIsLoading] = useState(false);
// const { getSubscriptions } = useIAP();
const [successfulProductBought, setSuccessfulProductBought] = useState<any>(
{},
);
const [successFlag, setSuccessFlag] = useState(false);
const startBuy = async (productIdToBuy: any) => {
try {
console.log('Items SKUs from the db>>>>', itemSKUs);
setIsLoading(true);
let response: any;
try {
response = await getSubscriptions({ skus: itemSKUs });
} catch (err: any) {
console.log('Error in getSubs>>>>>', err);
setIsLoading(false);
return; // Stop execution if fetching subscriptions fails
}
console.log('getSubscriptions response>>>>>', response);
console.log('productIdToBuy>>>>>>', productIdToBuy);
const filteredResponse = response.find(
(subs: any) => subs.productId === productIdToBuy
);
if (!filteredResponse) {
console.log("No matching subscription found.");
setIsLoading(false);
return;
}
console.log('Filtered Response:', filteredResponse);
if (Platform.OS === 'ios') {
console.log('In iOS');
try {
const purchase = await requestSubscription({ sku: filteredResponse.productId });
setIsLoading(false);
if (purchase) {
console.log('Subscription successful:', purchase?.transactionId);
setSuccessfulProductBought(purchase);
setSuccessFlag(true);
} else {
setIsLoading(false);
console.log('Subscription failed or canceled');
}
} catch (error) {
setIsLoading(false);
//FIXME: Remove alert
// Alert.alert("Subscription Alert", `Error in Purchase subscriptions ${error}`, [
// { text: "Retry", onPress: () => startBuy(productIdToBuy) },
// { text: "Cancel", style: "cancel" }
// ]);
console.error('Error purchasing subscription:', error);
}
} else {
//FOR ANDROID
setIsLoading(false);
purchaseFullApp(filteredResponse);
}
} catch (e) {
console.error('Error:' + e);
setIsLoading(false);
setConnectionErrorMsg('Please check your internet connection!');
}
};
useEffect(() => {
console.log('in useeffect');
// Purchase update listener
const purchaseUpdateSubscription = purchaseUpdatedListener(
async purchase => {
console.log('Purchase updated:', purchase?.transactionId);
try {
Platform.OS === 'android' && (await handlePurchaseUpdate(purchase));
} catch (error) {
console.error('Error handling purchase update:', error);
}
},
);
const purchaseErrorSubscription = purchaseErrorListener(error => {
console.error('Purchase error:', error);
});
return () => {
purchaseUpdateSubscription.remove();
purchaseErrorSubscription.remove();
endConnection();
};
}, []);
const purchaseFullApp = async (productToBuy: any) => {
if (connectionErrorMsg !== '') {
setConnectionErrorMsg('');
}
if (!connected) {
setConnectionErrorMsg('Please check your internet connection');
} else if (productToBuy?.productId) {
try {
console.log('IAP initiated', productToBuy.productId);
await flushFailedPurchasesCachedAsPendingAndroid();
console.log('After flusing failed purchase');
let subsDetails = {
sku: productToBuy.productId,
...(productToBuy?.subscriptionOfferDetails?.[0]?.offerToken && {
subscriptionOffers: [
{
sku: productToBuy.productId,
offerToken: productToBuy.subscriptionOfferDetails[0].offerToken,
},
],
}),
};
console.log('after subsDetails>>>>', subsDetails);
console.log('connection before subscription>>>>', connected);
await requestSubscription(subsDetails)
.catch(e => {
// await RNIap.requestPurchase({sku}).catch((e)=>{
console.log('android subscriptionn error>>>>', e);
})
.then(purchase => {
if (purchase) {
console.log('purchase success' + JSON.stringify(purchase));
console.log('connection after purchase>>>>', connected);
// setSuccessFlag(true);
}
});
} catch (e) {
console.error(e);
}
}
};
// Handle purchase update
const handlePurchaseUpdate = async (purchase: any) => {
console.log('purchase state' + purchase.purchaseStateAndroid);
switch (purchase.purchaseStateAndroid) {
case PurchaseStateAndroid.PURCHASED:
console.log('Purchase state is PURCHASED, finishing transaction');
await finishTransactionAndroid(purchase);
break;
case PurchaseStateAndroid.PENDING:
console.log('Purchase state is PENDING, waiting for completion');
break;
case 0: // Unspecified
console.log('Purchase state is UNSPECIFIED, retrying...');
// Retry mechanism (e.g., polling the server or rechecking the state)
setTimeout(() => {
console.log('Retrying purchase update check...');
handlePurchaseUpdate(purchase); // Re-check the purchase state
}, 5000); // Retry after 5 seconds
break;
default:
console.log('Unknown purchase state:', purchase.purchaseStateAndroid);
}
};
// Finish transaction
const finishTransactionAndroid = async (purchase: any) => {
try {
console.log('Finishing transaction for purchase:', purchase);
await finishTransaction({purchase, isConsumable: false}).catch(e => {
console.log('error in the finish transaction', e);
}); // false for non-consumable items
console.log('Transaction finished successfully:');
console.log('finishtrasaction >>> successFlag >>>>>' + successFlag);
setSuccessfulProductBought(purchase);
setSuccessFlag(true);
} catch (error) {
console.error('Error finishing transaction:', error);
}
};
return {
connectionErrorMsg,
successfulProductBought,
startBuy,
successFlag,
isLoading,
};
};
export default useInAppPurchase;
Did you check https://github.com/hyochan/react-native-iap/issues/2208?
@ngdbao thats the expected behavior
see: https://github.com/hyochan/react-native-iap/blob/d669bbc41dc865b4184cbe5e6e21607607b4a5d6/ios/RNIapIosSk2.swift#L665-L667
the product id (in your case, abc) should exists in the productStore (the internal cache):
https://github.com/hyochan/react-native-iap/blob/main/ios/RNIapIosSk2.swift#L466-L488
before it gets added to the array for returned values for getAvailablePurchases.
the call to
getSubscriptions({
skus: [abc, xyz],
}),
adds abc and xyz in the productStore
I think the checks for
if await productStore.getProduct(productID: transaction.productID) != nil {
addTransaction(transaction: transaction, verification: verification)
}
in the getAvailableItems method
should be removed, Android doesn't do the same check, and the method should just add the transaction to the array even if the product id that was already purchased doesn't exists in the productStore
The worked around it so call
getSubscriptions({
skus: <all possible skus>,
}),
before calling getAvailablePurchases
or patch the function RNIapIosSk2.getAvailablePurchases and remove all the checks for
if await productStore.getProduct(productID: transaction.productID) != nil {
addTransaction(transaction: transaction, verification: verification)
}
and just do
addTransaction(transaction: transaction, verification: verification)
I’m closing all issues reported in versions below 14, as the library now supports the new architecture with NitroModules and has been completely revamped.
FYI, getSubscriptions is no longer available and this is replaced with fetchProducts with type subs.