Team evidence · noindex

Architecture & data flow

How does the application actually work?

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

Loading diagram…

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

Key / locationShapeOwner moduleNotes
AsyncStorage persist:rootredux-persist blobsrc/redux/store.jsWhitelist: userDetails, badgeInfo, appStateInfo. Theme is not persisted.
userDetails.user inside persistLogin data + later space fieldsLogin + SpaceSelectionSource of truth for session, event, space, timezone, excel password, report emails
userDetails.token inside persist{ token }UnusedstoreUserToken is defined and never dispatched
badgeInfo.badgeInformationBadgeData[] synced/valid (and some “final” restricted rows from bulk sync)Scan + syncDevice vault for “online” rows
badgeInfo.badgeInformationOfflineBadgeData[] queueScan + syncDevice vault for unsynced
badgeInfo.totalCount / unsyncCountnumbersScan + syncDerived counters. Home UI recounts from arrays. Camera/Dashboard still increment these. Drift risk.
appStateInfo.isFirstLaunchonboarding flagOnboarding / AuthNavigationFirst-launch onboarding
appStateInfo.selectedClientId'adnec' | 'dmg' | 'xporience' | 'terrapinn' | missingClientSelectScreenChooses API host. Missing after upgrade with a session → treat as DMG (old hardcoded URL) and persist. Missing with no session → picker.
AsyncStorage userTokenstring bearer (intended)src/api/index.tsx reads; many screens removeTrail: never written. API still sends Authorization: Bearer ${token} which is empty
App Documents *_YYYY.xlsxExcel fileHomeHeader / downloadCSV / exportAllSpaceDataNot a database; local file
In-memory onlytheme, DataWedge profile, last-scan dedupe refsTheme reducer / dataWedgeConfig / scan hooksLost on kill except DataWedge profile on device

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

  1. LoginScreen.handleLogin or Login QR → same handler
  2. isInternetConnected (NetInfo)
  3. getUniqueId + sign(HS256) with active client secret (getActiveSecretKey) → { operation: 'userLogin', EPTOKEN }
  4. loginAPI → API('POST', '', data) in src/api/index.tsx (host from selected client)5. On 200 + data: dispatch(storeUserDetails(loginData)) with space fields nulled
  5. AsyncStorage.removeItem('userToken') — does not save a session bearer
  6. PersistGate writes persist:root asynchronously (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)

  1. SpaceSelectionScreen getSpaceList JWT { event_id, device_id }
  2. User tap → if current space has offline rows, syncAllOfflineBadges (inline generateLead loop)
  3. dispatch(storeUserDetails(updatedUser)) with space_id, space_name, type, assigned_badges
  4. 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

Loading diagram…

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 === 1 on 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

SurfaceDirectionMechanism
Zebra DataWedgeNative → JSBroadcast datawedge_broadcast_intent; profile BadgeScannerApp created at app start
CameraNative → JSvision-camera code scanner
MailJS → Nativereact-native-mail
ExcelJS → filesystemreact-native-blob-util
Wear / widgets / Live Activities—Not present

DataWedge is event-driven (push), not poll.

Network layer (all APIs)

OperationJWT claimsUsed by
userLoginusername, password, device_idLoginScreen
getSpaceListevent_id, device_idSpaceSelectionScreen
generateLeadbadge + event + type + times + user + space + deviceScan + sync

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

LayerWhat is validated
UILogin blanks; badge charset/length; space required
Domain (processBadgeScan)Duplicate today+space for Visitor; status matrix
StoreAlmost none — arrays replaced wholesale
PersistenceNone — JSON blob
NetworkBackend status + sync_status

Freshness guarantees

DataGuarantee
Redux session / badgesReactive in-app; persist is async
Space listOne-shot fetch; stale until screen refetch
isOnline on HomeNetInfo + Google 204 ping, debounced 10s — can be stale vs real Wi-Fi
Login internet checkImmediate NetInfo.fetch (different helper than scan)
Server leadsNever pulled back. Device cannot show “what backend stored” except via the last response

Where the trail went cold

These are not guesses. They are gaps:

  1. 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.
  2. userToken write path — no setItem('userToken') anywhere. Bearer header is always empty unless something outside the repo writes it.
  3. Whether DashBoardScreen is launched by a deep link or old APK — no in-repo navigation. Could still exist on old builds.
  4. assigned_badges usage after space select — stored on user; not used in processBadgeScan (no local allow-list check found).
  5. redux-persist corruption / migration — no versioned migrations found.
  6. Exact login response schema — typed in redux/types but 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.