UNPKG

a0-purchases

Version:

Lightweight subscription management for AI apps with auto-detecting providers

430 lines (339 loc) 10.5 kB
# a0-purchases A unified cross-platform in-app purchase library for React Native and Web with RevenueCat-level features. ## Installation ```bash npm install a0-purchases # or yarn add a0-purchases # or pnpm add a0-purchases ``` ## Features - 🌐 **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 ## Quick Start ### Basic Setup (Direct API) ```typescript 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); } ``` ### React Integration ```tsx 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> ); } ``` ## User ID Management The SDK provides flexible user ID management with automatic aliasing support: ### Anonymous Users (Default) ```typescript // Initialize without a user ID - creates anonymous user await Purchases.initialize(); console.log(Purchases.getUserId()); // "$AnonymousUser:abc123..." console.log(Purchases.isAnonymous()); // true ``` ### Custom User IDs ```typescript // Initialize with your own user ID await Purchases.initialize({ appUserId: 'user_12345' }); console.log(Purchases.getUserId()); // "user_12345" console.log(Purchases.isAnonymous()); // false ``` ### Anonymous to Identified (Aliasing) ```typescript // 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. ### Auth State Synchronization The SDK automatically keeps purchase user state in sync with your app's auth state: ```typescript // 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 ``` ## API Reference ### Core Methods - `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 ### Configuration Options ```typescript interface PurchasesConfig { // Enable debug logging (default: false) debug?: boolean; // Optional custom user ID (default: anonymous) appUserId?: string; } ``` ### React Hook API ```typescript 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(); ``` #### React Hook Examples ```tsx 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 Support | 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 | ## Best Practices ### DO: Let the Library Handle User IDs ```typescript // 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" }}> ``` ### DO: Handle Missing Offerings ```typescript // 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! ``` ### DO: Use the Provider Directly ```typescript // GOOD: Use A0PurchaseProvider directly export function App() { return ( <A0PurchaseProvider> <MainScreen /> </A0PurchaseProvider> ); } // UNNECESSARY: Double-wrapping contexts function SubscriptionProvider({ children }) { return ( <A0PurchaseProvider> <AnotherProvider> {children} </AnotherProvider> </A0PurchaseProvider> ); } ``` ### DO: Sync with Your Auth System ```typescript // 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> ); } ``` ### DO: Handle All Purchase States ```typescript 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" /> ); } ``` ## Debugging Enable debug mode to see detailed logs: ```typescript await Purchases.initialize({ debug: true }); ``` This will log: - Network requests to the backend - User ID changes and aliasing - Purchase flow steps - Platform adapter operations ## Troubleshooting ### ChunkLoadError in Web Environments If you encounter `ChunkLoadError: Loading chunk # failed` in production, this is typically caused by: 1. **Deployment timing**: Users have the old version loaded while you deploy a new version 2. **CDN caching**: Old chunk files are cached but no longer exist 3. **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 ## Migration from RevenueCat ```typescript // 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); ``` ## Error Handling The library uses typed errors with specific error codes: ```typescript 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); } } ``` ## License MIT