useIAP Hook
The useIAP
hook is the main interface for interacting with in-app purchases in Expo IAP. It provides a comprehensive API for managing purchases, subscriptions, and error handling.
Import
import {useIAP} from 'expo-iap';
Important: Hook Behavior
The useIAP
hook follows React Hooks conventions and differs from calling functions directly from expo-iap
(index exports):
- Automatic connection: Automatically calls
initConnection
on mount andendConnection
on unmount. - Void-returning methods: Methods like
fetchProducts
,requestPurchase
,getAvailablePurchases
, etc. returnPromise<void>
in the hook. They do not resolve to data. Instead, they update internal state exposed by the hook:products
,subscriptions
,availablePurchases
, etc. - Don't await for data: When using the hook, do not write
const x = await fetchProducts(...)
. Call the method, then read the corresponding state from the hook. - Use callbacks for purchases: Always use
onPurchaseSuccess
andonPurchaseError
callbacks passed touseIAP
for handling purchase results. ThecurrentPurchase
andcurrentPurchaseError
states are deprecated and should not be used in new code.
Basic Usage
const {
connected,
products,
subscriptions,
availablePurchases,
fetchProducts,
requestPurchase,
finishTransaction,
} = useIAP({
onPurchaseSuccess: async (purchase) => {
// IMPORTANT: Validate on your backend before finishing transaction
const isValid = await validateOnServer(purchase);
if (isValid) {
await finishTransaction({purchase, isConsumable: false});
}
},
onPurchaseError: (error) => {
console.error('Purchase failed:', error);
},
});
Configuration Options
useIAP(options)
Parameter | Type | Required | Description |
---|---|---|---|
options | UseIAPOptions | No | Configuration object |
UseIAPOptions
interface UseIAPOptions {
onPurchaseSuccess?: (purchase: Purchase) => void;
onPurchaseError?: (error: PurchaseError) => void;
shouldAutoSyncPurchases?: boolean; // Controls auto sync behavior inside the hook
onPromotedProductIOS?: (product: Product) => void; // iOS promoted products
}
Configuration Properties
onPurchaseSuccess
-
Type:
(purchase: Purchase) => void
-
Description: Called when a purchase completes successfully
-
Example:
onPurchaseSuccess: (purchase) => {
// Grant user access to purchased content
unlockFeature(purchase.productId);
};
onPurchaseError
-
Type:
(error: PurchaseError) => void
-
Description: Called when a purchase fails
-
Example:
onPurchaseError: (error) => {
if (error.code !== ErrorCode.UserCancelled) {
Alert.alert('Purchase Failed', error.message);
}
};
autoFinishTransactions
- Type:
boolean
- Default:
true
- Description: Whether to automatically finish transactions after successful purchases
Return Values
State Properties
connected
-
Type:
boolean
-
Description: Whether the IAP service is connected and ready
-
Example:
if (connected) {
// Safe to make IAP calls
fetchProducts({skus: ['product.id'], type: 'in-app'});
}
products
-
Type:
Product[]
-
Description: Array of available products
-
Example:
products.map((product) => <ProductItem key={product.id} product={product} />);
subscriptions
-
Type:
ProductSubscription[]
-
Description: Array of available subscription products
-
Example:
subscriptions.map((subscription) => (
<SubscriptionItem key={subscription.id} subscription={subscription} />
));
currentPurchase (Deprecated)
-
Type:
Purchase | null
-
Status: ⚠️ DEPRECATED - Do not use in new code
-
Description: Last purchase event captured by the hook. This state is deprecated and kept only for backward compatibility. Always use
onPurchaseSuccess
callback instead. -
Migration:
// ❌ Old way (deprecated)
const {currentPurchase} = useIAP();
useEffect(() => {
if (currentPurchase) {
handlePurchase(currentPurchase);
}
}, [currentPurchase]);
// ✅ New way (recommended)
const {} = useIAP({
onPurchaseSuccess: async (purchase) => {
await handlePurchase(purchase);
},
});
currentPurchaseError (Deprecated)
-
Type:
PurchaseError | null
-
Status: ⚠️ DEPRECATED - Do not use in new code
-
Description: Current purchase error. This state is deprecated and kept only for backward compatibility. Always use
onPurchaseError
callback instead. -
Migration:
// ❌ Old way (deprecated)
const {currentPurchaseError} = useIAP();
useEffect(() => {
if (currentPurchaseError) {
handleError(currentPurchaseError);
}
}, [currentPurchaseError]);
// ✅ New way (recommended)
const {} = useIAP({
onPurchaseError: (error) => {
handleError(error);
},
});
availablePurchases
-
Type:
Purchase[]
-
Description: Array of available purchases (restorable items)
-
Example:
availablePurchases.map((purchase) => (
<RestorableItem key={purchase.id} purchase={purchase} />
));
promotedProductIOS
-
Type:
Product | undefined
-
Description: The promoted product details (iOS only)
-
Example:
useEffect(() => {
if (promotedProductIOS) {
// Handle promoted product
handlePromotedProduct(promotedProductIOS);
}
}, [promotedProductIOS]);
Methods
fetchProducts
-
Type:
(params: { skus: string[]; type?: 'in-app' | 'subs' }) => Promise<void>
-
Description: Fetch products or subscriptions and update
products
/subscriptions
state. In the hook this returnsvoid
(no data result), by design. -
Do not await for data: Call it, then consume
products
/subscriptions
state from the hook. -
Example:
useEffect(() => {
if (!connected) return;
// In hook: returns void, updates state
fetchProducts({
skus: ['com.app.premium', 'com.app.coins_100'],
type: 'in-app',
});
fetchProducts({skus: ['com.app.premium_monthly'], type: 'subs'});
}, [connected, fetchProducts]);
// Later in render/effects
products.forEach((p) => console.log('product', p.id));
subscriptions.forEach((s) => console.log('sub', s.id));
requestPurchase
-
Type:
(request: RequestPurchaseProps) => Promise<void>
-
Description: Initiate a purchase request
-
Parameters:
request
: Purchase request configuration
-
Example:
const buyProduct = async (productId: string) => {
try {
// In hook: returns void. Listen via callbacks (onPurchaseSuccess/onPurchaseError)
await requestPurchase({
request: {
ios: {sku: productId},
android: {skus: [productId]},
},
});
} catch (error) {
console.error('Purchase request failed:', error);
}
};
Subscription Offers
When purchasing subscriptions, you need to specify the pricing plan (offer) for each platform:
Android Subscription Offers
Android requires subscriptionOffers
array containing offer tokens from fetchProducts()
. Each offer token represents a specific pricing plan (base plan, introductory offer, etc.).
const buySubscription = async (subscriptionId: string) => {
// 1) Fetch subscription products first
await fetchProducts({skus: [subscriptionId], type: 'subs'});
// 2) Find the subscription and build offers
const subscription = subscriptions.find((s) => s.id === subscriptionId);
if (!subscription) return;
const subscriptionOffers = (
subscription.subscriptionOfferDetailsAndroid ?? []
).map((offer) => ({
sku: subscriptionId,
offerToken: offer.offerToken,
}));
// 3) Request purchase with offers
await requestPurchase({
request: {
ios: {sku: subscriptionId},
android: {
skus: [subscriptionId],
// Only include subscriptionOffers when offers are available
...(subscriptionOffers.length > 0 && {subscriptionOffers}),
},
},
type: 'subs',
});
};
Note: subscriptionOffers
should only be included when subscription offers are available from fetchProducts()
. Without offers, Android purchases will fail.
iOS Subscription Offers
iOS uses withOffer
for promotional discounts configured in App Store Connect. This is optional and only needed for special promotional pricing.
const buySubscriptionWithOffer = async (
subscriptionId: string,
discountOffer?: DiscountOfferInputIOS,
) => {
await requestPurchase({
request: {
ios: {
sku: subscriptionId,
// Optional: apply promotional offer
...(discountOffer && {withOffer: discountOffer}),
},
android: {skus: [subscriptionId]},
},
type: 'subs',
});
};
Subscription helpers (hook)
-
getActiveSubscriptions(subscriptionIds?) => Promise<ActiveSubscription[]>
-
Returns active subscription info and also updates
activeSubscriptions
state. -
Exception to the hook’s void-return design: this method returns data for convenience.
-
Example:
const {getActiveSubscriptions, activeSubscriptions} = useIAP();
useEffect(() => {
if (!connected) return;
(async () => {
const subs = await getActiveSubscriptions(['premium_monthly']);
console.log('Subs from return:', subs.length);
console.log('Subs from state:', activeSubscriptions.length);
})();
}, [connected]);
-
-
hasActiveSubscriptions(subscriptionIds?) => Promise<boolean>
- Boolean convenience check to see if any active subscriptions exist (optionally filtered by IDs).
Removed in v2.9.0:
purchaseHistories
state andgetPurchaseHistories()
method. UsegetAvailablePurchases()
andavailablePurchases
instead.
getAvailablePurchases
-
Type:
() => Promise<void>
-
Description: Fetch available purchases (restorable items) from the store
-
Example:
const restorePurchases = async () => {
try {
// Updates `availablePurchases` state; do not expect a return value
await getAvailablePurchases();
// Read from state afterwards
console.log('Available purchases count:', availablePurchases.length);
} catch (error) {
console.error('Failed to fetch available purchases:', error);
}
};
validateReceipt
- Type:
(productId: string, params?: ValidationParams) => Promise<ValidationResult>
- Description: Validate a purchase receipt
- Parameters:
productId
: ID of the product to validateparams
: Required for Android, optional for iOS:packageName
(string, Android): Package name of your appproductToken
(string, Android): Purchase token from the purchaseaccessToken
(string, Android): Optional access token for server validationisSub
(boolean, Android): Whether this is a subscription
- Returns: Promise resolving to validation result
Important Platform Differences:
-
iOS: Only requires the product ID
-
Android: Requires additional parameters (packageName, productToken)
-
Example:
const validatePurchase = async (productId: string, purchase: any) => {
try {
if (Platform.OS === 'ios') {
// iOS: Simple validation with just product ID
const result = await validateReceipt(productId);
return result;
} else if (Platform.OS === 'android') {
// Android: Requires additional parameters
const purchaseToken = purchase.purchaseToken;
const packageName = purchase.packageNameAndroid;
if (!purchaseToken || !packageName) {
throw new Error(
'Android validation requires packageName and productToken',
);
}
const result = await validateReceipt(productId, {
packageName,
productToken: purchaseToken,
isSub: false, // Set to true for subscriptions
});
return result;
}
} catch (error) {
console.error('Validation failed:', error);
throw error;
}
};
getPromotedProductIOS
-
Type:
() => Promise<any | null>
-
Description: Get the promoted product details (iOS only)
-
Example:
const handlePromotedProduct = async () => {
const promotedProduct = await getPromotedProductIOS();
if (promotedProduct) {
console.log('Promoted product:', promotedProduct);
// Show custom purchase UI
}
};
requestPurchaseOnPromotedProductIOS
-
Type:
() => Promise<void>
-
Description: Complete the purchase of a promoted product (iOS only)
Removed in v2.9.0:
buyPromotedProductIOS
. UserequestPurchaseOnPromotedProductIOS
instead. -
Example:
const completePurchase = async () => {
try {
await requestPurchaseOnPromotedProductIOS();
console.log('Promoted product purchase completed');
} catch (error) {
console.error('Failed to purchase promoted product:', error);
}
};
Platform-Specific Usage
iOS Example
const IOSPurchaseExample = () => {
const {connected, products, requestPurchase, validateReceipt} = useIAP({
onPurchaseSuccess: async (purchase) => {
// Validate receipt on iOS
const validation = await validateReceipt(purchase.productId);
if (validation.isValid) {
unlockContent(purchase.productId);
}
},
});
const buyProduct = (product: Product) => {
requestPurchase({
request: {
ios: {sku: product.id},
android: {skus: [product.id]},
},
});
};
return (
<View>
{products
.filter((p) => p.platform === 'ios')
.map((product) => (
<Button
key={product.id}
title={`${product.title} - ${product.displayPrice}`}
onPress={() => buyProduct(product)}
/>
))}
</View>
);
};
Android Example
const AndroidPurchaseExample = () => {
const {connected, products, requestPurchase} = useIAP({
onPurchaseSuccess: (purchase) => {
// Android purchases are automatically validated by Google Play
unlockContent(purchase.productId);
},
});
const buyProduct = (product: Product) => {
requestPurchase({
request: {
ios: {sku: product.id},
android: {skus: [product.id]},
},
});
};
return (
<View>
{products
.filter((p) => p.platform === 'android')
.map((product) => (
<Button
key={product.id}
title={`${product.title} - ${product.displayPrice}`}
onPress={() => buyProduct(product)}
/>
))}
</View>
);
};
Error Handling
The useIAP
hook integrates with the centralized error handling system:
const {requestPurchase} = useIAP({
onPurchaseError: (error) => {
// Error is automatically typed as PurchaseError
switch (error.code) {
case ErrorCode.UserCancelled:
// Don't show error for user cancellation
break;
case ErrorCode.NetworkError:
Alert.alert('Network Error', 'Please check your connection');
break;
case ErrorCode.ItemUnavailable:
Alert.alert(
'Item Unavailable',
'This item is not available for purchase',
);
break;
default:
Alert.alert('Purchase Failed', error.message);
}
},
});
Best Practices
-
Always check
connected
before making IAP calls:useEffect(() => {
if (connected) {
fetchProducts({skus: productIds, type: 'in-app'});
}
}, [connected, fetchProducts]); -
Handle loading states:
const [loading, setLoading] = useState(false);
const buyProduct = async (productId: string) => {
setLoading(true);
try {
await requestPurchase({
request: {
ios: {sku: productId},
android: {skus: [productId]},
},
});
} finally {
setLoading(false);
}
}; -
Implement proper error handling:
const handleError = (error: PurchaseError) => {
// Log for debugging
console.error('IAP Error:', error);
// Show user-friendly message
if (error.code !== ErrorCode.UserCancelled) {
Alert.alert('Purchase Failed', error.message);
}
};
Promoted Products (iOS Only)
Handle App Store promoted products when users tap on them in the App Store:
const PromotedProductExample = () => {
const {promotedProductIOS, requestPurchaseOnPromotedProductIOS} = useIAP({
onPromotedProductIOS: (product) => {
console.log('Promoted product detected:', product);
},
onPurchaseSuccess: (purchase) => {
// Recommended: handle success via callback
},
onPurchaseError: (error) => {
// Recommended: handle errors via callback
},
});
useEffect(() => {
if (promotedProductIOS) {
handlePromotedProduct();
}
}, [promotedProductIOS]);
const handlePromotedProduct = async () => {
try {
// Show your custom purchase UI
const confirmed = await showPurchaseConfirmation(promotedProductIOS);
if (confirmed) {
// Complete the promoted purchase
await requestPurchaseOnPromotedProductIOS();
}
} catch (error) {
console.error('Error handling promoted product:', error);
}
};
const showPurchaseConfirmation = async (product: any) => {
return new Promise((resolve) => {
Alert.alert(
'Purchase Product',
`Would you like to purchase ${product.localizedTitle} for ${product.price}?`,
[
{text: 'Cancel', onPress: () => resolve(false), style: 'cancel'},
{text: 'Buy', onPress: () => resolve(true)},
],
);
});
};
return <View>{/* Your regular store UI */}</View>;
};