# 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