Purchase Listeners
expo-iap provides event listeners to handle purchase updates and errors. These listeners are essential for handling the asynchronous nature of in-app purchases.
purchaseUpdatedListener()
Listens for purchase updates from the store.
import {purchaseUpdatedListener} from 'expo-iap';
const setupPurchaseListener = () => {
const subscription = purchaseUpdatedListener((purchase) => {
console.log('Purchase received:', purchase);
handlePurchaseUpdate(purchase);
});
// Clean up listener when component unmounts
return () => {
if (subscription) {
subscription.remove();
}
};
};
const handlePurchaseUpdate = async (purchase) => {
try {
// Validate receipt on your server
const isValid = await validateReceiptOnServer(purchase);
if (isValid) {
// Grant purchase to user
await grantPurchaseToUser(purchase);
// Finish the transaction
await finishTransaction({purchase});
console.log('Purchase completed successfully');
} else {
console.error('Receipt validation failed');
}
} catch (error) {
console.error('Error handling purchase:', error);
}
};
Parameters:
callback
(function): Function to call when a purchase update is receivedpurchase
(Purchase): The purchase object
Returns: Subscription object with remove()
method
purchaseErrorListener()
Listens for purchase errors from the store.
import {purchaseErrorListener} from 'expo-iap';
const setupErrorListener = () => {
const subscription = purchaseErrorListener((error) => {
console.error('Purchase error:', error);
handlePurchaseError(error);
});
// Clean up listener when component unmounts
return () => {
if (subscription) {
subscription.remove();
}
};
};
const handlePurchaseError = (error) => {
switch (error.code) {
case 'E_USER_CANCELLED':
// User cancelled the purchase
console.log('Purchase cancelled by user');
break;
case 'E_NETWORK_ERROR':
// Network error occurred
showErrorMessage(
'Network error. Please check your connection and try again.',
);
break;
case 'E_ITEM_UNAVAILABLE':
// Product is not available
showErrorMessage('This product is currently unavailable.');
break;
case 'E_ALREADY_OWNED':
// User already owns this product
showErrorMessage('You already own this product.');
break;
default:
// Other errors
showErrorMessage(`Purchase failed: ${error.message}`);
break;
}
};
Parameters:
callback
(function): Function to call when a purchase error occurserror
(IAPError): The error object
Returns: Subscription object with remove()
method
promotedProductListener() (iOS only)
Listens for promoted product purchases initiated from the App Store.
import {promotedProductListener} from 'expo-iap';
const setupPromotedProductListener = () => {
const subscription = promotedProductListener((productId) => {
console.log('Promoted product purchase initiated:', productId);
handlePromotedProduct(productId);
});
return () => {
if (subscription) {
subscription.remove();
}
};
};
const handlePromotedProduct = async (productId) => {
try {
// Fetch the product details
const products = await getProducts({skus: [productId]});
const product = products[0];
if (product) {
// Show product details to user and confirm purchase
const confirmed = await showProductConfirmation(product);
if (confirmed) {
await requestPurchase({sku: productId});
}
}
} catch (error) {
console.error('Error handling promoted product:', error);
}
};
Parameters:
callback
(function): Function to call when a promoted product is selectedproductId
(string): The ID of the promoted product
Returns: Subscription object with remove()
method
Using Listeners with React Hooks
Functional Components
import React, {useEffect} from 'react';
import {purchaseUpdatedListener, purchaseErrorListener} from 'expo-iap';
export default function PurchaseManager() {
useEffect(() => {
// Set up purchase listeners
const purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => {
handlePurchaseUpdate(purchase);
});
const purchaseErrorSubscription = purchaseErrorListener((error) => {
handlePurchaseError(error);
});
// Cleanup function
return () => {
purchaseUpdateSubscription?.remove();
purchaseErrorSubscription?.remove();
};
}, []);
const handlePurchaseUpdate = async (purchase) => {
// Handle purchase logic
};
const handlePurchaseError = (error) => {
// Handle error logic
};
return <div>{/* Your component JSX */}</div>;
}
Class Components
import React, {Component} from 'react';
import {purchaseUpdatedListener, purchaseErrorListener} from 'expo-iap';
class PurchaseManager extends Component {
purchaseUpdateSubscription = null;
purchaseErrorSubscription = null;
componentDidMount() {
// Set up listeners
this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => {
this.handlePurchaseUpdate(purchase);
});
this.purchaseErrorSubscription = purchaseErrorListener((error) => {
this.handlePurchaseError(error);
});
}
componentWillUnmount() {
// Clean up listeners
if (this.purchaseUpdateSubscription) {
this.purchaseUpdateSubscription.remove();
}
if (this.purchaseErrorSubscription) {
this.purchaseErrorSubscription.remove();
}
}
handlePurchaseUpdate = async (purchase) => {
// Handle purchase logic
};
handlePurchaseError = (error) => {
// Handle error logic
};
render() {
return <div>{/* Your component JSX */}</div>;
}
}
Custom Hook for Purchase Handling
You can create a custom hook to encapsulate purchase listener logic:
import {useEffect, useCallback} from 'react';
import {
purchaseUpdatedListener,
purchaseErrorListener,
finishTransaction,
} from 'expo-iap';
export const usePurchaseHandler = () => {
const handlePurchaseUpdate = useCallback(async (purchase) => {
try {
// Validate receipt
const isValid = await validateReceiptOnServer(purchase);
if (isValid) {
// Grant purchase
await grantPurchaseToUser(purchase);
// Finish transaction
await finishTransaction({purchase});
// Show success message
showSuccessMessage('Purchase completed successfully!');
} else {
console.error('Receipt validation failed');
showErrorMessage('Purchase validation failed. Please contact support.');
}
} catch (error) {
console.error('Error handling purchase:', error);
showErrorMessage('An error occurred while processing your purchase.');
}
}, []);
const handlePurchaseError = useCallback((error) => {
console.error('Purchase error:', error);
switch (error.code) {
case 'E_USER_CANCELLED':
// Don't show error for user cancellation
break;
default:
showErrorMessage(`Purchase failed: ${error.message}`);
break;
}
}, []);
useEffect(() => {
// Set up listeners
const purchaseUpdateSubscription =
purchaseUpdatedListener(handlePurchaseUpdate);
const purchaseErrorSubscription =
purchaseErrorListener(handlePurchaseError);
// Cleanup
return () => {
purchaseUpdateSubscription?.remove();
purchaseErrorSubscription?.remove();
};
}, [handlePurchaseUpdate, handlePurchaseError]);
};
// Usage in component
export default function MyStoreComponent() {
usePurchaseHandler(); // Sets up listeners automatically
return <div>{/* Your store UI */}</div>;
}
Important Notes
Listener Lifecycle
- Set up early: Set up listeners as early as possible in your app lifecycle
- Clean up properly: Always remove listeners to prevent memory leaks
- Handle app states: Purchases can complete when your app is in the background
Error Handling
Always handle both purchase updates and errors:
useEffect(() => {
const purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => {
// Handle successful/pending purchases
});
const purchaseErrorSubscription = purchaseErrorListener((error) => {
// Handle purchase errors
});
return () => {
purchaseUpdateSubscription?.remove();
purchaseErrorSubscription?.remove();
};
}, []);
Purchase States
Purchases can be in different states:
- Purchased: Successfully completed
- Pending: Awaiting approval (e.g., parental approval)
- Failed: Purchase failed
Handle each state appropriately in your purchase listener.
Alternative: useIAP Hook
For simpler usage, consider using the useIAP
hook which automatically manages listeners:
import {useIAP} from 'expo-iap';
export default function StoreComponent() {
const {currentPurchase, currentPurchaseError} = useIAP();
useEffect(() => {
if (currentPurchase) {
handlePurchaseUpdate(currentPurchase);
}
}, [currentPurchase]);
useEffect(() => {
if (currentPurchaseError) {
handlePurchaseError(currentPurchaseError);
}
}, [currentPurchaseError]);
// Rest of component
}
The useIAP
hook provides a more React-friendly way to handle purchases without manually managing listeners.