3.4.7 - ExternalPurchaseCustomLink API (iOS 18.1+)
Expo IAP 3.4.7 adds support for Apple's ExternalPurchaseCustomLink API (iOS 18.1+) for apps using custom external purchase links.
New version releases and updates
View All TagsExpo IAP 3.4.7 adds support for Apple's ExternalPurchaseCustomLink API (iOS 18.1+) for apps using custom external purchase links.
This release simplifies field naming in Android input types (RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps). Since these types are already Android-specific, their fields no longer need the Android suffix.
Fields inside platform-specific input types no longer require the platform suffix. The parent type name already indicates the platform context.
Why this change?
When you write google: { offerToken: "..." }, the google key already tells you this is Android-specific. Adding Android suffix to fields inside is redundant:
// Redundant - we know it's Android from the parent
google: { offerTokenAndroid: "..." }
// Cleaner - parent context is sufficient
google: { offerToken: "..." }
| Old Name (v3.4.3) | New Name (v3.4.4) |
|---|---|
offerTokenAndroid | offerToken |
isOfferPersonalizedAndroid | isOfferPersonalized |
obfuscatedAccountIdAndroid | obfuscatedAccountId |
obfuscatedProfileIdAndroid | obfuscatedProfileId |
purchaseTokenAndroid | purchaseToken |
replacementModeAndroid | replacementMode |
developerBillingOptionAndroid | developerBillingOption |
Before (v3.4.3):
await requestPurchase({
request: {
google: {
skus: ['product_id'],
offerTokenAndroid: discountOffer.offerTokenAndroid,
isOfferPersonalizedAndroid: true,
obfuscatedAccountIdAndroid: 'user_123',
},
},
type: 'in-app',
});
After (v3.4.4):
await requestPurchase({
request: {
google: {
skus: ['product_id'],
offerToken: discountOffer.offerTokenAndroid, // Note: response field keeps suffix
isOfferPersonalized: true,
obfuscatedAccountId: 'user_123',
},
},
type: 'in-app',
});
The suffix removal only applies to input types (request parameters). Response types still use suffixes because they're cross-platform:
// Response fields KEEP the Android suffix
const product = products[0] as ProductAndroid;
const discountOffer = product.discountOffers?.[0];
// These response fields still have Android suffix
console.log(discountOffer?.offerTokenAndroid); // ✓ Keep suffix
console.log(discountOffer?.percentageDiscountAndroid); // ✓ Keep suffix
// But input fields don't need it anymore
await requestPurchase({
request: {
google: {
skus: [product.id],
offerToken: discountOffer?.offerTokenAndroid, // Input: no suffix
},
},
type: 'in-app',
});
This release also adds support for discount offers on one-time (in-app) purchases:
import {fetchProducts, requestPurchase} from 'expo-iap';
import type {ProductAndroid} from 'expo-iap';
// 1. Fetch products with discount offers
const products = await fetchProducts({
skus: ['premium_upgrade'],
type: 'in-app',
});
const product = products[0] as ProductAndroid;
// 2. Get the discount offer
const discountOffer = product.discountOffers?.[0];
// 3. Purchase with the discount
if (discountOffer?.offerTokenAndroid) {
await requestPurchase({
request: {
google: {
skus: [product.id],
offerToken: discountOffer.offerTokenAndroid, // Use simplified input field
},
},
type: 'in-app',
});
}
| Field Location | Suffix Required? | Example |
|---|---|---|
Inside RequestPurchaseAndroidProps | NO | offerToken |
Inside RequestSubscriptionAndroidProps | NO | purchaseToken |
| Cross-platform response type | YES | DiscountOffer.offerTokenAndroid |
| Package | Version |
|---|---|
| openiap-gql | 1.3.15 |
| openiap-google | 1.3.26 |
| openiap-apple | 1.3.13 |
For detailed changes, see the OpenIAP Release Notes.
This release syncs with OpenIAP v1.3.14, introducing iOS 18+ win-back offers, JWS promotional offers, Android 8.0+ product status codes, and important type cleanup.
Win-back offers allow you to re-engage churned subscribers with special discounts or free trials.
import {requestPurchase} from 'expo-iap';
// Apply a win-back offer during subscription purchase
await requestPurchase({
request: {
apple: {
sku: 'premium_monthly',
winBackOffer: {
offerId: 'winback_50_off', // iOS 18+
},
},
},
type: 'subs',
});
Win-back offers are automatically presented via StoreKit Message when eligible, or can be applied programmatically.
A new simplified format for promotional offers using compact JWS strings, back-deployed to iOS 15.
await requestPurchase({
request: {
apple: {
sku: 'premium_yearly',
promotionalOfferJWS: {
offerId: 'promo_20_off',
jws: 'eyJhbGciOiJFUzI1NiI...', // Server-signed JWS
},
},
},
type: 'subs',
});
Override system-determined introductory offer eligibility.
await requestPurchase({
request: {
apple: {
sku: 'premium_monthly',
introductoryOfferEligibility: true, // Force eligible
},
},
type: 'subs',
});
Get detailed feedback on why products couldn't be fetched.
import {fetchProducts} from 'expo-iap';
import type {ProductAndroid} from 'expo-iap';
const result = await fetchProducts({
skus: ['product_1', 'product_2'],
type: 'in-app',
});
result.forEach((product) => {
const androidProduct = product as ProductAndroid;
if (androidProduct.productStatusAndroid) {
switch (androidProduct.productStatusAndroid) {
case 'ok':
// Product available
break;
case 'not-found':
// SKU doesn't exist in Play Console
break;
case 'no-offers-available':
// User not eligible for any offers
break;
}
}
});
Include suspended subscriptions when fetching available purchases. This feature required native code updates to pass the option through to the OpenIAP SDK.
import {getAvailablePurchases} from 'expo-iap';
const purchases = await getAvailablePurchases({
includeSuspendedAndroid: true, // Include suspended subs
});
// Check if subscription is suspended
purchases.forEach((purchase) => {
if (purchase.isSuspendedAndroid) {
// Direct user to resolve payment issues
// Do NOT grant entitlements for suspended subscriptions
}
});
Important: Suspended subscriptions should NOT be granted entitlements. Users should be directed to the Play Store subscription center to resolve payment issues.
More granular error information for purchase failures.
// SubResponseCodeAndroid provides additional context:
// - 'no-applicable-sub-response-code'
// - 'payment-declined-due-to-insufficient-funds'
// - 'user-ineligible'
The following fields have been removed from RequestPurchaseIosProps because they only apply to subscription purchases:
winBackOffer - Win-back offers are subscription-only (iOS 18+)promotionalOfferJWS - JWS promotional offers are subscription-onlyintroductoryOfferEligibility - Introductory eligibility is subscription-onlyThese fields remain available in RequestSubscriptionIosProps where they belong.
Migration: If you were incorrectly using these fields with one-time purchases, move them to subscription purchases with type: 'subs'.
| Type | Platform | Description |
|---|---|---|
WinBackOfferInputIOS | iOS 18+ | Win-back offer configuration |
PromotionalOfferJwsInputIOS | iOS 15+ | JWS promotional offer input |
ProductStatusAndroid | Android 8.0+ | Product fetch status codes |
SubResponseCodeAndroid | Android 8.0+ | Granular purchase error codes |
BillingResultAndroid | Android 8.0+ | Extended billing result with sub-response |
SubscriptionOfferTypeIOS now includes 'win-back' typeRequestSubscriptionIosProps now supports:
winBackOfferpromotionalOfferJWSintroductoryOfferEligibilitywithOffer (promotional offer)PurchaseOptions now supports includeSuspendedAndroidProductAndroid and ProductSubscriptionAndroid now include productStatusAndroid| Package | Version |
|---|---|
| openiap-gql | 1.3.14 |
| openiap-google | 1.3.25 |
| openiap-apple | 1.3.13 |
For detailed changes, see the OpenIAP Release Notes.
This release reflects OpenIAP v1.3.11 updates, simplifying the PurchaseState enum and consolidating the Android billing API.
failed, restored, deferred (now only pending, purchased, unknown)BillingProgramAndroid| Before (Deprecated) | After (Recommended) |
|---|---|
alternativeBillingModeAndroid: 'user-choice' | enableBillingProgramAndroid: 'user-choice-billing' |
alternativeBillingModeAndroid: 'alternative-only' | enableBillingProgramAndroid: 'external-offer' |
// Before
const {connected} = useIAP({
alternativeBillingModeAndroid: 'user-choice',
});
// After
const {connected} = useIAP({
enableBillingProgramAndroid: 'user-choice-billing',
});
| Package | Version |
|---|---|
| openiap-gql | 1.3.11 |
| openiap-google | 1.3.21 |
| openiap-apple | 1.3.9 |
For detailed changes, see the OpenIAP Release Notes.
Expo IAP 3.3.5 brings Google Play Billing Library 8.3.0 support with the new External Payments program for Japan.
Expo IAP 3.3.0 brings Google Play Billing Library 8.2.0 features including the new Billing Programs API for external billing and one-time product discount support from Billing Library 7.0+.

Expo IAP 3.2.0 brings built-in purchase verification powered by IAPKit. Now you can verify purchases with enterprise-grade backend validation using a single API call—no server setup required.

Expo IAP 3.1.22 introduces Horizon OS support for Meta Quest devices, enabling developers to implement in-app purchases in VR applications using the same familiar API.
This release integrates Meta's Platform SDK for in-app purchases on Horizon OS, while maintaining the unified OpenIAP interface across iOS, Android, and now Horizon OS.
Expo IAP 3.1.9 introduces Alternative Billing support for both iOS and Android platforms, enabling developers to offer external payment options in compliance with App Store and Google Play requirements.
This release integrates StoreKit External Purchase APIs (iOS 16.0+) and Google Play Alternative Billing APIs, providing a unified interface for alternative payment flows across platforms.
Expo IAP 3.1.0 graduates the project into the full OpenIAP ecosystem. The release ships with three dedicated native stacks:
From 3.1.0 onward, Expo IAP stays in lockstep with these modules: Apple v1.2.2, Google v1.2.6, and GQL v1.0.8. That shared version alignment gives Expo IAP stable native compatibility and a unified type system straight from the OpenIAP schema.