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
Navigation Structure
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:
| Provider | Purpose |
|---|---|
Sentry.wrap | Error boundary and performance monitoring |
SQLiteProvider | Expo SQLite database connection with Drizzle migration on init |
QueryClientProvider | TanStack React Query cache and mutation management |
GestureHandlerRootView | Gesture handling (swipe-to-delete, touch events) |
SafeAreaProvider | Safe 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:
- Camera Activation —
/scanrequests camera permission and activates React Native Vision Camera - Frame Processing — Custom native plugin
visionCameraQrprocesses frames at 1 FPS to decode QR/barcodes - Duplicate Check — Checks SQLite for existing serial number; shows toast if duplicate
- Product Lookup — Calls Algolia API with the first 4 characters of the serial number as a filter
- Image Retrieval — Fetches sealed image from the Certi-Lock Image API
- Image Hashing — Generates SHA256 hash of the image using
expo-cryptofor local file naming - Local Storage — Saves product data to SQLite and image to app document directory
- Navigation — Redirects to
/details/[id]to display the verified item
Auto-Update Flow
Items older than 24 hours are automatically refreshed when viewed:
- Details screen checks
lastUpdatedIdagainst current value - If stale, calls
updateItem()to re-fetch from Algolia and Image API - Updates SQLite record and cached image file
- React Query invalidates related queries for UI refresh
Banner Flow
Promotional banners follow a cache-first strategy:
- Check AsyncStorage for cached banners with timestamp
- If cache is less than 24 hours old, use cached data
- Otherwise, fetch fresh banners from WordPress REST API
- Store new data in AsyncStorage with current timestamp
- 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 duplicateupdateItem— Invalidates["items"]and["item", id]on successdeleteItem— 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:
| Layer | Strategy |
|---|---|
| Sentry | Automatic crash reporting and performance tracing for unhandled exceptions |
| React Query | onError callbacks for API and mutation failures |
| Toast Messages | User-facing error/success notifications via react-native-toast-message |
| WordPress Logging | Critical errors posted to WordPress REST API via log-error.ts |
| Try/Catch | Individual API functions wrapped with error handling and fallback values |