Builders Events Mobile App - Architecture
System Architecture Overview
The Builders Events Mobile App is built using a modern React Native architecture with Expo as the development platform. The app follows a modular, component-based architecture with clear separation of concerns between UI, business logic, and data management layers.
┌─────────────────────────────────────────────────────────────┐
│ Mobile Application │
├─────────────────────────────────────────────────────────────┤
│ UI Layer (React Native Components) │
│ ├─ Screens (app/) │
│ ├─ Components (components/) │
│ └─ Providers (providers/) │
├─────────────────────────────────────────────────────────────┤
│ Business Logic Layer │
│ ├─ React Query Hooks (lib/queries/) │
│ ├─ API Client (lib/api-client.ts) │
│ ├─ Authentication (Clerk + lib/auth-utils.ts) │
│ └─ Utilities (lib/utils/) │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
│ ├─ React Query Cache (with AsyncStorage persistence) │
│ ├─ Secure Storage (Expo SecureStore) │
│ └─ Local State (React Context) │
├─────────────────────────────────────────────────────────────┤
│ Platform Layer (Expo Modules) │
│ ├─ Native Modules (iOS/Android) │
│ ├─ Notifications │
│ ├─ Location Services │
│ ├─ Biometric Authentication │
│ └─ Secure Storage │
└─────────────────────────────────────────────────────────────┘
↕
┌───────────────────────────┐
│ External Services │
├───────────────────────────┤
│ Backend API (Railway) │
│ Clerk Authentication │
│ Stripe Payments │
│ Expo Push Notifications │
│ Google/Apple Maps │
└───────────────────────────┘
Technology Stack Details
Core Framework
Expo SDK 52.0.48
- Managed React Native workflow with simplified native module access
- Over-the-air updates support
- EAS Build for cloud-based compilation
- Expo Router for file-based routing
- Pre-configured native modules (notifications, location, auth, etc.)
React Native 0.76.9
- Cross-platform mobile framework (iOS/Android)
- Native performance with JavaScript logic
- Hot reloading for rapid development
- Bridge architecture for native module communication
React 18.3.1
- Concurrent rendering for improved performance
- Automatic batching of state updates
- Suspense for data fetching
- Server Components support (future consideration)
TypeScript 5.3.0
- Full type safety across codebase
- Strict mode enabled
- Type inference for reduced boilerplate
- Interface-driven development
Navigation Architecture
Expo Router 4.0.22 (File-Based Routing)
The app uses Expo Router, which provides a file-system-based routing solution similar to Next.js. Routes are automatically generated based on file structure in the app/ directory.
Route Structure:
app/
├── _layout.tsx # Root layout (providers, global setup)
├── index.tsx # Entry point (auth redirect logic)
│
├── (auth)/ # Authentication stack (no tabs)
│ ├── _layout.tsx # Auth stack layout
│ ├── sign-in.tsx # Sign in screen
│ ├── sign-up.tsx # Sign up with email verification
│ └── email-link.tsx # Email verification handler
│
├── (tabs)/ # Main app (bottom tabs)
│ ├── _layout.tsx # Tab navigator layout
│ ├── index.tsx # Home/Events tab
│ └── more.tsx # More/Settings tab
│
└── event/ # Event details (modal stack)
└── [id]/ # Dynamic event ID route
├── index.tsx # Event overview
├── schedule.tsx # Event schedule
├── people.tsx # Event attendees
├── messages.tsx # Event messages
├── shop.tsx # Event shop
├── donations.tsx # Event donations
├── maps.tsx # Event maps
└── surveys.tsx # Event surveys
Navigation Features:
- Type-Safe Routes: TypeScript types auto-generated for all routes
- Deep Linking: Universal links (https://) and custom scheme (builders-events://)
- Stack Navigation: Hierarchical navigation with back buttons
- Tab Navigation: Bottom tab bar with 2 primary tabs
- Modal Navigation: Full-screen modals for focused tasks
- Typed Routes: Experiments enabled for typed route navigation
Deep Linking Configuration:
// app.config.ts
scheme: 'builders-events',
associatedDomains: [
'webcredentials:clerk.buildersevents.org',
'webcredentials:buildersinternational.com',
]
State Management Architecture
The app uses a multi-layered state management approach:
1. Server State (React Query)
TanStack React Query 5.17.0 - Primary state management solution
- Automatic caching with stale-while-revalidate pattern
- Background refetching for fresh data
- Optimistic updates for instant UI feedback
- Query invalidation for manual updates
- Persistent cache with AsyncStorage
Configuration:
// lib/query-client.ts
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 2,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
});
// Persist cache to AsyncStorage
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: QUERY_CACHE_KEY,
});
persistQueryClient({
queryClient,
persister: asyncStoragePersister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
Query Hooks:
// lib/queries/use-events.ts
export function useEvents() {
return useQuery({
queryKey: ['events'],
queryFn: () => api.getEvents(),
});
}
export function useEvent(id: string) {
return useQuery({
queryKey: ['event', id],
queryFn: () => api.getEvent(id),
enabled: !!id,
});
}
Available Query Hooks:
use-events.ts- Events listing and detailsuse-schedule.ts- Event schedulesuse-people.ts- Attendee directoryuse-messages.ts- Message threadsuse-products.ts- Shop productsuse-projects.ts- Donation projectsuse-surveys.ts- Survey listingsuse-locations.ts- Map locationsuse-meetings.ts- Meeting scheduling
2. Global State (React Context)
Context Providers:
// Root Layout Provider Stack
<ClerkProvider>
<QueryClientProvider>
<StripeProvider>
<SafeAreaProvider>
<EventProvider>
<BrandingProvider>
<ShopProvider>
<RootErrorBoundary>
{/* App Routes */}
</RootErrorBoundary>
</ShopProvider>
</BrandingProvider>
</EventProvider>
</SafeAreaProvider>
</StripeProvider>
</QueryClientProvider>
</ClerkProvider>
EventProvider (providers/EventProvider.tsx)
- Manages current event context
- Stores event registration keys in SecureStore
- Provides event switching functionality
- Tracks user's registered events
BrandingProvider (providers/BrandingProvider.tsx)
- Theme management (light/dark mode)
- Dynamic color scheme
- Brand asset loading
- Liquid Glass design system utilities
ShopProvider (providers/ShopProvider.tsx)
- Shopping cart state
- Product selection
- Checkout flow management
- Order history
3. Local State (React Hooks)
useStatefor component-level stateuseReducerfor complex form stateuseReffor mutable references (scroll positions, timers)useMemo/useCallbackfor performance optimization
4. Secure Storage (Expo SecureStore)
- Authentication tokens (Clerk JWT)
- Event registration keys
- User preferences
- Sensitive configuration
Authentication & Authorization
Clerk Expo 2.19.21 - Modern Authentication Platform
Architecture:
// Root Layout (_layout.tsx)
import { ClerkProvider, useAuth } from '@clerk/clerk-expo';
import * as SecureStore from 'expo-secure-store';
// Clerk token cache using SecureStore
const tokenCache = {
async getToken(key: string) {
return SecureStore.getItemAsync(key);
},
async saveToken(key: string, value: string) {
return SecureStore.setItemAsync(key, value);
},
};
<ClerkProvider
publishableKey={CLERK_PUBLISHABLE_KEY}
tokenCache={tokenCache}
>
<App />
</ClerkProvider>
Authentication Flow:
-
Sign Up (
app/(auth)/sign-up.tsx)- Email and password collection
- Client-side validation
- Clerk user creation
- Email verification sent
- Optional: Phone number verification
-
Email Verification (
app/(auth)/email-link.tsx)- Magic link handler
- Token exchange
- Session establishment
- Redirect to app
-
Sign In (
app/(auth)/sign-in.tsx)- Email/password authentication
- Optional: Passkey authentication (biometrics)
- Remember me functionality
- Session token storage
-
Biometric Authentication (
hooks/useBiometricAuth.ts)- Face ID / Touch ID support
- Fallback to password
- Secure key storage
- Re-authentication on app resume
Authorization:
- JWT tokens issued by Clerk
- Custom JWT template for API authorization
- Token refresh handled automatically
- API client injects token in Authorization header
Token Flow:
// lib/api-client.ts
let getTokenFn: (() => Promise<string | null>) | null = null;
export function setAuthTokenGetter(getToken: () => Promise<string | null>) {
getTokenFn = getToken;
}
// Axios interceptor
apiClient.interceptors.request.use(async (config) => {
if (getTokenFn) {
const token = await getTokenFn();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
});
// Root layout setup
import { useAuth } from '@clerk/clerk-expo';
import { setAuthTokenGetter } from '@/lib/api-client';
function RootLayout() {
const { getToken } = useAuth();
useEffect(() => {
setAuthTokenGetter(() => getToken({ template: 'api-token' }));
}, [getToken]);
}
Passkeys Integration:
// app.config.ts - iOS
infoPlist: {
NSFaceIDUsageDescription: 'Allow Builders Events to use Face ID for quick and secure sign in.',
}
// app.config.ts - Android
permissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT']
// Passkey creation
import { passkeys } from '@clerk/clerk-expo/passkeys';
async function createPasskey() {
await passkeys.create();
}
API Integration
Axios 1.6.0 - HTTP Client
Client Configuration:
// lib/api-client.ts
export const apiClient = axios.create({
baseURL: API_URL, // https://builders-eventsapi-production.up.railway.app
headers: {
'Content-Type': 'application/json',
},
timeout: 15000,
});
Request Interceptor:
apiClient.interceptors.request.use(
async (config) => {
// Inject authentication token
const token = await getTokenFn();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Log request details (development)
if (__DEV__) {
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
Response Interceptor:
apiClient.interceptors.response.use(
(response) => {
// Log response (development)
if (__DEV__) {
console.log(`API Response: ${response.status} ${response.config.url}`);
}
return response;
},
async (error: AxiosError) => {
// Handle authentication errors
if (error.response?.status === 401) {
// Token expired - trigger re-authentication
await refreshAuthToken();
}
// Handle network errors
if (!error.response) {
console.error('Network error:', error.message);
}
return Promise.reject(error);
}
);
API Methods:
export const api = {
// Events
getEvents: () => apiClient.get('/events'),
getEvent: (id: string) => apiClient.get(`/events/${id}`),
// Schedule
getSchedule: (eventId: string) => apiClient.get(`/events/${eventId}/schedule`),
// People
getPeople: (eventId: string) => apiClient.get(`/events/${eventId}/people`),
// Messages
getMessages: () => apiClient.get('/messages'),
sendMessage: (data: MessageData) => apiClient.post('/messages', data),
// Products
getProducts: (eventId: string) => apiClient.get(`/events/${eventId}/products`),
// Donations
getProjects: () => apiClient.get('/projects'),
// Surveys
getSurveys: (eventId: string) => apiClient.get(`/events/${eventId}/surveys`),
submitSurvey: (data: SurveyData) => apiClient.post('/surveys', data),
// Locations
getLocations: (eventId: string) => apiClient.get(`/events/${eventId}/locations`),
};
Error Handling:
// Custom error types
export class APIError extends Error {
constructor(
public statusCode: number,
public message: string,
public data?: unknown
) {
super(message);
}
}
// Error transformation
function handleAPIError(error: AxiosError): never {
if (error.response) {
throw new APIError(
error.response.status,
error.response.data?.message || error.message,
error.response.data
);
}
throw new APIError(0, 'Network error', error.message);
}
Push Notifications
Expo Notifications 0.29.14
Architecture:
// Root layout notification setup
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
// Configure notification handling
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
// Request permissions and get token
async function registerForPushNotifications() {
if (!Device.isDevice) {
console.log('Push notifications only work on physical devices');
return null;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Push notification permission denied');
return null;
}
const token = await Notifications.getExpoPushTokenAsync({
projectId: 'e7e931c3-ce24-4f8a-a6a1-96114f753ab8',
});
return token.data;
}
// Send token to backend
async function registerPushToken(token: string) {
await api.registerPushToken({ token });
}
Notification Channels (Android):
// Create notification channels for Android
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#004d72',
});
await Notifications.setNotificationChannelAsync('messages', {
name: 'Messages',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#e2783e',
});
}
Notification Handling:
// Listen for foreground notifications
Notifications.addNotificationReceivedListener((notification) => {
console.log('Notification received:', notification);
// Update UI, refetch data, etc.
});
// Listen for notification taps
Notifications.addNotificationResponseReceivedListener((response) => {
console.log('Notification tapped:', response);
const data = response.notification.request.content.data;
// Navigate based on notification data
if (data.type === 'message') {
router.push(`/messages/${data.messageId}`);
} else if (data.type === 'event') {
router.push(`/event/${data.eventId}`);
}
});
Configuration:
// app.config.ts
plugins: [
[
'expo-notifications',
{
icon: './assets/notification-icon.png',
color: '#004d72',
},
],
]
Maps Integration
React Native Maps 1.18.0
Platform-Specific Configuration:
iOS - Apple Maps
// app.config.ts
ios: {
infoPlist: {
NSLocationWhenInUseUsageDescription: 'This app uses your location to show nearby events and venues.',
NSLocationAlwaysAndWhenInUseUsageDescription: 'This app uses your location to show nearby events and venues.',
},
}
Android - Google Maps
// app.config.ts
android: {
permissions: ['ACCESS_FINE_LOCATION', 'ACCESS_COARSE_LOCATION'],
googleServicesFile: './google-services.json',
}
Map Component Usage:
import MapView, { Marker, PROVIDER_GOOGLE, PROVIDER_DEFAULT } from 'react-native-maps';
import * as Location from 'expo-location';
function EventMapScreen() {
const [location, setLocation] = useState<Location.LocationObject | null>(null);
const [markers, setMarkers] = useState<MarkerData[]>([]);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status === 'granted') {
const location = await Location.getCurrentPositionAsync({});
setLocation(location);
}
})();
}, []);
return (
<MapView
style={{ flex: 1 }}
provider={Platform.OS === 'android' ? PROVIDER_GOOGLE : PROVIDER_DEFAULT}
initialRegion={{
latitude: location?.coords.latitude ?? 37.7749,
longitude: location?.coords.longitude ?? -122.4194,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
showsUserLocation
showsMyLocationButton
>
{markers.map((marker) => (
<Marker
key={marker.id}
coordinate={{
latitude: marker.latitude,
longitude: marker.longitude,
}}
title={marker.title}
description={marker.description}
/>
))}
</MapView>
);
}
Payment Processing
Stripe React Native 0.38.6
Configuration:
// app.config.ts
plugins: [
[
'@stripe/stripe-react-native',
{
enableGooglePay: false, // Future: enable for Android
},
],
]
// Root layout
import { StripeProvider } from '@stripe/stripe-react-native';
<StripeProvider publishableKey={STRIPE_PUBLISHABLE_KEY}>
{/* App */}
</StripeProvider>
Payment Flow:
import { useStripe } from '@stripe/stripe-react-native';
function CheckoutScreen() {
const { confirmPayment } = useStripe();
async function handlePayment(amount: number) {
// 1. Create payment intent on backend
const { clientSecret } = await api.createPaymentIntent({ amount });
// 2. Confirm payment with Stripe
const { error, paymentIntent } = await confirmPayment(clientSecret, {
paymentMethodType: 'Card',
});
if (error) {
Alert.alert('Payment failed', error.message);
} else if (paymentIntent) {
Alert.alert('Payment successful!');
// Navigate to success screen
}
}
return (
<CardField
postalCodeEnabled={true}
placeholder={{
number: '4242 4242 4242 4242',
}}
cardStyle={{
backgroundColor: '#FFFFFF',
textColor: '#000000',
}}
style={{
width: '100%',
height: 50,
marginVertical: 30,
}}
onCardChange={(cardDetails) => {
console.log('Card details:', cardDetails);
}}
/>
);
}
File Structure
builders-events-mobile/
│
├── app/ # Expo Router pages (file-based routing)
│ ├── _layout.tsx # Root layout with providers
│ ├── index.tsx # Entry point with auth redirect
│ ├── (auth)/ # Authentication stack
│ │ ├── _layout.tsx # Auth stack layout
│ │ ├── sign-in.tsx # Sign in screen
│ │ ├── sign-up.tsx # Sign up screen
│ │ └── email-link.tsx # Email verification handler
│ ├── (tabs)/ # Main app tabs
│ │ ├── _layout.tsx # Tab navigator
│ │ ├── index.tsx # Home/Events tab
│ │ └── more.tsx # More/Settings tab
│ └── event/ # Event details
│ └── [id]/ # Dynamic event route
│ ├── index.tsx # Event overview
│ ├── schedule.tsx # Event schedule
│ ├── people.tsx # Event attendees
│ ├── messages.tsx # Event messages
│ ├── shop.tsx # Event shop
│ ├── donations.tsx # Event donations
│ ├── maps.tsx # Event maps
│ └── surveys.tsx # Event surveys
│
├── components/ # Reusable UI components
│ ├── EventCard.tsx # Event card component
│ ├── ScheduleItem.tsx # Schedule item component
│ ├── PersonCard.tsx # Person card component
│ ├── MessageBubble.tsx # Message bubble component
│ ├── ProductCard.tsx # Product card component
│ ├── ExpandableProductCard.tsx # Expandable product card
│ ├── LoadingSpinner.tsx # Loading indicator
│ ├── EmptyState.tsx # Empty state component
│ ├── BuiltByFooter.tsx # Footer component
│ └── RootErrorBoundary.tsx # Global error boundary
│
├── lib/ # Business logic and utilities
│ ├── api-client.ts # Axios API client
│ ├── api-client.unit.test.ts # API client tests
│ ├── auth-utils.ts # Authentication utilities
│ ├── biometric-auth.ts # Biometric auth logic
│ ├── query-client.ts # React Query configuration
│ ├── realtime-client.ts # WebSocket/realtime client
│ ├── queries/ # React Query hooks
│ │ ├── use-events.ts # Event queries
│ │ ├── use-events.unit.test.ts # Event query tests
│ │ ├── use-schedule.ts # Schedule queries
│ │ ├── use-people.ts # People queries
│ │ ├── use-messages.ts # Message queries
│ │ ├── use-products.ts # Product queries
│ │ ├── use-projects.ts # Project/donation queries
│ │ ├── use-surveys.ts # Survey queries
│ │ ├── use-locations.ts # Location queries
│ │ └── use-meetings.ts # Meeting queries
│ ├── utils/ # Utility functions
│ │ ├── base64.ts # Base64 encoding/decoding
│ │ └── liquid-glass.ts # Liquid Glass design utilities
│ └── mocks/ # Test mocks
│
├── providers/ # React Context providers
│ ├── EventProvider.tsx # Event context
│ ├── EventProvider.unit.test.tsx # Event provider tests
│ ├── BrandingProvider.tsx # Theme/branding context
│ ├── ShopProvider.tsx # Shopping cart context
│ ├── RealtimeProvider.tsx # WebSocket context
│ └── RealtimeProvider.unit.test.tsx
│
├── hooks/ # Custom React hooks
│ └── useBiometricAuth.ts # Biometric authentication hook
│
├── types/ # TypeScript type definitions
│ ├── index.ts # Main type exports
│ └── router.d.ts # Expo Router types
│
├── constants/ # App constants
│ ├── Colors.ts # Color scheme
│ └── Layout.ts # Layout constants
│
├── assets/ # Static assets
│ ├── icon.png # App icon
│ ├── splash.png # Splash screen
│ ├── splash.mp4 # Animated splash (optional)
│ ├── adaptive-icon.png # Android adaptive icon
│ ├── notification-icon.png # Notification icon
│ ├── favicon.png # Web favicon
│ ├── BlueLogo.png # Brand logo (blue)
│ └── WhiteLogo.png # Brand logo (white)
│
├── ios/ # iOS native code
│ ├── BuildersEvents/ # iOS app target
│ ├── BuildersEvents.xcodeproj/ # Xcode project
│ ├── BuildersEvents.xcworkspace/ # Xcode workspace
│ ├── Podfile # CocoaPods dependencies
│ └── Pods/ # CocoaPods modules
│
├── patches/ # Package patches (patch-package)
│
├── Refactor/ # Architecture refactoring notes
│ ├── CURRENT_STATE.md # Current architecture state
│ ├── FUTURE_STATE_NOTES.md # Planned architecture changes
│ └── CHANGE_IMPACT.md # Impact analysis
│
├── .expo/ # Expo local cache
├── .github/ # GitHub configuration
│
├── app.config.ts # Expo app configuration (TypeScript)
├── app.json # Expo app configuration (JSON)
├── eas.json # EAS Build configuration
├── package.json # Dependencies and scripts
├── pnpm-lock.yaml # pnpm lock file
├── pnpm-workspace.yaml # pnpm workspace config
├── tsconfig.json # TypeScript configuration
├── babel.config.js # Babel configuration
├── babel.jest-unit.cjs # Jest Babel config
├── metro.config.js # Metro bundler configuration
├── jest.unit.config.cjs # Jest configuration
├── jest.setup.unit.ts # Jest setup and mocks
├── .eslintrc.cjs # ESLint configuration
├── .env # Environment variables (gitignored)
├── .env.example # Environment variables template
├── .gitignore # Git ignore rules
├── entry.js # Custom entry point
├── expo-env.d.ts # Expo TypeScript types
├── google-services.json # Google Services config (Android)
├── .xcode.env # Xcode environment variables
│
├── README.md # Project overview
├── SETUP.md # Setup instructions
├── TESTING.md # Testing guide
├── PROJECT_STRUCTURE.md # Detailed structure documentation
├── IMPLEMENTATION_SUMMARY.md # Recent implementation summary
├── NEXT_STEPS.md # Planned improvements
└── DEBUGGING_WHITE_SCREEN.md # Troubleshooting guide
Design Patterns
Component Patterns
1. Screen Components - Full-screen pages with data fetching
// app/(tabs)/index.tsx
export default function HomeScreen() {
const { data: events, isLoading, error } = useEvents();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorState error={error} />;
if (!events || events.length === 0) return <EmptyState />;
return (
<FlatList
data={events}
renderItem={({ item }) => <EventCard event={item} />}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={refetch} />
}
/>
);
}
2. Container/Presenter Pattern - Separation of logic and UI
// Container (logic)
function EventDetailsContainer({ eventId }: { eventId: string }) {
const { data: event, isLoading } = useEvent(eventId);
const navigation = useNavigation();
const handleRSVP = async () => {
await api.rsvpToEvent(eventId);
queryClient.invalidateQueries(['event', eventId]);
};
return (
<EventDetailsPresenter
event={event}
isLoading={isLoading}
onRSVP={handleRSVP}
onBack={() => navigation.goBack()}
/>
);
}
// Presenter (UI)
function EventDetailsPresenter({ event, isLoading, onRSVP, onBack }) {
if (isLoading) return <LoadingSpinner />;
return (
<View>
<Text>{event.title}</Text>
<Button onPress={onRSVP}>RSVP</Button>
</View>
);
}
3. Compound Components - Related components grouped together
// EventCard as a compound component
function EventCard({ event }: { event: Event }) {
return (
<EventCard.Container>
<EventCard.Image source={{ uri: event.imageUrl }} />
<EventCard.Content>
<EventCard.Title>{event.title}</EventCard.Title>
<EventCard.Date>{formatDate(event.startDate)}</EventCard.Date>
<EventCard.Location>{event.location}</EventCard.Location>
</EventCard.Content>
<EventCard.Actions>
<EventCard.Button onPress={() => {}}>View Details</EventCard.Button>
</EventCard.Actions>
</EventCard.Container>
);
}
Data Fetching Patterns
1. Query Hooks - Declarative data fetching
function EventScreen({ eventId }: { eventId: string }) {
const { data, isLoading, error, refetch } = useEvent(eventId);
// Data automatically cached and revalidated
// Pull-to-refresh with refetch()
// Loading and error states handled declaratively
}
2. Optimistic Updates - Instant UI feedback
function useRSVPMutation(eventId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: string) => api.rsvpToEvent(eventId, userId),
onMutate: async (userId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries(['event', eventId]);
// Snapshot previous value
const previousEvent = queryClient.getQueryData(['event', eventId]);
// Optimistically update cache
queryClient.setQueryData(['event', eventId], (old: Event) => ({
...old,
attendees: [...old.attendees, userId],
}));
return { previousEvent };
},
onError: (err, userId, context) => {
// Rollback on error
queryClient.setQueryData(['event', eventId], context.previousEvent);
},
onSettled: () => {
// Refetch to ensure cache is correct
queryClient.invalidateQueries(['event', eventId]);
},
});
}
3. Infinite Queries - Pagination and infinite scroll
function useInfiniteMessages() {
return useInfiniteQuery({
queryKey: ['messages'],
queryFn: ({ pageParam = 1 }) => api.getMessages({ page: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextPage,
});
}
function MessagesScreen() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteMessages();
return (
<FlatList
data={data?.pages.flatMap((page) => page.messages)}
onEndReached={() => hasNextPage && fetchNextPage()}
onEndReachedThreshold={0.5}
ListFooterComponent={() =>
isFetchingNextPage ? <LoadingSpinner /> : null
}
/>
);
}
Build Configuration
EAS Build
Configuration (eas.json):
{
"cli": {
"version": ">= 16.22.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"node": "20.11.0",
"env": {
"EXPO_POD_INSTALL_ARGS": "--repo-update --clean-install",
"EXPO_PUBLIC_API_URL": "https://builders-eventsapi-production.up.railway.app"
}
},
"preview": {
"distribution": "internal",
"node": "20.11.0",
"env": {
"EXPO_POD_INSTALL_ARGS": "--repo-update --clean-install",
"EXPO_PUBLIC_API_URL": "https://builders-eventsapi-production.up.railway.app"
}
},
"production": {
"autoIncrement": true,
"node": "20.11.0",
"ios": {
"image": "latest"
},
"android": {
"image": "latest"
},
"env": {
"EXPO_POD_INSTALL_ARGS": "--repo-update --clean-install",
"EXPO_PUBLIC_API_URL": "https://builders-eventsapi-production.up.railway.app"
}
}
},
"submit": {
"production": {}
}
}
Build Commands:
# Development build (with dev client)
eas build --profile development --platform ios
eas build --profile development --platform android
# Preview build (internal testing)
eas build --profile preview --platform ios
eas build --profile preview --platform android
# Production build
eas build --profile production --platform ios
eas build --profile production --platform android
# Build both platforms
eas build --profile production --platform all
Pre/Post Install Hooks:
// package.json
"scripts": {
"eas-build-pre-install": "bash -lc 'if [ \"$EAS_BUILD_PLATFORM\" = \"ios\" ]; then pod repo remove trunk || true; pod setup; pod repo update; pod cache clean boost --all || true; rm -rf ~/Library/Caches/CocoaPods || true; fi'",
"eas-build-post-install": "bash -lc 'if [ \"$EAS_BUILD_PLATFORM\" = \"ios\" ]; then FILE=node_modules/react-native/third-party-podspecs/boost.podspec; if [ -f \"$FILE\" ]; then sed -i'' -e \"s|s.source = { :http => .* }|s.source = { :git => \\\"https://github.com/boostorg/boost.git\\\", :tag => \\\"boost-1.83.0\\\" }|\" \"$FILE\"; fi; fi'",
"postinstall": "patch-package"
}
App Store Deployment
iOS:
# Build for App Store
eas build --profile production --platform ios
# Submit to App Store
eas submit --platform ios --latest
Android:
# Build for Google Play
eas build --profile production --platform android
# Submit to Google Play
eas submit --platform android --latest
Testing Architecture
Unit Testing
Jest 29.7.0 with Node-only test environment
Configuration (jest.unit.config.cjs):
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.unit.test.ts', '**/*.unit.test.tsx'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.unit.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'^@components/(.*)$': '<rootDir>/components/$1',
'^@lib/(.*)$': '<rootDir>/lib/$1',
'^@providers/(.*)$': '<rootDir>/providers/$1',
'^@hooks/(.*)$': '<rootDir>/hooks/$1',
'^@types/(.*)$': '<rootDir>/types/$1',
},
collectCoverageFrom: [
'lib/**/*.{ts,tsx}',
'providers/**/*.{ts,tsx}',
'components/**/*.{ts,tsx}',
'!**/*.d.ts',
'!**/__tests__/**',
'!**/*.test.*',
'!**/mocks/**',
],
coverageThresholds: {
global: {
branches: 50,
functions: 50,
lines: 50,
statements: 50,
},
},
};
Test Setup (jest.setup.unit.ts):
// Mock Expo modules
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
apiUrl: 'http://localhost:3000/api',
},
},
}));
jest.mock('expo-secure-store');
jest.mock('expo-router');
jest.mock('@tanstack/react-query');
jest.mock('react-native', () => ({
Platform: { OS: 'ios', select: jest.fn() },
StyleSheet: { create: jest.fn() },
// ... other mocks
}));
Test Examples:
// lib/api-client.unit.test.ts
describe('API Client', () => {
it('should set auth token getter', () => {
const getToken = jest.fn();
setAuthTokenGetter(getToken);
expect(getToken).toBeDefined();
});
it('should have API methods', () => {
expect(typeof api.getEvents).toBe('function');
expect(typeof api.getEvent).toBe('function');
});
});
Test Coverage:
- ✅ 29 passing tests
- ✅ 3 test suites
- ✅ ~2 second execution time
- ✅ 50% minimum coverage
Performance Optimizations
React Native Performance
1. Memoization
const MemoizedEventCard = React.memo(EventCard, (prev, next) => {
return prev.event.id === next.event.id;
});
2. FlatList Optimization
<FlatList
data={events}
renderItem={renderEventCard}
keyExtractor={(item) => item.id}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
3. Image Optimization
<Image
source={{ uri: event.imageUrl }}
style={{ width: 300, height: 200 }}
resizeMode="cover"
progressiveRenderingEnabled={true}
cache="force-cache"
/>
Query Cache Optimization
1. Stale-While-Revalidate
staleTime: 5 * 60 * 1000, // Data fresh for 5 minutes
cacheTime: 10 * 60 * 1000, // Cache kept for 10 minutes
2. Selective Refetching
refetchOnWindowFocus: true, // Refetch when app returns to foreground
refetchOnReconnect: true, // Refetch when network reconnects
refetchOnMount: false, // Don't refetch if cache is fresh
3. Query Prefetching
// Prefetch event details when hovering event card
function EventCard({ event }) {
const queryClient = useQueryClient();
const handlePrefetch = () => {
queryClient.prefetchQuery(['event', event.id], () =>
api.getEvent(event.id)
);
};
return <Pressable onPressIn={handlePrefetch}>...</Pressable>;
}
Security Considerations
Data Security
- All authentication tokens stored in Expo SecureStore (hardware-backed encryption)
- HTTPS-only API communication
- Certificate pinning (future consideration)
- No sensitive data in AsyncStorage
Code Security
- Environment variables not committed to git
- API keys injected at build time
- Source maps disabled in production
- ProGuard/R8 obfuscation (Android)
User Privacy
- Location permissions requested only when needed
- Camera permissions requested only for QR scanning
- Biometric data stored on-device only (never sent to server)
- GDPR-compliant data deletion
Monitoring & Debugging
Development Debugging
- React Native Debugger integration
- Expo Dev Tools
- Console logging with structured breadcrumbs
- Network request/response logging
Production Monitoring
- Crash reporting (future: Sentry integration)
- Analytics (future: Segment/Amplitude integration)
- Performance monitoring (future: Firebase Performance)
- Error tracking with detailed stack traces
Documentation Version: 1.0 Last Updated: March 26, 2026 Maintained By: Builders International Development Team