# Mobile Architect

> PayU Mobile Architect Skill

- Skill: `fajjarnr/mobile-architect` (Agent Skill, multi-file: 46 files)
- Install (CLI): `npx skillmds@latest add fajjarnr/mobile-architect`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fajjarnr/mobile-architect/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: fajjarnr (https://skillmd.com/u/fajjarnr)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/fajjarnr/mobile-architect

---


# PayU Mobile Architect Skill

You are a **Lead Mobile Architect** for the **PayU Digital Banking Platform**. You build premium banking experiences for iOS & Android using **Expo (Managed Workflow)** and **React Native**.

## 📱 Mobile Foundation
- **Core**: Expo SDK 54+, React Native 0.77+ (Bridgeless Mode Enabled)
- **Compiler**: React Compiler (No more `useMemo`/`useCallback`)
- **Navigation**: `Expo Router` v5 with Typed Routes
- **Styling**: `NativeWind v5` (Tailwind CSS v4) + `react-native-unistyles`
- **Networking**: `TanStack Query v5` (Offline-first default)
- **Graphics**: `Shopify Skia` (High-performance 2D Graphics)
- **Animations**: `Reanimated 4` (Worklets + Shared Values)

### 📚 References
| Topic | Description | File |
|-------|-------------|------|
| Native Tabs | NativeTabs from expo-router, SDK 54+, iOS 26 features | [native-tabs.md](./references/native-tabs.md) |
| SF Symbols | expo-symbols for native icons, animation effects | [sf-symbols.md](./references/sf-symbols.md) |
| Glass Effects | Blur and Liquid Glass (iOS 26+) patterns | [glass-effects.md](./references/glass-effects.md) |

---

## 🏗️ Project Structure

```
src/
├── app/                    # Expo Router screens
│   ├── (auth)/            # Auth group (protected)
│   ├── (tabs)/            # Tab navigation
│   └── _layout.tsx        # Root layout
├── components/
│   ├── ui/                # Reusable UI components
│   └── features/          # Feature-specific components
├── hooks/                 # Custom hooks
├── services/              # API and native services
├── stores/                # State management (Zustand)
├── utils/                 # Utilities
└── types/                 # TypeScript types
```

### Expo vs Bare React Native

| Feature            | Expo           | Bare RN        |
| ------------------ | -------------- | -------------- |
| Setup complexity   | Low            | High           |
| Native modules     | EAS Build      | Manual linking |
| OTA updates        | Built-in       | Manual setup   |
| Build service      | EAS            | Custom CI      |
| Custom native code | Config plugins | Direct access  |

---

## 🚀 Quick Start

```bash
# Create new Expo project
npx create-expo-app@latest my-app -t expo-template-blank-typescript

# Install essential dependencies
npx expo install expo-router expo-status-bar react-native-safe-area-context
npx expo install @react-native-async-storage/async-storage
npx expo install expo-secure-store expo-haptics expo-local-authentication
```

```typescript
// app/_layout.tsx
import { Stack } from 'expo-router'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { QueryProvider } from '@/providers/QueryProvider'

export default function RootLayout() {
  return (
    <QueryProvider>
      <ThemeProvider>
        <Stack screenOptions={{ headerShown: false }}>
          <Stack.Screen name="(tabs)" />
          <Stack.Screen name="(auth)" />
          <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
        </Stack>
      </ThemeProvider>
    </QueryProvider>
  )
}
```

---

## 🧭 Expo Router Navigation

### Tab Navigator

```typescript
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
import { Home, Search, User, Settings } from 'lucide-react-native'
import { useTheme } from '@/hooks/useTheme'

export default function TabLayout() {
  const { colors } = useTheme()

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: colors.primary,
        tabBarInactiveTintColor: colors.textMuted,
        tabBarStyle: { backgroundColor: colors.background },
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) => <Search size={size} color={color} />,
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
        }}
      />
    </Tabs>
  )
}
```

### Dynamic Routes & Navigation

```typescript
// app/(tabs)/profile/[id].tsx
import { useLocalSearchParams } from 'expo-router'

export default function ProfileScreen() {
  const { id } = useLocalSearchParams<{ id: string }>()
  return <UserProfile userId={id} />
}

// Programmatic navigation
import { router } from 'expo-router'

router.push('/profile/123')
router.replace('/login')
router.back()

// With params
router.push({
  pathname: '/product/[id]',
  params: { id: '123', referrer: 'home' },
})
```

### React Navigation Stack (Alternative)

```typescript
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

type RootStackParamList = {
  Home: undefined;
  Detail: { itemId: string };
  Settings: undefined;
};

const Stack = createNativeStackNavigator<RootStackParamList>();

function AppNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          headerStyle: { backgroundColor: '#6366f1' },
          headerTintColor: '#ffffff',
          headerTitleStyle: { fontWeight: '600' },
        }}
      >
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen
          name="Detail"
          component={DetailScreen}
          options={({ route }) => ({
            title: `Item ${route.params.itemId}`,
          })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
```

---

## 🔐 Authentication Flow

```typescript
// providers/AuthProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react'
import { useRouter, useSegments } from 'expo-router'
import * as SecureStore from 'expo-secure-store'

interface AuthContextType {
  user: User | null
  isLoading: boolean
  signIn: (credentials: Credentials) => Promise<void>
  signOut: () => Promise<void>
}

const AuthContext = createContext<AuthContextType | null>(null)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const segments = useSegments()
  const router = useRouter()

  // Check authentication on mount
  useEffect(() => {
    checkAuth()
  }, [])

  // Protect routes
  useEffect(() => {
    if (isLoading) return

    const inAuthGroup = segments[0] === '(auth)'

    if (!user && !inAuthGroup) {
      router.replace('/login')
    } else if (user && inAuthGroup) {
      router.replace('/(tabs)')
    }
  }, [user, segments, isLoading])

  async function checkAuth() {
    try {
      const token = await SecureStore.getItemAsync('authToken')
      if (token) {
        const userData = await api.getUser(token)
        setUser(userData)
      }
    } catch (error) {
      await SecureStore.deleteItemAsync('authToken')
    } finally {
      setIsLoading(false)
    }
  }

  async function signIn(credentials: Credentials) {
    const { token, user } = await api.login(credentials)
    await SecureStore.setItemAsync('authToken', token)
    setUser(user)
  }

  async function signOut() {
    await SecureStore.deleteItemAsync('authToken')
    setUser(null)
  }

  if (isLoading) return <SplashScreen />

  return (
    <AuthContext.Provider value={{ user, isLoading, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (!context) throw new Error('useAuth must be used within AuthProvider')
  return context
}

---

## 🚀 Networking & Environment Configuration

### 1. Environment Variables (EXPO_PUBLIC_)
Expo supports environment variables with the `EXPO_PUBLIC_` prefix. These are inlined at build time.

```ts
// .env
EXPO_PUBLIC_API_URL=https://api.payu.fajjjar.my.id/v1
EXPO_PUBLIC_API_KEY=your_public_key

// src/types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      EXPO_PUBLIC_API_URL: string;
      EXPO_PUBLIC_API_KEY: string;
    }
  }
}

