Architecture and data flow
Audit date: 19 Aug 2026. This is a map of the current codebase, not a proposal.
This app is React Native. State is Redux + redux-persist, not Zustand. Persistence is AsyncStorage only. There is no SQLite and no MMKV. There are no Wear OS, Live Activities, Dynamic Island, or widgets.
Layer map
Auth gate: src/Routes/index.tsx — if selectedClientId is missing, show client picker; else if userDetails.user is truthy, show app routes; else auth routes.
API host + JWT secret: src/config/clients.ts (baseUrl, secretKey per client). src/api/index.tsx reads store.getState().appStateInfo.selectedClientId at request time for the URL. Signing uses getActiveSecretKey() / getSecretKeyFromClientId. There is no compile-time BASE_URL for live requests.
Shared request headers include User-Agent: AccessControlApp/1.0 (ReactNative) so hosts that block default OkHttp UA (HTML 403) still reach the JSON API.
Persistent storage table
No other stores were found (no SecureStore, no SQLite).
Feature: Login
Source of truth: Redux userDetails.user after success. Backend is truth for whether login is allowed. Phone is truth for who is logged in on this device.
Write path
LoginScreen.handleLoginor Login QR → same handlerisInternetConnected(NetInfo)getUniqueId+sign(HS256)with active client secret (getActiveSecretKey) →{ operation: 'userLogin', EPTOKEN }loginAPI→API('POST', '', data)insrc/api/index.tsx(host from selected client)5. On 200 +data:dispatch(storeUserDetails(loginData))with space fields nulledAsyncStorage.removeItem('userToken')— does not save a session bearer- PersistGate writes
persist:rootasynchronously (redux-persist, no transaction)
Read path: Routes selector is reactive. Space/Home read user.data.* from Redux. Freshness = persisted snapshot until next login/space change. Not refetched on resume.
Validation: UI empty/space checks. Backend 400/409/500. getLoginErrorMessage strips technical 500 text.
Failure: No session write. App kill mid-persist can leave a partial persist blob (redux-persist limitation). No rollback.
Sync/native: None.
Cold trail: interceptor.js attaches Bearer from userDetails.user.token but interceptor is never registered in index.js / App.tsx. Dead code.
Feature: Space selection
Source of truth: Space list is a component cache (spacesCacheRef) for the session. Selected space is copied onto userDetails.user.data (Redux persist). Backend is truth for the list.
Write path (select)
SpaceSelectionScreengetSpaceListJWT{ event_id, device_id }- User tap → if current space has offline rows,
syncAllOfflineBadges(inlinegenerateLeadloop) dispatch(storeUserDetails(updatedUser))with space_id, space_name, type, assigned_badges- Navigate Home
Read path: One-shot fetch on screen focus. Not a Redux subscription for the list. Selected space is reactive via Redux.
Validation: Filter rows that lack space_name. No schema library.
Feature: Badge scan (two live paths)
Path A — Home laser (Zebra DataWedge) — primary floor path
Files: App.tsx configureDataWedge → HomeScreen useScanner → useBadgeScanHandler.handleBadgeScan → HomeScreen/services/badgeService.processBadgeScan → badgeScanDecision.ts.
Source of truth after a scan
- Backend: whether the badge is valid (
sync_status === 1on 200) - Phone: the two arrays in
badgeInfo(what this device saw)
Duplication risk: Camera path writes the same arrays.
Write path: validate alphanumeric → duplicate-today for Visitor → if offline, saveOffline → else JWT generateLead → status matrix → storeBadgeDetails or storeBadgeDetailsOffline.
Read path: Home counts useMemo over arrays filtered by badgeBelongsToActiveSession (session user + current client) + current space + today. Reactive Redux. Guaranteed as fresh as last dispatch + persist flush.
Propagation: No widget. Excel/email read the same arrays later.
Validation: UI regex; domain duplicate; backend 403/404/sync_status.
Failure: Transport errors save offline and still toast success. No DB transaction. Kill after Redux dispatch but before persist flush can lose the last scan.
Path B — Camera (SCANNER_SCREEN)
HomeScreen navigates to SCANQR/ScanQR.tsx → useBadgeScanner → SCANQR/services/badgeService.uploadBadge.
This is a second implementation of generateLead handling. It uses HTTP 503 where Home uses 801. Duplicate-today is checked after the API on camera. Charset differs (@ allowed on Home).
Path C — DashBoardScreen laser
Registered in AppRoutes but nothing navigates to it. Dead UI with a third scan implementation. LaserScan.tsx is also not on the stack.
Do not assume Path C is live.
Feature: Offline sync
Source of truth: badgeInformationOffline until a bulk loop moves rows.
Write path (header): HomeHeader.bulkSyncUploadBadges — sequential generateLead per row for current space.
Write path (space change): SpaceSelectionScreen.syncAllOfflineBadges — copy of the same idea, not a shared function.
Read: Redux subscription. Not a poll. User-triggered (refresh icon / space change).
Failure: Partial success is kept. 409 saves progress then logs out. No single HTTP batch API.
Feature: Export and email
Source of truth: Same badge arrays. Not the server.
Write: XLSX to DocumentDir. Email via Mailer.mail with recipients from login payload (email_id_for_checkin_data).
No network write of the report to our API.
Feature: Logout
getLogoutGuardResult in logoutGuard.ts. If allowed: remove userToken, logoutUser() (user+token null). Badge arrays remain. Next login with another account filters via badgeBelongsToActiveSession (user_id / event_id / client_id). Legacy rows with both user ids missing are treated as belonging to the current session. Legacy rows with no client_id are treated as DMG.
Native / platform
DataWedge is event-driven (push), not poll.
Network layer (all APIs)
Timeout default 10s; scan upload 3s (SCAN_UPLOAD_TIMEOUT_MS).
Headers: Content-Type, Accept, fixed User-Agent: AccessControlApp/1.0 (ReactNative), optional Authorization from AsyncStorage userToken (usually empty).
API() catches and rethrows with statusCode. loginAPI / uplaodBadgesAPI catch and return the error object. Callers must inspect res.response. Easy to mis-handle.
Validation boundaries
Freshness guarantees
Where the trail went cold
These are not guesses. They are gaps:
- Backend OpenAPI / real status catalog — not in this repo. Mobile comments mention 801; camera/sync mention 503. Which one the server emits is unknown here.
userTokenwrite path — nosetItem('userToken')anywhere. Bearer header is always empty unless something outside the repo writes it.- Whether
DashBoardScreenis launched by a deep link or old APK — no in-repo navigation. Could still exist on old builds. assigned_badgesusage after space select — stored on user; not used inprocessBadgeScan(no local allow-list check found).- redux-persist corruption / migration — no versioned migrations found.
- Exact login response schema — typed in
redux/typesbut optional fields may exist on the wire that mobile ignores.
If you need intent behind duplicate scan engines or 801 vs 503, ask — do not assume a single “correct” status until backend confirms.