# apps/mobile/src/lib - Library Integration Modules
This directory contains wrapper modules and integration code for external SDKs and libraries used by the mobile app.
## Directory Structure
```
lib/
├── revenuecat.ts # RevenueCat SDK wrapper for in-app purchases
├── utils.ts # Utility functions (cn() for className merging)
└── CLAUDE.md # This file
```
## utils.ts
**Purpose**: Common utility functions for the mobile app.
### cn(...inputs: ClassValue[])
**Purpose**: Safely merge Tailwind CSS className strings with conflict resolution. This is essential for Uniwind styling.
**How it works**:
- Uses `clsx` for conditional className combination
- Uses `tailwind-merge` to resolve conflicting Tailwind utilities
- Prevents className conflicts (e.g., both `bg-red-500` and `bg-blue-500` in output)
**Usage**:
```tsx
import { cn } from "@/lib/utils";
// Basic merging
const classes = cn("px-4 py-2", "px-8"); // Result: "py-2 px-8" (px-8 wins)
// Conditional classes
// Component with className prop
function MyComponent({ className }: { className?: string }) {
return (
);
}
```
**When to use**:
- ALWAYS use when merging multiple className strings
- ALWAYS use when accepting className prop in components
- Use for conditional/dynamic styling with Uniwind
**Reference**: See `@docs/uniwind/llms.txt` for more Uniwind styling patterns
## revenuecat.ts
**Purpose**: Wrapper functions around the RevenueCat SDK (`react-native-purchases`) for managing in-app purchases.
**Location**: `revenuecat.ts`
### Key Functions
#### initializeRevenueCat()
- **Purpose**: Initialize RevenueCat SDK once at app startup
- **Requirements**: `EXPO_PUBLIC_REVENUECAT_API_KEY` environment variable
- **Side Effects**: Configures RevenueCat SDK with API key, sets debug logging
- **Usage**: Call once in app startup (e.g., in payments screen on first load)
```tsx
import { initializeRevenueCat } from "@/src/lib/revenuecat";
initializeRevenueCat();
```
#### identifyUser(userId: string)
- **Purpose**: Link current user's RevenueCat purchases to their account
- **Params**: User ID from InstantDB
- **Effect**: Links purchases across devices for same user
- **Usage**: Call after user authentication
```tsx
import { identifyUser } from "@/src/lib/revenuecat";
const { id: userId } = db.useUser();
if (userId) {
identifyUser(userId);
}
```
#### getProducts(): Promise
- **Purpose**: Fetch available products configured in RevenueCat dashboard
- **Returns**: Array of store products with pricing and metadata
- **Side Effects**: Connects to App Store/Google Play to get latest product info
- **Product ID**: Currently hardcoded to fetch `["premium_upgrade"]` product
- **Error**: Returns empty array on failure
```tsx
import { getProducts } from "@/src/lib/revenuecat";
const products = await getProducts();
```
#### purchaseProduct(product: PurchasesStoreProduct): Promise
- **Purpose**: Initiate purchase flow for a product
- **Params**: Product object from getProducts()
- **Returns**: Updated customer info after purchase (if successful)
- **UI**: Opens native OS purchase sheet (App Store on iOS, Google Play on Android)
- **Error Handling**: Returns null on failure, errors should be caught in calling code
```tsx
import { purchaseProduct } from "@/src/lib/revenuecat";
const customerInfo = await purchaseProduct(product);
if (customerInfo) {
// Purchase successful
} else {
// Purchase failed
}
```
#### getCustomerInfo(): Promise
- **Purpose**: Fetch current user's purchase and entitlement information
- **Returns**: Object with active subscriptions, entitlements, and purchases
- **Usage**: Check if user has access to premium features
- **Error**: Returns null on failure
```tsx
import { getCustomerInfo } from "@/src/lib/revenuecat";
const info = await getCustomerInfo();
if (info?.entitlements.active.includes("premium")) {
// User has premium access
}
```
#### restorePurchases(): Promise
- **Purpose**: Restore purchases from another device or app reinstall
- **Effect**: Syncs purchases from App Store/Google Play back to user account
- **Usage**: Call when user taps "Restore Purchases" button
- **Error**: Returns null on failure
```tsx
import { restorePurchases } from "@/src/lib/revenuecat";
const customerInfo = await restorePurchases();
if (customerInfo) {
// Purchases restored
}
```
### Important Implementation Notes
**Hardcoded Product ID**:
- Currently set to `["premium_upgrade"]` in `getProducts()`
- Must match product ID configured in RevenueCat dashboard
- Product must exist in App Store Connect (iOS) and Google Play Console (Android)
- Can be made dynamic by passing as parameter
**Environment Variable**:
- `EXPO_PUBLIC_REVENUECAT_API_KEY` must be set in `.env` or build configuration
- Prefix `EXPO_PUBLIC_` makes it accessible to Expo app (not secrets)
- Different keys for development vs production builds
**Debug Mode**:
- `LOG_LEVEL.DEBUG` enabled for development to see SDK logs
- Consider reducing to `LOG_LEVEL.WARN` for production
**Testing**:
- Use sandbox test accounts in App Store Settings (iOS) or Google Play Console (Android)
- Purchases won't charge real accounts in sandbox mode
- Essential for testing purchase flows
### Integration Pattern
Typical usage in a payment screen:
```tsx
import { getProducts, purchaseProduct, initializeRevenueCat } from "@/src/lib/revenuecat";
import { useMutation } from "@tanstack/react-query";
import { trpc } from "@repo/trpc/client";
function PaymentsScreen() {
// Initialize on first load
useEffect(() => {
initializeRevenueCat();
loadProducts();
}, []);
// Load available products
const loadProducts = async () => {
const products = await getProducts();
setProducts(products);
};
// Record purchase in backend
const recordPurchase = useMutation(
trpc.payments.recordMobilePurchase.mutationOptions()
);
// Handle purchase
const handlePurchase = async (product) => {
const customerInfo = await purchaseProduct(product);
if (customerInfo) {
// Record in backend via tRPC
await recordPurchase.mutateAsync({
userId,
productId: product.identifier,
// ... other data
});
}
};
}
```
### Error Handling Best Practices
- Always wrap calls in try-catch
- Log errors to console for debugging
- Show user-friendly error messages in UI
- Don't assume success - check return values
- Test both iOS and Android platforms
## Adding New Library Modules
Follow the same pattern for other external SDKs:
1. Create new file: `library-name.ts`
2. Import SDK at top
3. Create wrapper functions that return Promise-based APIs
4. Handle errors gracefully with fallback values
5. Include JSDoc comments for all exported functions
6. Store SDK-specific config (keys, IDs) clearly at top of file
7. Use TypeScript for complete type safety