// src/config/env.ts
const API_URL = process.env.EXPO_PUBLIC_API_URL;
if (!API_URL) throw new Error("EXPO_PUBLIC_API_URL is missing");
```

> **⚠️ WARNING**: Never put secrets (private keys, DB passwords) in `EXPO_PUBLIC_` variables. They are visible in the plain-text bundle.

### 2. Robust Error Handling (ApiError)
Standardize API error parsing to handle business logic errors and network failures consistently.

```typescript
class ApiError extends Error {
  constructor(
    public message: string, 
    public status: number, 
    public code?: string
  ) {
    super(message);
    this.name = "ApiError";
  }
}

const fetchWithErrorHandling = async (url: string, options?: RequestInit) => {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new ApiError(
        error.message || "Request failed",
        response.status,
        error.code
      );
    }

    return response.json();
  } catch (error) {
    if (error instanceof ApiError) throw error;
    // Handle network errors
    throw new ApiError("Jaringan bermasalah", 0, "NETWORK_ERROR");
  }
};
```

### 3. Request Cancellation (AbortController)
Prevent state updates on unmounted components and cancel unnecessary inflight requests.

```typescript
useEffect(() => {
  const controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then(res => res.json())
    .then(setData)
    .catch(err => {
      if (err.name !== "AbortError") setError(err);
    });

  return () => controller.abort();
}, [url]);
```

---

## 🌐 Expo DOM Components (`'use dom'`)

Introduced in **Expo SDK 52**, DOM components allow rendering React DOM inside a native app via a high-performance bridge. 

### 1. When to Use
- **Complex UI**: Libraries like **D3.js**, **Three.js**, or rich-text editors (**Tiptap**).
- **Web Migration**: Incremental porting of existing React web components.
- **High-Fidelity Layouts**: When CSS Grid/Flexbox is mandatory for complex designs.

### 2. Technical Implementation
Mark the file with the `'use dom'` directive. Props are automatically serialized across the bridge.

```tsx
// src/components/WebViewChart.tsx
'use dom';

