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
clsxfor conditional className combination - Uses
tailwind-mergeto resolve conflicting Tailwind utilities - Prevents className conflicts (e.g., both
bg-red-500andbg-blue-500in output)
Usage:
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
<View className={cn(
"flex-1 p-4",
isActive && "bg-blue-500",
isPending && "opacity-50"
)} />
// Component with className prop
function MyComponent({ className }: { className?: string }) {
return (
<View className={cn("base-classes", className)} />
);
}
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_KEYenvironment 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)
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
import { identifyUser } from "@/src/lib/revenuecat";
const { id: userId } = db.useUser();
if (userId) {
identifyUser(userId);
}
getProducts(): Promise<PurchasesStoreProduct[]>
- 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
import { getProducts } from "@/src/lib/revenuecat";
const products = await getProducts();
purchaseProduct(product: PurchasesStoreProduct): Promise<CustomerInfo | null>
- 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
import { purchaseProduct } from "@/src/lib/revenuecat";
const customerInfo = await purchaseProduct(product);
if (customerInfo) {
// Purchase successful
} else {
// Purchase failed
}
getCustomerInfo(): Promise<CustomerInfo | null>
- 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
import { getCustomerInfo } from "@/src/lib/revenuecat";
const info = await getCustomerInfo();
if (info?.entitlements.active.includes("premium")) {
// User has premium access
}
restorePurchases(): Promise<CustomerInfo | null>
- 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
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"]ingetProducts() - 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_KEYmust be set in.envor build configuration- Prefix
EXPO_PUBLIC_makes it accessible to Expo app (not secrets) - Different keys for development vs production builds
Debug Mode:
LOG_LEVEL.DEBUGenabled for development to see SDK logs- Consider reducing to
LOG_LEVEL.WARNfor 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:
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:
- Create new file:
library-name.ts - Import SDK at top
- Create wrapper functions that return Promise-based APIs
- Handle errors gracefully with fallback values
- Include JSDoc comments for all exported functions
- Store SDK-specific config (keys, IDs) clearly at top of file
- Use TypeScript for complete type safety