a0-purchases
TypeScript icon, indicating that this package has built-in type declarations

0.2.45 • Public • Published

a0-purchases

A unified cross-platform in-app purchase library for React Native and Web with RevenueCat-level features.

Installation

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)

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

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)

// 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

// 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)

// 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:

// 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

interface PurchasesConfig {
  // Enable debug logging (default: false)
  debug?: boolean;
  
  // Optional custom user ID (default: anonymous)
  appUserId?: string;
}

React Hook API

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

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

// 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

// 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

// 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

// 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

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:

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

// 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:

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

Package Sidebar

Install

npm i a0-purchases

Weekly Downloads

33

Version

0.2.45

License

MIT

Unpacked Size

193 kB

Total Files

66

Last publish

Collaborators

  • a0_dev