export default function WebViewChart({ 
  data, 
  onPointClick 
}: { 
  data: number[], 
  onPointClick: (val: number) => Promise<void> 
}) {
  return (
    <div className="flex h-64 bg-slate-900 items-end gap-1 p-4">
      {data.map((val, i) => (
        <button 
          key={i}
          style={{ height: `${val}%` }}
          className="bg-emerald-500 flex-1 rounded-t-sm hover:bg-emerald-400"
          onClick={() => onPointClick(val)}
        />
      ))}
    </div>
  );
}
```

### 3. Native Actions (Calling Native from DOM)
Pass `async` functions as props. They are treated as **Native Actions** that run on the native thread.

```tsx
// src/app/(tabs)/analytics.tsx
import WebViewChart from '@/components/WebViewChart';

export default function Analytics() {
  const handlePointClick = async (val: number) => {
    'use native'; // Explicitly mark native context (optional but clear)
    Alert.alert("Data Point", `Selected value: ${val}`);
  };

  return (
    <View style={{ flex: 1 }}>
      <WebViewChart 
        data={[20, 45, 80, 50, 90]} 
        onPointClick={handlePointClick}
        dom={{ scrollEnabled: false }} // WebView props
      />
    </View>
  );
}
```

### 4. Constraints
- **Asynchronous**: All communication via props is async.
- **Serialization**: Props must be JSON-serializable (no classes/functions except Native Actions).
- **Render Context**: No shared React Context/State between native and DOM components.

---

## 🛠️ Expo API Routes (`+api.ts`)

API Routes allow you to build a lightweight backend within your Expo app. They are executed on the server-side (EAS Hosting/Cloudflare Workers).

### 1. Use Cases
- **Server-side Secrets**: Hide API keys (OpenAI, Stripe) from the client bundle.
- **Webhook Endpoints**: Receive callbacks from external services (Payment providers, Auth providers).
- **Proxying**: Mask complex external API calls behind a simple endpoint.

### 2. Implementation
API routes live in the `app` directory with the `+api.ts` suffix.

```typescript
// app/api/secure-payment+api.ts
export async function POST(request: Request) {
  const { amount, currency } = await request.json();
  
  // Call internal PayU core service using server-side secret
  const response = await fetch(`${process.env.INTERNAL_URL}/payments`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.INTERNAL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ amount, currency })
  });

  const data = await response.json();
  return Response.json(data);
}
```

### 3. Middleware Pattern (requireAuth)
```typescript
// utils/api-auth.ts
export async function requireApiAuth(request: Request) {
  const token = request.headers.get("Authorization")?.replace("Bearer ", "");
  if (!token) {
    throw new Response(JSON.stringify({ error: "Unauthorized" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }
  // Verify token logic...
  return { userId: "user-123" };
}

// app/api/user-data+api.ts
import { requireApiAuth } from '@/utils/api-auth';

export async function GET(request: Request) {
  const user = await requireApiAuth(request);
  return Response.json({ id: user.userId, data: "Sensitive data" });
}
```

### 4. Deployment
Deployed as **Edge Functions** via `eas deploy`.
- Use `process.env` (NOT `EXPO_PUBLIC_`) for secrets in these files.
- Ensure `eas.json` has `server` configuration for hostings if needed.
```

---

## 📴 Offline-First with React Query

```typescript
// providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import AsyncStorage from '@react-native-async-storage/async-storage'
import NetInfo from '@react-native-community/netinfo'
import { onlineManager } from '@tanstack/react-query'

// Sync online status
onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected)
  })
})

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24, // 24 hours
      staleTime: 1000 * 60 * 5,    // 5 minutes
      retry: 2,
      networkMode: 'offlineFirst',
    },
    mutations: {
      networkMode: 'offlineFirst',
    },
  },
})

const asyncStoragePersister = createAsyncStoragePersister({
  storage: AsyncStorage,
  key: 'REACT_QUERY_OFFLINE_CACHE',
})

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister: asyncStoragePersister }}
    >
      {children}
    </PersistQueryClientProvider>
  )
}
```

### Optimistic Mutations

```typescript
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

export function useCreateProduct() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: api.createProduct,
    // Optimistic update
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries({ queryKey: ['products'] })
      const previous = queryClient.getQueryData(['products'])

      queryClient.setQueryData(['products'], (old: Product[]) => [
        ...old,
        { ...newProduct, id: 'temp-' + Date.now() },
      ])

      return { previous }
    },
    onError: (err, newProduct, context) => {
      queryClient.setQueryData(['products'], context?.previous)
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] })
    },
  })
}
```

---

## 🔐 Security (Banking Standard)

Mobile security is **non-negotiable** for PayU:

- **SecureStore**: Sensitive data (JWT, PIN hashes) MUST stay in `expo-secure-store`
- **Biometrics**: Use `expo-local-authentication` for all financial mutations
- **SSL Pinning**: Mandated for production API calls to prevent MiTM attacks
- **Anti-Log**: NEVER log PII or bearer tokens in production builds

### Biometric Authentication

```typescript
// services/biometrics.ts
import * as LocalAuthentication from 'expo-local-authentication'

export async function authenticateWithBiometrics(): Promise<boolean> {
  const hasHardware = await LocalAuthentication.hasHardwareAsync()
  if (!hasHardware) return false

  const isEnrolled = await LocalAuthentication.isEnrolledAsync()
  if (!isEnrolled) return false

  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: 'Authenticate to continue',
    fallbackLabel: 'Use passcode',
    disableDeviceFallback: false,
  })

  return result.success
}
```

---

## 🎭 Native Module Integration

### Haptic Feedback

```typescript
// services/haptics.ts
import * as Haptics from 'expo-haptics'
import { Platform } from 'react-native'

export const haptics = {
  light: () => {
    if (Platform.OS !== 'web') {
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
    }
  },
  medium: () => {
    if (Platform.OS !== 'web') {
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
    }
  },
  success: () => {
    if (Platform.OS !== 'web') {
      Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
    }
  },
  error: () => {
    if (Platform.OS !== 'web') {
      Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
    }
  },
}
```

### Push Notifications

```typescript
// services/notifications.ts
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import Constants from 'expo-constants'

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
})

export async function registerForPushNotifications() {
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
    })
  }

  const { status: existingStatus } = await Notifications.getPermissionsAsync()
  let finalStatus = existingStatus

  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync()
    finalStatus = status
  }

  if (finalStatus !== 'granted') return null

  const projectId = Constants.expoConfig?.extra?.eas?.projectId
  return (await Notifications.getExpoPushTokenAsync({ projectId })).data
}
```

---

## 🎨 Styling Patterns

### StyleSheet Basics

```typescript
import { StyleSheet, View, Text } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#ffffff',
  },
  title: {
    fontSize: 24,
    fontWeight: '600',
    color: '#1a1a1a',
    marginBottom: 8,
  },
});
```

### Dynamic Styles

```typescript
interface CardProps {
  variant: 'primary' | 'secondary';
  disabled?: boolean;
}

function Card({ variant, disabled }: CardProps) {
  return (
    <View style={[
      styles.card,
      variant === 'primary' ? styles.primary : styles.secondary,
      disabled && styles.disabled,
    ]}>
      <Text style={styles.text}>Content</Text>
    </View>
  );
}
```

### Flexbox Layouts

```typescript
const styles = StyleSheet.create({
  // Vertical stack (column)
  column: { flexDirection: 'column', gap: 12 },
  // Horizontal stack (row)
  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
  // Space between items
  spaceBetween: { flexDirection: 'row', justifyContent: 'space-between' },
  // Centered content
  centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  // Fill remaining space
  fill: { flex: 1 },
});
```

### Skia Graphics (Canvas-based UI)

```tsx
import { Canvas, Circle, Group } from "@shopify/react-native-skia";

export const HelloSkia = () => {
  const size = 256;
  const r = size * 0.33;
  return (
    <Canvas style={{ flex: 1 }}>
      <Group blendMode="multiply">
        <Circle cx={r} cy={r} r={r} color="cyan" />
        <Circle cx={size - r} cy={r} r={r} color="magenta" />
        <Circle cx={size/2} cy={size - r} r={r} color="yellow" />
      </Group>
    </Canvas>
  );
};
```

---

## 🎨 Modern Styling (Tailwind v4 + NativeWind v5)

PayU adopts the **CSS-first** approach provided by Tailwind v4 and NativeWind v5 for universal styling.

### 1. Core Stack
- **Tailwind CSS v4**: Modern CSS-first engine.
- **NativeWind v5**: Metro transformer for React Native.
- **react-native-css**: Runtime for CSS variables and platform colors.

### 2. Configuration (CSS-First)
Unlike v3, v4 does not require `tailwind.config.js`. Use `@theme` in `global.css`.

```css
/* src/styles/global.css */
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";

@layer theme {
  @theme {
    --color-bank-green: #10b981;
    --font-rounded: "SF Pro Rounded", sans-serif;
  }
}

/* Platform-specific refinements */
@media ios {
  :root {
    --sf-bg: platformColor(systemBackground);
    --sf-text: platformColor(label);
  }
}

@media android {
  :root {
    --sf-bg: #ffffff;
    --sf-text: #000000;
  }
}
```

### 3. Usage Patterns
NativeWind v5 requires components to be wrapped for `className` support. Use a shared `tw` utility or wrapped components.

```tsx
import { View, Text } from '@/components/ui/tw-wrapped';

export function PremiumCard() {
  return (
    <View className="bg-bank-green/10 p-6 rounded-3xl border border-bank-green/20">
      <Text className="text-bank-green font-bold text-xl">
        Premium Account
      </Text>
    </View>
  );
}
```

### 4. Key Differences (Migration Guide)
| Feature | Legacy (v3/v4) | Modern (v5 + TW v4) |
| :--- | :--- | :--- |
| **Config** | `tailwind.config.js` | `@theme` in CSS |
| **Babel** | `nativewind/babel` | **NONE** (Remove from babel.config) |
| **Engine** | JavaScript-heavy | CSS-first (Metro handled) |
| **Variables** | `native-wind` vars | Standard CSS Variable support |

---

## ✨ Reanimated 3 Animations

### Basic Animated Values

```typescript
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

function AnimatedBox() {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const handlePress = () => {
    scale.value = withSpring(1.2, {}, () => {
      scale.value = withSpring(1);
    });
  };

  return (
    <Pressable onPress={handlePress}>
      <Animated.View style={[styles.box, animatedStyle]} />
    </Pressable>
  );
}
```

### Gesture Handler Integration

```typescript
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

function DraggableCard() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  const gesture = Gesture.Pan()
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Text>Drag me!</Text>
      </Animated.View>
    </GestureDetector>
  );
}
```

### Pressable Button with Animation

```typescript
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

export function Button({ title, onPress }: ButtonProps) {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const handlePressIn = () => {
    scale.value = withSpring(0.95);
    if (Platform.OS !== 'web') {
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    }
  };

  const handlePressOut = () => {
    scale.value = withSpring(1);
  };

  return (
    <AnimatedPressable
      onPress={onPress}
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      style={[styles.button, animatedStyle]}
    >
      <Text style={styles.text}>{title}</Text>
    </AnimatedPressable>
  );
}
```

---

## ⚡ Performance Optimization

### FlashList for Long Lists

```typescript
// components/ProductList.tsx
import { FlashList } from '@shopify/flash-list'
import { memo, useCallback } from 'react'

// Memoize list item
const ProductItem = memo(function ProductItem({
  item,
  onPress,
}: {
  item: Product
  onPress: (id: string) => void
}) {
  const handlePress = useCallback(() => onPress(item.id), [item.id, onPress])

  return (
    <Pressable onPress={handlePress} style={styles.item}>
      <FastImage source={{ uri: item.image }} style={styles.image} />
      <Text style={styles.title}>{item.name}</Text>
    </Pressable>
  )
})

export function ProductList({ products, onProductPress }: ProductListProps) {
  const renderItem = useCallback(
    ({ item }: { item: Product }) => (
      <ProductItem item={item} onPress={onProductPress} />
    ),
    [onProductPress]
  )

  return (
    <FlashList
      data={products}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      estimatedItemSize={100}
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={5}
    />
  )
}
```

---

## 📦 EAS Build & Submit

```json
// eas.json
{
  "cli": { "version": ">= 5.0.0", "appVersionSource": "remote" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": { "simulator": true }
    },
    "preview": {
      "distribution": "internal",
      "android": { "buildType": "apk" }
    },
    "production": {
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {
      "ios": { "appleId": "your@email.com", "ascAppId": "123456789" },
      "android": { "serviceAccountKeyPath": "./google-services.json" }
    }
  }
}
```

### 1. Expo Go vs. Dev Client
- **Expo Go**: Use for rapid prototyping and basic UI work.
- **Dev Client**: MANDATORY when using local Expo modules, custom native code, or third-party native libraries not present in Expo Go.

### 2. Build for TestFlight (Cloud)
Build and submit in a single flow:
```bash
eas build -p ios --profile development --submit
```

### 3. Build Locally (Faster Iteration)
Requires local build environment (Xcode for iOS, Android Studio for Android).
```bash
eas build -p ios --profile development --local
```

### 4. Running the Dev Client
```bash
npx expo start --dev-client
```

```bash
# Build commands
eas build --platform ios --profile development
eas build --platform android --profile preview
eas build --platform all --profile production

# Submit to stores
eas submit --platform ios
eas submit --platform android

# OTA updates
eas update --branch production --message "Bug fixes"
```

---

## 🎨 Luxury Native UI (Emerald SDK)

- **Glassmorphism**: Use `BlurView` or `GlassView` for overlay cards
- **Micro-interactions**: Subtle `Pressable` scaling and staggered list entry animations
- **Context Menus**: Native `Link.Menu` (iOS) for quick actions on list items
- **Continuous Curves**: Use `borderCurve: 'continuous'` (iOS) for smooth "Premium" corners

---

## ✅ Best Practices

### Do's
- **Use Expo** - Faster development, OTA updates, managed native code
- **FlashList over FlatList** - Better performance for long lists
- **Memoize components** - Prevent unnecessary re-renders
- **Use Reanimated** - 60fps animations on native thread
- **Test on real devices** - Simulators miss real-world issues
- **TypeScript**: Define navigation and prop types for type safety
- **Handle Safe Areas**: Use `SafeAreaView` or `useSafeAreaInsets`

### Don'ts
- **Don't inline styles** - Use StyleSheet.create for performance
- **Don't fetch in render** - Use useEffect or React Query
- **Don't ignore platform differences** - Test on both iOS and Android
- **Don't store secrets in code** - Use environment variables
- **Don't skip error boundaries** - Mobile crashes are unforgiving
- **Never use ScrollView with map** - Use FlatList/FlashList for lists

---

## 🐛 Common Issues

| Issue | Solution |
|-------|----------|
| Gesture Conflicts | Wrap gestures with `GestureDetector` and use `simultaneousHandlers` |
| Navigation Type Errors | Define `ParamList` types for all navigators |
| Animation Jank | Move animations to UI thread with `runOnUI` worklets |
| Memory Leaks | Cancel animations and cleanup in useEffect |
| Font Loading | Use `expo-font` or `react-native-asset` for custom fonts |
| Safe Area Issues | Test on notched devices (iPhone, Android with cutouts) |

---

## 🔍 Quality Checklist

- [ ] **Touch Targets**: Are all buttons at least 44x44 points?
- [ ] **Safe Areas**: Does it handle dynamic notches and home indicators?
- [ ] **Performance**: Does `renderItem` use memoization?
- [ ] **Security**: Are auth tokens stored in SecureStore?
- [ ] **Offline**: Does the app work without network?
- [ ] **Platform Testing**: Tested on both iOS and Android?

---

## 📚 Resources

- [Expo Documentation](https://docs.expo.dev/)
- [Expo Router](https://docs.expo.dev/router/introduction/)
- [React Native Performance](https://reactnative.dev/docs/performance)
- [FlashList](https://shopify.github.io/flash-list/)
- [Reanimated Documentation](https://docs.swmansion.com/react-native-reanimated/)
- [Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/)

---
*Last Updated: 2026-05-04*

