A unified cross-platform in-app purchase library for React Native and Web with RevenueCat-level features.
npm install a0-purchases
# or
yarn add a0-purchases
# or
pnpm add a0-purchases
- 🌐 Cross-platform: Works on iOS, Android, and Web
- 🔄 Automatic user management: Anonymous users with seamless aliasing
- 🎯 Simple API: One unified interface across all platforms
- 🔌 Platform adapters: Native IAP for mobile, Stripe for web
- 🏃 Zero configuration: Works out of the box
- 🐛 Debug mode: Comprehensive logging for development
import { Purchases } from 'a0-purchases';
// Initialize the SDK
await Purchases.initialize({
debug: true // Enable debug logs in development
});
// Check if user has premium access
if (Purchases.isPremium()) {
// Unlock premium features
}
// Get available packages
const offerings = Purchases.getOfferings();
const package = offerings.current?.availablePackages[0];
// Make a purchase
try {
const result = await Purchases.purchase(package.identifier);
console.log('Purchase successful!', result.customerInfo);
} catch (error) {
console.error('Purchase failed', error);
}
import { A0PurchaseProvider, useA0Purchases } from 'a0-purchases';
// Wrap your app with the provider
function App() {
return (
<A0PurchaseProvider config={{ debug: true }}>
<YourApp />
</A0PurchaseProvider>
);
}
// Use the hook in your components
function PremiumButton() {
const {
isPremium,
isAnonymous,
purchase,
logIn,
isLoading
} = useA0Purchases();
if (isPremium) {
return <Text>You have premium!</Text>;
}
return (
<View>
<Button
onPress={() => purchase('premium_monthly')}
disabled={isLoading}
>
Upgrade to Premium
</Button>
{isAnonymous && (
<Text onPress={() => logIn('user_123')}>
Sign in to save your purchase
</Text>
)}
</View>
);
}
The SDK provides flexible user ID management with automatic aliasing support:
// Initialize without a user ID - creates anonymous user
await Purchases.initialize();
console.log(Purchases.getUserId()); // "$AnonymousUser:abc123..."
console.log(Purchases.isAnonymous()); // true
// Initialize with your own user ID
await Purchases.initialize({
appUserId: 'user_12345'
});
console.log(Purchases.getUserId()); // "user_12345"
console.log(Purchases.isAnonymous()); // false
// Start with anonymous user
await Purchases.initialize();
// User makes purchases while anonymous
await Purchases.purchase('premium_monthly');
// Later, after user signs in
await Purchases.logIn('user_12345');
// The anonymous user is now aliased with the identified user
// All purchases are transferred automatically
Note: Aliasing only works from anonymous → identified users. You cannot alias two identified users together for security reasons.
The SDK automatically keeps purchase user state in sync with your app's auth state:
// User logs in to your app
await Purchases.initialize({ appUserId: 'user_123' });
// Later, user logs out of your app
// Initialize without appUserId to reset to anonymous
await Purchases.initialize();
// Creates new anonymous user - old custom ID is cleared
// This prevents users from staying logged in to purchases
// after logging out of your app
-
Purchases.initialize(config?)
- Initialize the SDK -
Purchases.getCustomerInfo()
- Get current user's purchase info -
Purchases.getOfferings()
- Get available packages -
Purchases.isPremium()
- Quick check for premium access -
Purchases.isAnonymous()
- Check if current user is anonymous -
Purchases.getUserId()
- Get current user ID -
Purchases.purchase(packageId)
- Make a purchase -
Purchases.restore()
- Restore purchases (mobile only) -
Purchases.refreshCustomerInfo()
- Sync with backend -
Purchases.logIn(userId)
- Switch to identified user -
Purchases.logOut()
- Sign out current user -
Purchases.getManageSubscriptionUrl()
- Get subscription management URL -
Purchases.subscribe(listener)
- Listen to state changes -
Purchases.destroy()
- Clean up resources
interface PurchasesConfig {
// Enable debug logging (default: false)
debug?: boolean;
// Optional custom user ID (default: anonymous)
appUserId?: string;
}
const {
isPremium, // boolean - has active premium
isLoading, // boolean - operation in progress
isAnonymous, // boolean - is current user anonymous
userId, // string | null - current user ID
purchase, // (packageId: string) => Promise<void>
restore, // () => Promise<void>
logIn, // (userId: string) => Promise<void>
logOut, // () => Promise<void>
refreshCustomerInfo, // () => Promise<void>
getCustomerInfo, // () => CustomerInfo | null
offerings, // PurchasesOfferings
} = useA0Purchases();
function UserProfile() {
const { userId, isAnonymous, logIn, logOut } = useA0Purchases();
if (isAnonymous) {
return (
<Button onPress={() => logIn('user_123')}>
Sign In to Save Purchases
</Button>
);
}
return (
<View>
<Text>Logged in as: {userId}</Text>
<Button onPress={logOut}>Sign Out</Button>
</View>
);
}
function SubscriptionManager() {
const { isPremium, purchase, restore, refreshCustomerInfo } = useA0Purchases();
return (
<View>
{isPremium ? (
<Text>Premium Active ✓</Text>
) : (
<Button onPress={() => purchase('premium_monthly')}>
Upgrade to Premium
</Button>
)}
<Button onPress={restore}>Restore Purchases</Button>
<Button onPress={refreshCustomerInfo}>Refresh Status</Button>
</View>
);
}
Platform | Purchase Method | Restore | Notes |
---|---|---|---|
iOS | StoreKit (via expo-iap) | ✅ | Native Apple payments |
Android | Google Play Billing | ✅ | Native Google payments |
Web | Stripe Checkout | N/A | Redirects to hosted checkout |
// GOOD: Let the library manage user state
function App() {
const userIdFromAuth = getCurrentUserId(); // undefined if logged out
return (
<A0PurchaseProvider config={{
appUserId: userIdFromAuth, // Pass through your auth state
debug: __DEV__
}}>
<YourApp />
</A0PurchaseProvider>
);
}
// BAD: Hardcoding user IDs
<A0PurchaseProvider config={{ appUserId: "123" }}>
// GOOD: Check for offerings before using
const { offerings } = useA0Purchases();
const packages = offerings?.current?.availablePackages || [];
// BAD: Assuming offerings exist
Object.values(offerings.all)[0].availablePackages // Can crash!
// GOOD: Use A0PurchaseProvider directly
export function App() {
return (
<A0PurchaseProvider>
<MainScreen />
</A0PurchaseProvider>
);
}
// UNNECESSARY: Double-wrapping contexts
function SubscriptionProvider({ children }) {
return (
<A0PurchaseProvider>
<AnotherProvider>
{children}
</AnotherProvider>
</A0PurchaseProvider>
);
}
// GOOD: Keep purchase user in sync with app auth
function App() {
const { user } = useAuth(); // Your auth system
return (
<A0PurchaseProvider config={{
appUserId: user?.id // undefined when logged out = anonymous
}}>
<YourApp />
</A0PurchaseProvider>
);
}
function PurchaseButton() {
const { purchase, isLoading, isPremium } = useA0Purchases();
if (isPremium) return <Text>Already Premium!</Text>;
if (isLoading) return <ActivityIndicator />;
return (
<Button
onPress={async () => {
try {
await purchase('premium_monthly');
} catch (error) {
if (error.userCancelled) {
// User cancelled - no action needed
} else {
Alert.alert('Purchase failed', error.message);
}
}
}}
title="Upgrade"
/>
);
}
Enable debug mode to see detailed logs:
await Purchases.initialize({ debug: true });
This will log:
- Network requests to the backend
- User ID changes and aliasing
- Purchase flow steps
- Platform adapter operations
If you encounter ChunkLoadError: Loading chunk # failed
in production, this is typically caused by:
- Deployment timing: Users have the old version loaded while you deploy a new version
- CDN caching: Old chunk files are cached but no longer exist
- Bundler issues: Dynamic imports creating chunks that aren't properly deployed
Solution in v0.2.5+: The library now uses static imports instead of dynamic imports to prevent chunk splitting issues.
If you still encounter this error:
- Clear browser cache
- Ensure all build artifacts are deployed together
- Consider using service workers to handle version mismatches
// RevenueCat
Purchases.configure({ apiKey: "..." });
const offerings = await Purchases.getOfferings();
await Purchases.purchasePackage(package);
// a0-purchases
await Purchases.initialize();
const offerings = Purchases.getOfferings();
await Purchases.purchase(package.identifier);
The library uses typed errors with specific error codes:
try {
await Purchases.purchase('premium_monthly');
} catch (error) {
if (error.userCancelled) {
// User cancelled the purchase
} else if (error.code === PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_ERROR) {
// Product not available
} else {
// Other error
console.error(error.message);
}
}
MIT