Skip to main content

Architecture & Navigation

The Certi-Lock® mobile app is built with Expo Router for file-based navigation, React Native for cross-platform rendering, and a provider-based architecture for global state and configuration.


Directory Structure

scottsdale-mint-mobile-app/
├── app/ # Expo Router screens (file-based routing)
│ ├── _layout.tsx # Root layout with providers and header
│ ├── index.tsx # Home/Dashboard screen
│ ├── scan.tsx # QR/Barcode scanner screen
│ ├── history.tsx # Scanned items history list
│ ├── details/
│ │ └── [id]/
│ │ ├── index.tsx # Item verification/details screen
│ │ └── sealed.tsx # View sealed image screen
│ └── +not-found.tsx # 404 fallback page
├── api/ # API integration functions
│ ├── client.ts # React Query client configuration
│ ├── get-data.ts # Algolia product lookup
│ ├── get-image.ts # Image API + SHA256 hashing
│ ├── get-items.ts # SQLite query (all items)
│ ├── get-item-by-id.ts # SQLite query (single item)
│ ├── save-item.ts # Insert scanned item + image
│ ├── update-item.ts # Update item with fresh data
│ ├── delete-item.ts # Delete item + cached image
│ ├── get-banners.ts # Promotional banner fetch (24h cache)
│ ├── get-fallback-image.ts # Fallback image by metal type/shape
│ └── log-error.ts # WordPress error logging
├── components/ # Reusable React Native components
├── db/ # Database schema and configuration
│ └── schema.ts # Drizzle ORM table definitions
├── drizzle/ # Database migrations
│ ├── 0000_*.sql # Initial schema
│ ├── 0001_*.sql – 0004_*.sql # Incremental migrations
│ └── meta/ # Migration metadata
├── modules/ # Custom native modules
│ └── vision-camera-qr/ # QR/barcode frame processor
│ ├── ios/ # Swift native implementation
│ └── android/ # Kotlin native implementation
├── assets/ # Static assets
│ ├── fonts/ # Montserrat + Playfair Display families
│ └── images/ # Fallback images, icons, splash
├── utils/ # Utility functions
├── global.css # NativeWind global styles
├── app.json # Expo app configuration
├── eas.json # EAS build profiles
├── metro.config.js # Metro bundler configuration
├── babel.config.js # Babel transpilation configuration
├── tailwind.config.js # Tailwind CSS theme configuration
├── drizzle.config.ts # Drizzle ORM configuration
└── tsconfig.json # TypeScript configuration

The app uses Expo Router for file-based routing. Each file in the app/ directory maps directly to a route:

Route                    Screen              Description
─────────────────────────────────────────────────────────────
/ Home/Dashboard Promotional banners, quick actions
/scan QR Scanner Camera-based barcode/QR scanning
/history Item History Searchable list of scanned items
/details/[id] Item Details Product verification and specs
/details/[id]/sealed Sealed Image Full-resolution sealed image view

Root Layout (app/_layout.tsx)

The root layout wraps the entire app with required providers and configures the navigation header:

// Provider hierarchy (outermost to innermost)
<Sentry.wrap>
<SQLiteProvider databaseName="db.db" onInit={migrateDb}>
<QueryClientProvider client={queryClient}>
<GestureHandlerRootView>
<SafeAreaProvider>
<Stack screenOptions={{ header: CustomHeader }}>
{/* Screen routes */}
</Stack>
<Toast />
</SafeAreaProvider>
</GestureHandlerRootView>
</QueryClientProvider>
</SQLiteProvider>
</Sentry.wrap>

Provider responsibilities:

ProviderPurpose
Sentry.wrapError boundary and performance monitoring
SQLiteProviderExpo SQLite database connection with Drizzle migration on init
QueryClientProviderTanStack React Query cache and mutation management
GestureHandlerRootViewGesture handling (swipe-to-delete, touch events)
SafeAreaProviderSafe area insets for notches and system UI

Custom Header

The app uses a custom header component rendered across all screens with context-sensitive navigation:

  • Left: Barcode scanner icon — links to /scan
  • Center: Certi-Lock logo — links to / (home)
  • Right: History icon — links to /history
  • Dropdown Menus: Context-specific actions per screen (powered by zeego)

Data Flow

Scan-to-Verify Flow

The primary user flow follows this sequence:

┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌───────────────┐
│ /scan │───►│ saveItem() │───►│ /details/[id]│───►│ /details/ │
│ Camera │ │ API + DB │ │ Verification │ │ [id]/sealed │
└──────────┘ └──────────────┘ └───────────────┘ └───────────────┘
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Algolia API │ │
│ │ Image API │ │
│ │ SQLite Save │ │
│ └──────────────┘ │
│ │
▼ ▼
Frame processor Auto-update if older
decodes QR at 1 FPS than 24 hours

Step-by-step:

  1. Camera Activation/scan requests camera permission and activates React Native Vision Camera
  2. Frame Processing — Custom native plugin visionCameraQr processes frames at 1 FPS to decode QR/barcodes
  3. Duplicate Check — Checks SQLite for existing serial number; shows toast if duplicate
  4. Product Lookup — Calls Algolia API with the first 4 characters of the serial number as a filter
  5. Image Retrieval — Fetches sealed image from the Certi-Lock Image API
  6. Image Hashing — Generates SHA256 hash of the image using expo-crypto for local file naming
  7. Local Storage — Saves product data to SQLite and image to app document directory
  8. Navigation — Redirects to /details/[id] to display the verified item

Auto-Update Flow

Items older than 24 hours are automatically refreshed when viewed:

  1. Details screen checks lastUpdatedId against current value
  2. If stale, calls updateItem() to re-fetch from Algolia and Image API
  3. Updates SQLite record and cached image file
  4. React Query invalidates related queries for UI refresh

Promotional banners follow a cache-first strategy:

  1. Check AsyncStorage for cached banners with timestamp
  2. If cache is less than 24 hours old, use cached data
  3. Otherwise, fetch fresh banners from WordPress REST API
  4. Store new data in AsyncStorage with current timestamp
  5. Tapping a banner opens the linked URL in the device browser

React Query Configuration

The app uses TanStack React Query for all server and database state:

// Query key conventions
["banners"] // Promotional banners
["items", searchTerm] // Item list (filtered by search)
["item", id] // Single item by ID

Mutation behavior:

  • saveItem — Invalidates ["items"] on success, shows toast on duplicate
  • updateItem — Invalidates ["items"] and ["item", id] on success
  • deleteItem — Invalidates ["items"] on success, removes cached image file

TypeScript Configuration

{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": ["./*"]
}
}
}
  • Strict mode enabled for full type safety
  • Path alias @/ resolves to the project root for clean imports (e.g., @/api/get-data)

Error Handling

Errors are handled at multiple levels:

LayerStrategy
SentryAutomatic crash reporting and performance tracing for unhandled exceptions
React QueryonError callbacks for API and mutation failures
Toast MessagesUser-facing error/success notifications via react-native-toast-message
WordPress LoggingCritical errors posted to WordPress REST API via log-error.ts
Try/CatchIndividual API functions wrapped with error handling and fallback values