# 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
