Skip to Content
MobileMisc

Misc

This guide will help you understand codebase & configuration for mobile and some common errors.

Basic Foundation

Core Technologies:

  • Expo SDK 54 (~54.0.27) with React Native 0.81.5 and React 19.1.0.
  • New Architecture enabled via newArchEnabled: true in app.config.ts.
  • TypeScript for end-to-end type safety.
  • NativeWind 4.2.1 with TailwindCSS 4.0.6 for utility-first styling.
  • Jotai for atomic state management and selective re-rendering.
  • React Navigation 7.x (native-stack + bottom-tabs) for app navigation.
  • React Query (TanStack Query) via jotai-tanstack-query integration.
  • oRPC contract-first API layer with full type safety.
  • Firebase (@react-native-firebase/* v22.4.0) for Analytics, Crashlytics, Messaging, and Remote Config.
  • MMKV (react-native-mmkv ~3.3.3) for encrypted key-value storage.
  • Vision Camera (react-native-vision-camera ^4.7.3) for card scanning and ML-powered capture flows.
  • Flash List (@shopify/flash-list 2.0.2) for high-performance lists.
  • Reanimated (react-native-reanimated ~4.2.0) for performant animations.

Project Structure:

  • app/: Main app screens/pages.
  • app/{feature}/_atoms/: Feature-specific atoms (queries, mutations, ui, effects, forms, plus feature-specific extras like types/mutations-optimistic).
  • components/: Reusable UI components.
  • db/: SQLite database layer (Drizzle ORM, schema, queries, loaders).
  • lib/: Core utilities and configurations.
    • lib/atoms/: Global app-wide atoms (profile, onboarding, force-update, notifications).
    • lib/store/shared/: Cross-feature shared atoms (game, user, price, misc).
    • lib/store/utils/: Atom utilities (atomWithSwr, atomWithDebouncedMutation, debounced field helpers, atom compare/debounce helpers).
    • lib/network/: Network status monitoring and offline handling.
    • lib/storage.tsx: MMKV storage wrapper and JSON helpers.
    • lib/api/: oRPC client setup and configuration.
    • lib/i18n/locales/: i18n language files.
    • lib/react-query/: Query utilities and React Query provider setup.
  • navigators/: Navigation setup and routing.
  • types/: TypeScript type definitions.
  • utils/: Helper functions and utilities.

The project uses React Navigation rather than expo-router for mobile-specific navigation control.

Setup CI/CD

The CI/CD setup for this project leverages GitHub Actions to automate the build and deployment process. Here’s a brief overview of the configuration:

GitHub Actions Workflow

The project uses a composite GitHub Action defined in .github/actions/eas-build/action.yml to handle the build process for different environments (development, staging, production).

Key Inputs:

  • APP_ENV: Specifies the environment for the build (development, staging, production).
  • AUTO_SUBMIT: Determines if the build should be automatically submitted to app stores.
  • EXPO_TOKEN: Required token for accessing the Expo account.
  • VERSION: The version of the app to be built.
  • ANDROID and IOS: Flags to trigger builds for Android and iOS platforms respectively.

Build Steps:

  1. Pre-Build Script: Generates necessary native folders based on the APP_ENV.
  2. Platform-Specific Builds: Executes builds for Android and iOS based on the provided flags.

Scripts in package.json

The package.json includes several scripts to facilitate the build and deployment process:

  • Build Commands:
    • build:staging:ios and build:staging:android: Build the app for staging environment.
    • build:prod:ios and build:prod:android: Build the app for production environment.
  • Update Commands:
    • app:update:staging: Updates the app on the staging channel.
    • app:update:prod: Updates the app on the production channel.

These scripts are integrated into the CI/CD pipeline to ensure seamless deployment and updates across different environments.

Styling with NativeWind

The project uses NativeWind for utility-class styling in React Native.

Configuration

  1. Tailwind Configuration
// @ts-nocheck const colors = require("./src/components/ui/colors") /** @type {import('tailwindcss').Config} */ module.exports = { // NOTE: Update this to include the paths to all of your component files. content: ["./src/**/*.{js,jsx,ts,tsx}"], presets: [require("nativewind/preset")], darkMode: "false", theme: { extend: { fontFamily: { mulish: ["Mulish-Regular"], mulishBold: ["Mulish-Bold"], mulishExtraBold: ["Mulish-ExtraBold"], mulishMedium: ["Mulish-Medium"], mulishSemiBold: ["Mulish-SemiBold"], }, colors, spacing: { 0: "0px", 0.5: "2px", 1: "4px", 1.5: "6px", 2: "8px", 2.5: "10px", 3: "12px", 3.5: "14px", 3.75: "15px", 4: "16px", 5: "20px", 6: "24px", 7: "28px", 8: "32px", 9: "36px", 10: "40px", 11: "44px", 12: "48px", 14: "56px", 16: "64px", 20: "80px", 24: "96px", 28: "112px", 32: "128px", 36: "144px", 40: "160px", 44: "176px", 48: "192px", 52: "208px", 56: "224px", 60: "240px", 64: "256px", 68: "272px", 72: "288px", 80: "320px", 96: "384px", }, }, }, plugins: [], }
  1. TypeScript Support

The nativewind-env.d.ts file provides NativeWind TypeScript support.

/// <reference types="nativewind/types" />

Important Note to Ensure Monorepo Compatibility

Metro Configuration

Use the real monorepo-aware Metro config from apps/mobile/metro.config.js:

const { getDefaultConfig } = require("expo/metro-config") const { withNativeWind } = require("nativewind/metro") const path = require("node:path") // Find the project and workspace directories const projectRoot = __dirname // This can be replaced with `find-yarn-workspace-root` const monorepoRoot = path.resolve(projectRoot, "../..") const config = getDefaultConfig(projectRoot) // 1. Watch all files within the monorepo config.watchFolders = [monorepoRoot] // 2. Let Metro know where to resolve packages and in what order config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, "node_modules"), path.resolve(monorepoRoot, "node_modules"), ] // 3. Enable symlink support for pnpm config.resolver.unstable_enableSymlinks = true config.resolver.unstable_enablePackageExports = true // 3b. Block web-only packages hoisted at monorepo root config.resolver.blockList = [ /.*\/node_modules\/@storybook\/.*/, /.*\/node_modules\/storybook\/.*/, /.*\/node_modules\/@chromatic-com\/.*/, /.*\/apps\/storybook\/.*/, /.*\/apps\/admin\/node_modules\/.*/, ] // 4. Add extra node modules for workspace package resolution config.resolver.extraNodeModules = { "react-native-card-scanner": path.resolve( monorepoRoot, "packages/mobile/react-native-card-scanner", ), } config.resolver.sourceExts.push("sql") config.resolver.sourceExts.push("cjs") // Add transformer config to handle import.meta polyfill config.transformer = { ...config.transformer, unstable_transformImportMeta: true, } // Add .pte (PyTorch ExecuTorch) and .mdb (ObjectBox database) as asset extensions config.resolver.assetExts.push("pte") config.resolver.assetExts.push("mdb") module.exports = withNativeWind(config, { input: "./global.css" })

Local Database (SQLite + Drizzle)

Mobile uses a local SQLite database (cn.db) via expo-sqlite, with Drizzle ORM (drizzle-orm/expo-sqlite) for type-safe queries.

  • WAL mode and tuned pragmas are applied at startup for better write/read performance.
  • Writes are serialized via runDbWrite() to avoid overlapping SQLite writes.
  • Full-text search is supported with FTS5 virtual tables in the database schema/migrations.
  • Migrations are run at app startup via useMigrations(SQLiteDB, migrations) in AppNavigator.
export const dbName = "cn.db" export const db = SQLite.openDatabaseSync(dbName, { enableChangeListener: true, }) // Enable WAL mode for better concurrency (critical for multi-game updates) db.execSync("PRAGMA journal_mode = WAL") // Set synchronous mode to NORMAL for faster writes while maintaining data integrity db.execSync("PRAGMA synchronous = NORMAL") // Set cache size to 64MB for better query performance db.execSync("PRAGMA cache_size = -64000") // Store temporary tables and indices in memory for better performance db.execSync("PRAGMA temp_store = MEMORY") // Enable memory-mapped I/O for better read performance (256MB) db.execSync("PRAGMA mmap_size = 268435456") // Optimize page size for mobile devices (4KB is optimal for most mobile storage) db.execSync("PRAGMA page_size = 4096") // Enable foreign keys db.execSync("PRAGMA foreign_keys = ON") // Wait briefly for locked databases instead of failing immediately. db.execSync("PRAGMA busy_timeout = 5000") // Serialize write transactions to avoid overlapping SQLite writes. let writeQueue: Promise<void> = Promise.resolve() export async function runDbWrite<T>(task: () => Promise<T>): Promise<T> { let release: (() => void) | undefined const next = new Promise<void>((resolve) => { release = resolve }) const previous = writeQueue writeQueue = previous.then(() => next) await previous try { return await task() } finally { release?.() } }

MMKV Storage

The app uses an encrypted MMKV instance in src/lib/storage.tsx:

  • MMKV instance is encrypted with key "cn".
  • Helpers: getItem, setItem, readJsonSync, writeJsonSync, removeItem, deleteKey.
  • Jotai persistence uses atomWithMMKVStorage from src/lib/jotaiStorage.ts.

Common persisted data includes auth-related values, user preferences, scanned-card history, and CDN data versions.

export const storage = new MMKV({ id: "storage", encryptionKey: "cn", }) export const atomWithMMKVStorage = <T>(key: string, initialValue: T) => { return atomWithStorage<T>(key, initialValue, jotaiStorage as SyncStorage<T>) }

Network & Offline Support

Offline behavior is centered in src/lib/network/:

  • isDefinitelyOfflineAtom provides conservative offline detection.
  • networkRecoveryVersionAtom increments on offline -> online transitions.
  • Query atoms depend on networkRecoveryVersionAtom and gate on online status.
  • Active queries are refetched on reconnect, and paused mutations are resumed.
  • OfflineBanner (src/components/offline-banner/offline-banner.tsx) displays connectivity status in-app.
Last updated on