Code Review — Dashboard Mobile App (apps/dashboard-mobile)
Date: 2025-07
Reviewer: GitHub Copilot
Scope: Full source review of apps/dashboard-mobile/src/
Stack: Expo 55, React Native 0.83.4, React 19, TypeScript, TanStack Query v5, react-native-sse, Expo SecureStore, expo-local-authentication
Summary
The mobile app is a React Native / Expo client for the Leadmetrics dashboard. It covers home stats, activities, approvals, leads, AI chat, and notifications. The auth architecture is solid — JWT tokens in SecureStore, transparent refresh via a request-serialising singleton, and a reducer-based AuthContext. The test suite has good breadth (unit + component tests for nearly every screen).
All critical, major, and moderate issues from the original review have been resolved. One minor cosmetic issue remains before production builds.
Minor Issues
MIN-5: app.json missing app icon
File: app.json
The Android adaptiveIcon has only backgroundColor set, and no foregroundImage. The iOS icon field and the root icon field are both absent. This will use Expo’s default icon for any EAS build or native run, making the app indistinguishable from the template on a device. A proper icon should be added before any production release.
Positive Observations
-
Token storage in SecureStore — Tokens use
expo-secure-storerather thanAsyncStorage, giving hardware-backed encryption on both iOS (Keychain) and Android (Keystore). The right choice for JWT storage on mobile. -
Refresh deduplication — The module-level
refreshPromisesingleton inclient.tsensures concurrent 401 responses trigger exactly one refresh rather than a race condition that would invalidate each other’s tokens. Well-implemented. -
Request timeout in
mobileFetch—AbortControllerwith a 30s default timeout prevents stalled connections from hanging the UI indefinitely on poor mobile networks. -
Biometric unlock via
AuthContext.unlockSession()— Successful biometric auth callsunlockSession()which dispatchesRESTORE_SESSION, causingRootNavigatorto transition toAppTabscorrectly. -
useInfiniteQueryfor paginated lists — BothActivitiesScreenandLeadsScreenuse cursor-based infinite scroll withgetNextPageParam,onEndReached, andListFooterComponentloading indicators. -
queryClient.tsoffline caching — MMKV-backed persistence with a 24hgcTimeand amaxAgepurge on the persister gives good offline UX without unbounded cache growth. The exclusion ofnotificationsfrom the persisted cache is correct (stale notification badges are confusing). -
SSE stream cleanup —
sseRef.current?.close()before creating a new SSE connection insendMessage()correctly prevents abandoned streams from accumulating. TheisStreamingguard also prevents double-sends. -
Good test coverage for screens — Nearly every screen has a corresponding test file. The
client.test.tscovers the happy path, 401 refresh, and SESSION_EXPIRED scenarios. This is notably better test discipline than the web apps. -
Zod + react-hook-form on LoginScreen — Proper schema validation with field-level error display and a Zod resolver, rather than manual
if (!email)checks.
Issue Summary
| ID | Severity | File(s) | Description | Status |
|---|---|---|---|---|
| MIN-5 | Minor | app.json | Missing app icon (icon, adaptiveIcon.foregroundImage) | Open |