Frequently Asked Questions
General Questions
What is expo-iap?
expo-iap
is a comprehensive in-app purchase solution specifically designed for Expo and React Native applications. It provides a unified API for handling purchases across iOS and Android with TypeScript support and modern React hooks.
How is expo-iap different from react-native-iap?
expo-iap
is the official successor to react-native-iap
. After 8 years of maintaining react-native-iap
(with 3K+ stars, 230+ contributors, and 214K monthly downloads), the project is being gradually deprecated in favor of expo-iap
.
Why the migration?
- Expo-first approach: Expo is now the recommended way to start React Native projects
- Better performance: Built for New Architecture with no manual bridging required
- Lower maintenance cost: Easier to test, update, and support
- Renderer-agnostic: Works seamlessly across different React Native architectures
- Production-ready: Already used in major apps with 2M+ daily active users
Key improvements in expo-iap:
- Native Expo Modules integration
- Modern React hooks API (
useIAP
) - Enhanced TypeScript support with full type safety
- Streamlined error handling with centralized error codes
- Cleaner APIs and smoother developer experience
- Out-of-the-box New Architecture support
For more details about the migration and reasoning, see:
Can I use expo-iap in a bare React Native project?
Yes! expo-iap
works in both Expo managed and bare React Native projects. However, it's optimized for Expo workflows.
Setup and Configuration
Why can't I test in-app purchases in Expo Go?
In-app purchases require native modules that aren't available in Expo Go. You need to create a custom development client or use a bare workflow.
Do I need to configure anything in my app stores?
Yes, you need to:
iOS (App Store Connect):
- Complete agreements, tax, and banking information
- Create in-app purchase products
- Set up sandbox test accounts
Android (Google Play Console):
- Upload your app to a testing track
- Create in-app products
- Add test accounts
How long does it take for products to become available?
- iOS: Products can take up to 24 hours to become available in sandbox
- Android: Products are usually available immediately after app upload
Products and Purchases
Why does getProducts()
return an empty array?
Common causes:
- Connection not established - check
connected
state - Product IDs don't match store configuration exactly
- Products not yet approved/available (iOS)
- App not uploaded to store (Android)
- Testing on simulator/emulator
const {connected, getProducts} = useIAP();
useEffect(() => {
if (connected) {
getProducts({skus: ['com.yourapp.product1']});
}
}, [connected]);
Can I purchase products without calling getProducts()
first?
No, you should always call getProducts()
first. This ensures:
- Products are available and properly configured
- You have the latest pricing and product information
- The store connection is established
How do users cancel subscriptions?
Users cannot cancel subscriptions within your app. You need to direct them to the platform-specific subscription management:
import {deepLinkToSubscriptions} from 'expo-iap';
const openSubscriptionManagement = () => {
deepLinkToSubscriptions();
};
How do I restore purchases?
For non-consumable products and subscriptions:
const {getAvailablePurchases} = useIAP();
const restorePurchases = async () => {
try {
const purchases = await getAvailablePurchases();
// Process and validate restored purchases
for (const purchase of purchases) {
await validateAndGrantPurchase(purchase);
}
} catch (error) {
console.error('Restore failed:', error);
}
};
Receipt Validation
Do I need to validate receipts on my server?
Yes, absolutely! Client-side validation is not secure. Always validate receipts on your secure server to prevent fraud.
What should I do after successful receipt validation?
- Grant the purchase to the user (update database, unlock features, etc.)
- Call
finishTransaction()
to complete the purchase - Log the transaction for your records
const handlePurchaseUpdate = async (purchase) => {
try {
// 1. Validate on server
const isValid = await validateReceiptOnServer(purchase);
if (isValid) {
// 2. Grant purchase
await grantPurchaseToUser(purchase);
// 3. Finish transaction
await finishTransaction({purchase});
}
} catch (error) {
console.error('Purchase validation failed:', error);
}
};
What happens if I don't call finishTransaction()
?
- The purchase will remain in a pending state
- The store will continue to notify your app about this purchase
- On iOS, the user may see repeated purchase prompts
- On Android, the purchase may be refunded automatically
Error Handling
What should I do when a purchase fails?
Handle different error types appropriately:
const handlePurchaseError = (error) => {
switch (error.code) {
case 'E_USER_CANCELLED':
// User cancelled - no action needed
break;
case 'E_NETWORK_ERROR':
// Show retry option
showRetryDialog();
break;
case 'E_ITEM_UNAVAILABLE':
// Product not available
showProductUnavailableMessage();
break;
case 'E_ALREADY_OWNED':
// User already owns this
showAlreadyOwnedMessage();
break;
default:
// Log for investigation
console.error('Purchase error:', error);
break;
}
};
How do I handle network errors during purchases?
Network errors during purchases are tricky because the purchase might still go through. Always:
- Don't retry the purchase immediately
- Check for pending purchases on app restart
- Implement proper error messaging for users
Testing
Can I test in-app purchases on simulators?
No, in-app purchases only work on real devices. Use:
- iOS: Real iPhone/iPad with sandbox account
- Android: Real Android device with signed build
Why do my purchases work in testing but not in production?
Common issues:
- Different product IDs between testing and production
- App not properly signed for production
- Store review process not completed
- Different bundle ID or package name
How do I test subscription cancellations?
You can't directly test cancellations in sandbox, but you can:
- Test subscription purchase flow
- Test subscription restoration
- Use server-side webhook notifications for cancellation handling
Performance and Best Practices
When should I initialize the connection?
Initialize as early as possible in your app's lifecycle:
function App() {
const {connected} = useIAP(); // Connection starts automatically
return <YourAppContent />;
}
How do I prevent multiple purchase attempts?
Implement purchase state management:
const [isPurchasing, setIsPurchasing] = useState(false);
const handlePurchase = async (productId) => {
if (isPurchasing) return;
setIsPurchasing(true);
try {
await requestPurchase({sku: productId});
} finally {
setIsPurchasing(false);
}
};
Should I cache product information?
Yes, cache products to improve performance:
const [cachedProducts, setCachedProducts] = useState({});
const getProductsWithCache = async (skus) => {
const uncachedSkus = skus.filter((sku) => !cachedProducts[sku]);
if (uncachedSkus.length > 0) {
const products = await getProducts({skus: uncachedSkus});
// Cache the products
setCachedProducts((prev) => ({
...prev,
...products.reduce((acc, product) => {
acc[product.productId] = product;
return acc;
}, {}),
}));
}
return skus.map((sku) => cachedProducts[sku]).filter(Boolean);
};
Migration and Updates
How do I migrate from react-native-iap?
expo-iap
is the official successor to react-native-iap
. The migration is straightforward with these key changes:
Installation:
# Remove react-native-iap
npm uninstall react-native-iap
# Install expo-iap
npx expo install expo-iap
API Changes:
// react-native-iap (OLD)
import {useIAP, withIAPContext} from 'react-native-iap';
function App() {
return (
<withIAPContext>
<YourApp />
</withIAPContext>
);
}
// expo-iap (NEW)
import {useIAP} from 'expo-iap'; // No context wrapper needed
function App() {
return <YourApp />; // Hook works anywhere in your app
}
Hook Usage (mostly compatible):
// Most of the useIAP API remains the same
const {
connected,
products,
purchases,
getProducts,
requestPurchase,
finishTransaction,
// ... other methods
} = useIAP();
Key Benefits of Migration:
- Automatic connection management - no manual connection setup
- Better error handling - centralized error codes across platforms
- Expo Config Plugin - simplified native configuration
- New Architecture support - future-proof your app
- Active maintenance - continued updates and support
Migration Timeline:
react-native-iap
will be gradually deprecated over time- Critical security updates will continue for existing users
- New features and improvements will only be added to
expo-iap
For detailed migration guide and community support:
What if expo-iap doesn't support a feature I need?
- Check if it's in the roadmap
- File a feature request
- Consider contributing to the project
- For urgent needs, you might need to use react-native-iap
Troubleshooting
My app crashes when making purchases
Common causes:
- Not handling purchase updates properly
- Memory leaks from not cleaning up listeners
- Trying to finish transactions multiple times
Purchases are successful but features aren't unlocked
This usually indicates:
- Receipt validation is failing
- Purchase handling logic has bugs
- Database/state updates are not working
Check your server logs and purchase handling code.
I get "Item already owned" errors
This happens when:
- Trying to purchase a non-consumable product again
- Previous transaction wasn't finished properly
- Need to restore purchases instead
[iOS] purchaseUpdatedListener is called twice after finishTransaction
Issue: On iOS, purchaseUpdatedListener
may be called twice for the same transaction when using andDangerouslyFinishTransactionAutomatically: false
and manually calling finishTransaction()
.
Symptoms:
- First call: Immediate after successful purchase
- Second call: After
finishTransaction()
is called (or on app restart for products) - Both calls have the same
transactionId
Example:
// This pattern may cause duplicate calls
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
console.log('Purchase received:', purchase.transactionId);
await validateOnServer(purchase);
await finishTransaction({purchase, isConsumable: false});
// ⚠️ Listener may be called again after finishTransaction
});
await requestPurchase({
sku: 'your.product.id',
andDangerouslyFinishTransactionAutomatically: false,
});
Workaround: Track processed transactions to avoid duplicate processing:
const processedTransactions = new Set();
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
const transactionId = purchase.transactionId;
// Skip if already processed
if (processedTransactions.has(transactionId)) {
console.log('Transaction already processed:', transactionId);
return;
}
// Mark as processed
processedTransactions.add(transactionId);
try {
console.log('Processing purchase:', transactionId);
await validateOnServer(purchase);
await finishTransaction({purchase, isConsumable: false});
} catch (error) {
// Remove from processed set if validation fails
processedTransactions.delete(transactionId);
console.error('Purchase processing failed:', error);
}
});
Root Cause: This appears to be an Apple StoreKit behavior where finishing a transaction triggers another purchase notification. This is a known iOS platform limitation, not specific to expo-iap.
Related Issues:
Still Need Help?
If your question isn't answered here:
- Check the GitHub issues
- Review the troubleshooting guide
- Create a minimal reproduction example
- Join the community discussions