Alternative Billing
What you'll build
Use alternative billing to redirect users to external payment systems or offer payment choices alongside platform billing.
View the full example source:
- GitHub: alternative-billing.tsx
iOS - External Purchase URL
Redirect users to an external website for payment (iOS 16.0+):
import {Platform, Button, Alert} from 'react-native';
import {presentExternalPurchaseLinkIOS, type Product} from 'expo-iap';
function IOSAlternativeBilling({product}: {product: Product}) {
const handlePurchase = async () => {
if (Platform.OS !== 'ios') return;
try {
const result = await presentExternalPurchaseLinkIOS(
`https://your-payment-site.com/purchase/${product.id}`,
);
if (result.success) {
Alert.alert(
'Redirected',
'Complete purchase on the external website. You will be redirected back to the app.',
);
} else if (result.error) {
Alert.alert('Error', result.error);
}
} catch (error: any) {
if (error.code !== 'user-cancelled') {
Alert.alert('Error', error.message);
}
}
};
return <Button title="Buy (External URL)" onPress={handlePurchase} />;
}
Important Notes
- iOS 16.0+ Required: External URLs only work on iOS 16.0 and later
- Configuration Required: External URLs must be configured in
app.config.ts(see Alternative Billing Guide) - No Callback:
onPurchaseUpdatedwill NOT fire when using external URLs - Deep Linking: Implement deep linking to return users to your app
Android - Billing Programs API (8.2.0+)
The new Billing Programs API provides a unified way to handle external billing:
import {Platform, Button, Alert} from 'react-native';
import {
isBillingProgramAvailableAndroid,
launchExternalLinkAndroid,
createBillingProgramReportingDetailsAndroid,
type Product,
} from 'expo-iap';
function AndroidBillingPrograms({product}: {product: Product}) {
const handlePurchase = async () => {
if (Platform.OS !== 'android') return;
try {
// Step 1: Check availability
const availability = await isBillingProgramAvailableAndroid('external-offer');
if (!availability.isAvailable) {
Alert.alert('Error', 'External offer program not available');
return;
}
// Step 2: Launch external link
await launchExternalLinkAndroid({
billingProgram: 'external-offer',
launchMode: 'launch-in-external-browser-or-app',
linkType: 'link-to-digital-content-offer',
linkUri: `https://your-payment-site.com/purchase/${product.id}`,
});
// Step 3: After payment completes externally, get reporting token
const details = await createBillingProgramReportingDetailsAndroid('external-offer');
console.log('Token:', details.externalTransactionToken);
// Step 4: Report token to Google Play backend within 24 hours
// await reportToGoogleBackend(details.externalTransactionToken);
Alert.alert('Success', 'Billing program flow completed');
} catch (error: any) {
Alert.alert('Error', error.message);
}
};
return <Button title="Buy (Billing Programs)" onPress={handlePurchase} />;
}
Billing Program Types
external-offer- External offer programsexternal-content-link- External content link programs
Launch Modes
launch-in-external-browser-or-app- Opens in external browsercaller-will-launch-link- App handles the launch
Link Types
link-to-digital-content-offer- Digital content offerslink-to-app-download- App download links
Android - Alternative Billing (Legacy)
Deprecated
The legacy alternative billing API is deprecated. Use the Billing Programs API instead.
Manual 3-step flow for alternative billing only:
import {Platform, Button, Alert} from 'react-native';
import {
checkAlternativeBillingAvailabilityAndroid,
showAlternativeBillingDialogAndroid,
createAlternativeBillingTokenAndroid,
type Product,
} from 'expo-iap';
function AndroidAlternativeBillingOnly({product}: {product: Product}) {
const handlePurchase = async () => {
if (Platform.OS !== 'android') return;
try {
// Step 1: Check availability
const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
if (!isAvailable) {
Alert.alert('Error', 'Alternative billing not available');
return;
}
// Step 2: Show information dialog
const userAccepted = await showAlternativeBillingDialogAndroid();
if (!userAccepted) {
console.log('User declined');
return;
}
// Step 2.5: Process payment with your payment system
// ... your payment processing logic here ...
console.log('Processing payment...');
// Step 3: Create reporting token (after successful payment)
const token = await createAlternativeBillingTokenAndroid(product.id);
console.log('Token created:', token);
// Step 4: Report token to Google Play backend within 24 hours
// await reportToGoogleBackend(token);
Alert.alert('Success', 'Alternative billing completed (DEMO)');
} catch (error: any) {
Alert.alert('Error', error.message);
}
};
return <Button title="Buy (Alternative Only)" onPress={handlePurchase} />;
}
Flow Steps
- Check availability - Verify alternative billing is enabled
- Show info dialog - Display Google's information dialog
- Process payment - Handle payment with your system
- Create token - Generate reporting token
- Report to Google - Send token to Google within 24 hours
Android - User Choice Billing
Let users choose between Google Play and alternative billing:
import {Platform, Button} from 'react-native';
import {useIAP, requestPurchase, type Product} from 'expo-iap';
function AndroidUserChoiceBilling({product}: {product: Product}) {
// Initialize with user choice billing program
const {connected} = useIAP({
enableBillingProgramAndroid: 'user-choice-billing',
onPurchaseSuccess: (purchase) => {
// Fires if user selects Google Play
console.log('Google Play purchase:', purchase);
},
});
const handlePurchase = async () => {
if (Platform.OS !== 'android' || !connected) return;
try {
// Google will show selection dialog automatically
await requestPurchase({
request: {
google: {
skus: [product.id],
},
},
type: 'in-app',
});
// If user selects Google Play: onPurchaseSuccess fires
// If user selects alternative: manual flow required
} catch (error: any) {
console.error('Purchase error:', error);
}
};
return <Button title="Buy (User Choice)" onPress={handlePurchase} />;
}
Selection Dialog
- Google shows automatic selection dialog
- User chooses: Google Play (30% fee) or Alternative (lower fee)
- Different callbacks based on user choice
Complete Cross-Platform Example
import {useState, useCallback} from 'react';
import {Platform, View, Button, Alert} from 'react-native';
import {
useIAP,
requestPurchase,
presentExternalPurchaseLinkIOS,
isBillingProgramAvailableAndroid,
launchExternalLinkAndroid,
createBillingProgramReportingDetailsAndroid,
type Product,
type BillingProgramAndroid,
} from 'expo-iap';
type BillingMode = 'billing-programs' | 'user-choice-billing';
function AlternativeBillingScreen() {
const [billingMode, setBillingMode] = useState<BillingMode>('billing-programs');
const {connected, products} = useIAP({
enableBillingProgramAndroid:
Platform.OS === 'android' && billingMode === 'user-choice-billing'
? 'user-choice-billing'
: 'external-offer',
onPurchaseSuccess: (purchase) => {
console.log('Purchase successful:', purchase);
},
onPurchaseError: (error) => {
console.error('Purchase error:', error);
},
});
const handleIOSPurchase = useCallback(async (product: Product) => {
const result = await presentExternalPurchaseLinkIOS(
`https://your-payment-site.com/purchase/${product.id}`,
);
if (result.success) {
Alert.alert('Redirected', 'Complete purchase on external website');
} else if (result.error) {
Alert.alert('Error', result.error);
}
}, []);
const handleAndroidBillingPrograms = useCallback(async (product: Product) => {
const availability = await isBillingProgramAvailableAndroid('external-offer');
if (!availability.isAvailable) {
Alert.alert('Error', 'Billing program not available');
return;
}
await launchExternalLinkAndroid({
billingProgram: 'external-offer',
launchMode: 'launch-in-external-browser-or-app',
linkType: 'link-to-digital-content-offer',
linkUri: `https://your-payment-site.com/purchase/${product.id}`,
});
const details = await createBillingProgramReportingDetailsAndroid('external-offer');
Alert.alert('Success', `Token: ${details.externalTransactionToken.substring(0, 20)}...`);
}, []);
const handleAndroidUserChoice = useCallback(async (product: Product) => {
await requestPurchase({
request: {
google: {
skus: [product.id],
},
},
type: 'in-app',
});
}, []);
const handlePurchase = (product: Product) => {
if (Platform.OS === 'ios') {
handleIOSPurchase(product);
} else if (Platform.OS === 'android') {
if (billingMode === 'billing-programs') {
handleAndroidBillingPrograms(product);
} else {
handleAndroidUserChoice(product);
}
}
};
const cycleBillingMode = () => {
const modes: BillingMode[] = ['billing-programs', 'user-choice-billing'];
const currentIndex = modes.indexOf(billingMode);
setBillingMode(modes[(currentIndex + 1) % modes.length]);
};
return (
<View>
{/* Android: Mode selector */}
{Platform.OS === 'android' ? (
<Button title={`Mode: ${billingMode}`} onPress={cycleBillingMode} />
) : null}
{/* Products list */}
{products.map((product) => (
<Button
key={product.id}
title={`Buy ${product.title}`}
onPress={() => handlePurchase(product)}
/>
))}
</View>
);
}
Configuration
useIAP Hook
const {connected} = useIAP({
// Available programs: 'user-choice-billing', 'external-offer', 'external-payments', 'external-content-link'
enableBillingProgramAndroid: 'user-choice-billing',
});
Root API
import {initConnection} from 'expo-iap';
await initConnection({
enableBillingProgramAndroid: 'external-offer',
});
Testing
iOS
- Test on iOS 16.0+ devices
- Verify external URL opens in Safari
- Test deep link return flow
Android
- Configure billing programs in Google Play Console
- Test Billing Programs API (8.2.0+) first
- Fall back to legacy API if needed
- Verify token generation and reporting
Best Practices
- Use Billing Programs API - Prefer the new 8.2.0+ API over legacy methods
- Backend Validation - Always validate on server
- Clear UI - Show users they're leaving the app
- Error Handling - Handle all error cases
- Token Reporting - Report within 24 hours (Android)
- Deep Linking - Essential for iOS return flow
