Bilko Mobile Implementation Spec — Phase 1

Bilko Mobile Companion — Phase 1 Implementation Spec

Document type: Implementation Specification (Phase 1) MC task: #102483 Author: Paul Hudson / Skybound Date: 2026-05-28 Status: Direction approved; not dispatch-ready until Phase 0 auth/backend blockers are closed Updated: 2026-06-04 Depends on: docs/mobile/MOBILE-ARCHITECTURE.md, docs/mobile/MOBILE-PRD.md


1. Technology Decision

1.1 Framework — React Native with Expo (Managed Workflow)

Decision: React Native + Expo SDK (managed workflow first, bare only if forced)

Rationale grounded in the existing Bilko codebase:

Not chosen: Flutter. Dart is a cold start for the team. No design system reuse. Separate CI pipeline. No business justification at current headcount. Not chosen: Native Swift/Kotlin. Two codebases. No shared types or API client. Onerous CI/code-signing setup for MVP.

1.2 Navigation — Expo Router v4 (file-based, built on React Navigation v7)

Expo Router is the canonical navigation layer in Expo SDK 52+. It is file-system-based (mirrors Next.js App Router conventions the team already knows), supports typed routes, and integrates deep links and universal links via its built-in linking config. It wraps React Navigation v7 internally.

Directory layout under apps/mobile/app/ maps directly to routes — (auth)/login.tsx, (tabs)/today/index.tsx, etc. No separate navigator configuration file is needed for the basic shell; the file tree is the configuration.

Do NOT use React Navigation v7 standalone (without Expo Router). The file-based approach is preferred.

1.3 State Management — Zustand (mirror web stores)

The web app already has Zustand 4.5.0 installed and uses typed stores in apps/web/lib/stores/. Mobile should mirror the same store shape for dashboard, invoices, expenses, and auth.

Pattern: one store file per domain slice — src/store/authStore.ts, src/store/dashboardStore.ts, src/store/invoiceStore.ts, src/store/expenseStore.ts, src/store/settingsStore.ts.

Stores hold: remote data state, loading/error flags, optimistic update helpers, and (Phase 2) sync queue references. They do NOT hold tokens — tokens live in Keychain only (see section 2.3).

Alternatives considered and rejected:

1.4 API Client — Typed fetch wrapper (adapt apps/web/lib/api.ts)

The web app's api.ts is a useful reference for endpoint shape, error handling, and multipart upload, but mobile must not reuse browser-only cookie/session assumptions. The web client is a typed fetch wrapper around getApiBaseUrl() with:

Mobile should reproduce the endpoint/error/upload pattern in src/api/client.ts with these differences:

  1. Token/session material is loaded from expo-secure-store on app launch and stored back on refresh/session update.
  2. credentials: 'include' cookie refresh is not used in React Native.
  3. Customer login uses Microsoft Entra External ID via OIDC Authorization Code + PKCE (expo-auth-session) and then exchanges/validates the Entra token with Bilko backend for Bilko org/role context.

API base URL resolution: always https://api.bilko.cloud/api/v1 (production) or an env-var override. The window.location hostname trick used in the web api-base.ts does not apply in React Native. Use EXPO_PUBLIC_API_URL instead.

Library: native fetch (available in React Native). No axios. No ky. These are unnecessary given the thin abstraction already proven in the web codebase.

1.5 Token Storage — expo-secure-store

Entra/Bilko access, ID, refresh/session tokens only in expo-secure-store.

Reasoning: expo-secure-store maps to iOS Keychain Services and Android Keystore under the hood. It satisfies the hard security requirement in MOBILE-ARCHITECTURE.md section 10: "tokens only in Keychain/Keystore-backed secure storage." AsyncStorage is plaintext on disk and is forbidden for token storage.

Key names:

On cold start the auth store reads from SecureStore; on logout it wipes all auth keys.

react-native-keychain is an alternative but it requires native code changes and does not work in the Expo managed workflow without a custom dev client build. expo-secure-store is the managed-workflow-safe choice.

1.6 Camera and OCR Library — expo-camera + expo-image-manipulator

expo-camera is the managed workflow camera primitive. It handles permission requests, preview, and photo capture on both iOS and Android. Phase 1 uses it only for still photo capture.

expo-image-manipulator (also managed) is used to resize images to max 1920px on the longest dimension and compress to JPEG ≤ 0.85 quality before upload. This is the only Phase 1 image processing step; no on-device OCR.

expo-document-picker handles the PDF/file import path from the share sheet (Phase 2, but the library is added to the Phase 1 scaffold so it does not trigger a native rebuild later).

1.7 Localization — expo-localization + i18n-js (mirror web message files)

The web app has five locale files: bs.json, en.json, hr.json, sr-Cyrl.json, sr-Latn.json. These files use next-intl conventions with namespaced keys.

Mobile should import the same message JSON files via a shared package (packages/i18n/) or by symlinking apps/web/messages/ for Phase 1. The locale is detected via expo-localization (device locale) and can be overridden in Settings.

Library: i18n-js (maintained, small, works in RN) or react-i18next (heavier but matches the pattern if the team wants parity with web). Recommendation: i18n-js for Phase 1 to keep the bundle lean.

Market detection for feature gating (HR-only travel orders): driven by organization.country from GET /auth/me, not by device locale.


2. Project Structure

2.1 apps/mobile/ Directory Layout

apps/mobile/
├── app/                              # Expo Router routes (file = route)
│   ├── (auth)/
│   │   ├── _layout.tsx              # Auth stack layout (no tab bar)
│   │   └── login.tsx                # Login screen
│   ├── (tabs)/
│   │   ├── _layout.tsx              # Tab bar layout (authenticated shell)
│   │   ├── today/
│   │   │   └── index.tsx            # Dashboard / Today screen
│   │   ├── invoices/
│   │   │   ├── index.tsx            # Invoice list
│   │   │   └── [id].tsx             # Invoice detail
│   │   ├── expenses/
│   │   │   ├── index.tsx            # Expense list / recent activity
│   │   │   └── scan.tsx             # Camera capture screen (modal)
│   │   └── more/
│   │       └── index.tsx            # Profile / Settings / Logout
│   ├── travel-order/
│   │   └── new.tsx                  # Travel order wizard (HR only — gated)
│   └── _layout.tsx                  # Root layout (font loading, splash guard)
├── src/
│   ├── api/
│   │   ├── client.ts                # Base fetch wrapper (mirrors web api-base + api.ts)
│   │   ├── auth.ts                  # /auth/* endpoint methods
│   │   ├── dashboard.ts             # /reports/dashboard + /mobile/dashboard
│   │   ├── invoices.ts              # /invoices/* endpoint methods
│   │   ├── expenses.ts              # /expenses/* endpoint methods
│   │   └── travel-orders.ts        # /travel-orders/* (HR only)
│   ├── components/
│   │   ├── ui/                      # Bilko design-system primitives (Button, Card, Badge, etc.)
│   │   ├── InvoiceCard.tsx
│   │   ├── ExpenseCard.tsx
│   │   ├── DashboardSummary.tsx
│   │   ├── CountryBadge.tsx         # Currency + country label display
│   │   └── SyncStatusBar.tsx        # Phase 2 — placeholder only in Phase 1
│   ├── hooks/
│   │   ├── useAuth.ts               # Read auth store + token helpers
│   │   ├── useDashboard.ts
│   │   ├── useInvoices.ts
│   │   ├── useExpenses.ts
│   │   └── useCountryConfig.ts      # Reads org.country → feature flags
│   ├── store/
│   │   ├── authStore.ts             # JWT tokens, user profile, org metadata
│   │   ├── dashboardStore.ts
│   │   ├── invoiceStore.ts
│   │   └── expenseStore.ts
│   ├── services/
│   │   ├── tokenService.ts          # SecureStore read/write/clear for tokens
│   │   ├── imageService.ts          # Resize + compress before upload
│   │   └── countryService.ts        # Country config → feature gate helpers
│   ├── i18n/
│   │   ├── index.ts                 # i18n-js setup + locale detection
│   │   └── messages/                # Symlink to apps/web/messages/ or copy
│   ├── types/
│   │   ├── api.ts                   # Shared API response types (copied from web where stable)
│   │   └── country.ts               # CountryMobileConfig (from MOBILE-ARCHITECTURE.md)
│   └── utils/
│       ├── currency.ts              # Format currency by country (EUR/RSD/BAM)
│       ├── date.ts                  # Date formatting helpers
│       └── validation.ts            # Form field validators
├── assets/
│   ├── images/
│   │   ├── icon.png                 # 1024x1024 app icon (Bilko "B" logo)
│   │   └── splash.png               # 1284x2778 splash screen
│   └── fonts/
│       ├── WorkSans-*.ttf           # Body font (mirrors web)
│       └── NationalPark-*.otf       # Heading font (mirrors web)
├── app.config.ts                    # Expo config (bundle IDs, env vars, plugins)
├── eas.json                         # EAS Build profiles (dev/staging/production)
├── package.json
└── tsconfig.json                    # Extends @alai/tsconfig

Key structural choices:

2.2 Configuration and Environment Variables

Expo uses EXPO_PUBLIC_* prefix for variables baked into the JS bundle at build time.

EXPO_PUBLIC_API_URL=https://api.bilko.cloud/api/v1
EXPO_PUBLIC_APP_ENV=production
EXPO_PUBLIC_SENTRY_DSN=...

All three market environments (HR/BA/RS) use the same API URL (api.bilko.cloud). Market behavior is driven by organization.country returned from /auth/me after login — not by build-time environment variables. There is no per-market build variant at the API URL level.

Build variants (from MOBILE-ARCHITECTURE.md section 12):

EAS Profile Bundle ID API URL Analytics
development no.alai.bilko.dev http://localhost:4000/api/v1 (or staging) off
staging no.alai.bilko.staging https://api-stage.bilko.cloud/api/v1 limited
production no.alai.bilko https://api.bilko.cloud/api/v1 consent-based

Bundle IDs are defined in app.config.ts via process.env.APP_ENV switching. Secrets are injected via EAS Secrets (never committed).

2.3 Localization File Strategy

The web app ships five message files under apps/web/messages/:

Phase 1 approach: copy these files into src/i18n/messages/ at scaffold time and commit them alongside the mobile app. They will diverge from the web copies as mobile-specific strings are added (tab labels, camera permission prompts, etc.). A shared packages/i18n/ extraction is planned for Phase 3 when the divergence cost justifies the shared package overhead.

Locale detection order: device locale via expo-localization → org language preference from /auth/meen fallback.


3. Phase 1 Screens (MVP)

Screen 1: Splash / Auto-login Check

Purpose: entry point, decides whether to route to Login or Today tab.

Behavior:

  1. App launches, splash screen is visible (native splash via expo-splash-screen).
  2. tokenService.ts reads Bilko/Entra token state from SecureStore.
  3. If no valid session/refresh token: hide splash, navigate to /(auth)/login.
  4. If a valid session/refresh token exists: refresh through the Entra/Bilko auth bridge. On success: store updated token state, hide splash, navigate to /(tabs)/today. On failure: clear all tokens, navigate to /(auth)/login.
  5. Maximum wait: 5 seconds; show error and navigate to login on timeout.

Implementation note: this logic lives in app/_layout.tsx as a root-level effect, not a dedicated screen component. expo-splash-screen is kept visible until the auth check resolves.

Screen 2: Login

Route: /(auth)/login

Fields:

Behavior:

Design: full-screen, centered card. Bilko plum #8B6BBF primary button. Logo at top. Work Sans body font.

Screen 3: Dashboard (Today Tab)

Route: /(tabs)/today/index

Data sources:

Displayed sections:

  1. Greeting header ("Dobar dan, [name]" / localized)
  2. Revenue this month — formatted in org currency (EUR/RSD/BAM)
  3. Expenses this month — formatted in org currency
  4. Unpaid invoices — count + total amount
  5. Top 3 unpaid invoices by amount — InvoiceCard component (tap → invoice detail)
  6. "Capture expense" floating action button → navigates to /(tabs)/expenses/scan

Loading state: skeleton placeholders for all cards (no spinner). Error state: inline "Unable to load — tap to retry" per section. Offline state: show cached data with "Last updated [time]" indicator (Phase 1 cache is in-memory store only; full SQLite cache is Phase 2).

Country-specific behavior:

Screen 4: Invoice Scan (Camera Capture)

Route: /(tabs)/expenses/scan (presented as a modal)

Flow:

  1. Request camera permission via expo-camera (permission rationale: "Bilko treba kameru za snimanje računa").
  2. Camera preview fills screen. Shutter button at bottom centre.
  3. On capture: preview captured image + confirm/retake buttons.
  4. On confirm: imageService.ts resizes to max 1920px longest side, JPEG quality 0.85.
  5. Show "Add expense details" form: description (required), amount (numeric), date (defaults today), category (select).
  6. "Save" calls POST /api/v1/expenses (JSON body) then POST /api/v1/expenses/{id}/documents (multipart with the compressed image).
  7. On success: show brief success toast, dismiss modal, refresh expense list.
  8. On upload failure: show error with "Retry" option. Do not discard the image or the draft expense ID.

Phase 1 note: this is capture-and-upload, not OCR extraction. The backend stores the image as scan_pending. Amount and description are entered manually by the user.

Screen 5: Travel Order Quick-Add (HR Only)

Route: /travel-order/new (presented as a modal, not a tab)

Gate: only reachable if useCountryConfig().country === 'HR'. The "Travel Order" entry point is hidden in the More tab for RS/BA users. No navigation guard is sufficient — the screen itself also checks the gate and shows an error if reached by URL manipulation.

Three-step wizard:

Step 1 — Basic details:

Step 2 — Allowances:

Step 3 — Review + Submit:

This mirrors the web "putni-nalozi" wizard reduced to 3 steps. Full expense attachment for receipts is Phase 2.

Screen 6: Recent Activity (Expenses Tab)

Route: /(tabs)/expenses/index

Displays last 10 invoices and expenses interleaved by date (most recent first).

Data:

Each row shows: type icon, description/contact name, amount in org currency, date, status badge.

Tap on invoice row → /(tabs)/invoices/[id] (invoice detail view, read-only in Phase 1). Tap on expense row → expense detail (read-only in Phase 1 — full edit is Phase 2).

FAB at bottom right: "Scan expense" → navigates to camera capture screen.

Screen 7: Profile / Settings (More Tab)

Route: /(tabs)/more/index

Sections:

  1. User info: full name, email, organization name, country flag + name
  2. App info: app version (from expo-constants), environment (staging/production)
  3. Language override: select from [hr, bs, sr-Latn, sr-Cyrl, en]
  4. "Log out" button (destructive red)

Logout behavior:

  1. Call POST /api/v1/auth/logout (best effort — do not block logout on API failure).
  2. Clear bilko_access_token, bilko_refresh_token, bilko_token_expires_at from SecureStore.
  3. Clear all Zustand stores.
  4. Navigate to /(auth)/login.

4. API Integration

4.1 Auth Flow

Microsoft Entra External ID OIDC Authorization Code + PKCE
  app:     expo-auth-session starts hosted login
  success: Entra authorization code/token response

POST /api/v1/auth/entra/session   (Phase 0 backend bridge; see `docs/backend/MOBILE-ENTRA-AUTH-BRIDGE-SPEC.md`)
  body:    { idToken/accessToken or authorizationCode exchange result }
  success: { user, organization, tokens/session: { accessToken, refreshOrSessionToken?, expiresIn } }
  → tokenService.store(...)
  → authStore.setUser(user), authStore.setOrg(organization)

POST /api/v1/auth/entra/refresh or Entra SDK/session refresh
  body:    implementation-specific; no httpOnly browser cookie dependency
  success: { accessToken, expiresIn }
  → tokenService.updateAccessToken(accessToken)

POST /api/v1/auth/logout
  body:    {}  (access token in Authorization header)
  → tokenService.clear()
  → optionally revoke Bilko session / Entra session where supported

The current Kotlin/Ktor web auth flow is cookie-oriented: login/register set an httpOnly refreshToken cookie and /auth/refresh reads call.request.cookies["refreshToken"]. Mobile must not depend on that browser cookie path. Phase 0 must add/confirm the Entra External ID bridge and Bilko user/org mapping before Slice A is dispatched. Backend contract: docs/backend/MOBILE-ENTRA-AUTH-BRIDGE-SPEC.md.

4.2 Automatic 401 Retry

client.ts intercepts 401 responses identically to the web api.ts:

  1. Call tokenService.getRefreshToken().
  2. If present: call /auth/refresh. On success: update stored access token, retry original request once.
  3. On refresh failure: call authStore.logout() → clears tokens + navigates to login.
  4. Guard: prevent infinite loop on /auth/refresh itself responding 401.

4.3 User Profile and Org Info

GET /api/v1/auth/me
  Authorization: Bearer <accessToken>
  returns: {
    user: { id, email, fullName, role },
    organization: { id, name, country, baseCurrency, language, vatNumber }
  }

Called once after login and stored in authStore. organization.country drives all country-gating logic (HR-only features, currency display, PDV/VAT labels).

4.4 Invoice List

GET /api/v1/invoices?limit=10&status=&sort=created_desc
  Authorization: Bearer <accessToken>
  returns: { data: Invoice[], total: number, page: number }

Phase 1 uses the existing /api/v1/invoices endpoint (same as web). A dedicated /mobile/invoices BFF endpoint (listed in MOBILE-ARCHITECTURE.md section 6) is desirable for Phase 2 to return a mobile-optimized projection. Phase 1 maps the existing response shape in src/types/api.ts.

4.5 Expense Upload (Multipart)

Step 1: POST /api/v1/expenses
  body (JSON): { description, amount, date, category, currency }
  returns: { id, ... }

Step 2: POST /api/v1/expenses/{id}/documents
  body (multipart/form-data): file = <compressed JPEG>
  returns: { uploaded, documentId, url, fileName, message }

The web expenses.uploadDocument() already implements this exact two-step pattern. Mobile src/api/expenses.ts replicates it using FormData with { uri, name, type } format for React Native's fetch multipart encoding.

Image size constraint: imageService.ts enforces max 1920px before upload. If the resulting JPEG exceeds 5 MB (unusual), a second compression pass at 0.7 quality is applied. The backend imposes a 10 MB limit on the documents endpoint.

4.6 Travel Orders (HR Only)

POST /api/v1/travel-orders
  Authorization: Bearer <accessToken>
  body: {
    destination: string,
    purpose: string,
    departureDate: string,   // ISO date
    returnDate: string,      // ISO date
    dailyAllowanceRate: number,
    numberOfDays: number,
    advancePayment?: number,
    currency: "EUR"           // HR always EUR
  }
  returns: { id, orderNumber, status }

If the /travel-orders endpoint does not yet exist in the Kotlin/Ktor backend, this is a backend dependency for Slice D. The slice cannot be completed until the backend endpoint is available. Backend team must confirm endpoint availability or create a stub before Slice D dispatch.


5. Camera and OCR Plan

Phase 1 (this spec — Slices A–F)

Phase 2 (separate MC — not in this spec)

Backend-side OCR candidates:

The choice depends on data residency requirements (GDPR, BA data sovereignty), per-page cost budget, and accuracy threshold for HR/RS/BA merchant receipt formats. This decision requires a separate product + legal review. Defer to Phase 2 MC.

Fiken pattern alignment

Fiken's "bare ta bilde med appen, vi foreslår hvordan det skal registreres" pattern is the target UX for Phase 2+. In Phase 1 the equivalent copy is: "Snimite račun odmah, unesite iznos, sinkroniziramo s računovođom" (capture now, enter amount, syncs to accountant).


6. Native Features

Biometric Login (Phase 2)

expo-local-authentication provides LocalAuthentication.authenticateAsync() for FaceID / TouchID on iOS and BiometricPrompt on Android.

Phase 1: not implemented. The setting does not appear in the More tab. Phase 2: add "Enable biometric login" toggle in Settings. On enable: store a biometric-protected flag in SecureStore. On app foreground after lock timeout: prompt biometric before showing sensitive screens. Token is not re-issued — biometric unlocks the in-memory auth state only.

Push Notifications (Deferred — Phase 3)

Phase 1 does not request notification permission, register push tokens, or wire notification handlers. There is no confirmed backend device-token endpoint yet, and prompting users before useful notifications exist is bad UX.

Phase 3 may use expo-notifications or direct FCM/APNs after product/security confirms provider choice and backend device-token storage.

Offline Queue (Phase 2)

Phase 1 has no offline queue. If the user is offline:

Phase 2 adds SQLite via expo-sqlite and the sync queue described in MOBILE-ARCHITECTURE.md sections 7 and 8.


7. Deployment

iOS — TestFlight via EAS Build

  1. EAS Build runs on Expo infrastructure (no local Mac required).
  2. eas.json defines three profiles: development (simulator build), staging (ad-hoc distribution for internal testers), production (App Store / TestFlight).
  3. Code signing: Distribution Certificate + Provisioning Profile stored in EAS credentials manager. Alem provides Apple Developer account credentials once.
  4. TestFlight submission: eas submit --platform ios --profile staging after a successful staging build.
  5. Phase 1 does not auto-submit to TestFlight from CI. The developer triggers eas submit manually after reviewing the build artifact.

Android — Internal Track via EAS Build + Play Console

  1. EAS Build produces an AAB (Android App Bundle).
  2. Upload artifact to Google Play Console Internal Testing track manually for Phase 1.
  3. Phase 1 does not use eas submit --platform android automatically. Manual upload to Play Console.
  4. Signing key: generated by EAS and stored in EAS credentials (not in the repo).

CI — GitHub Actions

Phase 1 CI runs on every push to feature/bilko-mobile-* and main branches:

jobs:
  mobile-check:
    runs-on: ubuntu-latest
    steps:
      - TypeScript type check (tsc --noEmit)
      - ESLint
      - Vitest unit tests (src/services/, src/store/, src/utils/)
      - EAS Build trigger for staging profile (manual workflow_dispatch only in Phase 1)

No auto-submit to TestFlight or Play Console in Phase 1. CI produces a build artifact URL. Full auto-submit pipeline is Phase 3 (after E2E tests are in place).


8. Implementation Slices

All slices are independent dispatch units. Each has a clear done-state, measurable acceptance test, and a named API dependency that must be confirmed green before the slice starts.

Slice A — Expo Scaffold + Login + JWT Auth

Scope:

Done when: a developer can run the app on iOS Simulator, complete Entra External ID login with a real Bilko staging account, be taken to a blank /(tabs)/today placeholder screen, and log out successfully. Tokens/session material confirmed in SecureStore (not AsyncStorage/Zustand).

API dependencies: Entra tenant/app registration + Bilko backend auth bridge for token validation, user/org provisioning, role mapping, and session/API token issuance.

Slice B — Dashboard + Invoice/Expense Read-Only Lists

Scope:

Done when: authenticated user sees live data from staging API on the dashboard and can scroll the invoice/expense lists. HR company shows EUR, RS shows RSD, BA shows BAM.

API dependencies: GET /api/v1/reports/dashboard, GET /api/v1/invoices, GET /api/v1/expenses, GET /api/v1/auth/me.

Slice C — Invoice Scan (Camera + Upload as Expense)

Scope:

Done when: user can point camera at a paper invoice, confirm the photo, enter description + amount + date, save, and see the expense appear in the list with a document attachment. Verified on both iOS Simulator and Android Emulator.

API dependencies: POST /api/v1/expenses, POST /api/v1/expenses/{id}/documents (multipart). Both are live in the existing backend.

Slice D — Travel Order Quick-Add (HR Only)

Scope:

Done when: HR-country test account can complete the 3-step wizard and receive a travel order number. RS/BA accounts do not see the entry point. Verified that navigating directly to the route on a non-HR account shows a clear "not available" message.

API dependencies: POST /api/v1/travel-orders — backend must confirm this endpoint exists or create a stub. This is the highest-risk dependency for Phase 1; flag to backend team immediately.

Slice E — i18n + Market Detection + Settings Screen

Scope:

Done when: switching language in Settings immediately re-renders all visible text without app restart. All five locales render without missing key warnings. Currency format is correct per org country independent of device locale.

API dependencies: none beyond GET /api/v1/auth/me (already in Slice B).

Slice F — TestFlight Build + Internal Share

Scope:

Done when: at least two internal testers (Alem + one developer) have installed the app via TestFlight on iPhone and via Play Console on Android, completed the login flow, and confirmed the dashboard loads.

API dependencies: staging API must be reachable from TestFlight/emulator devices.


9. Out of Scope — Phase 1

The following are explicitly deferred. They must not be partially implemented in Phase 1 slices:


10. Risks and Mitigations

Risk Likelihood Impact Mitigation
Entra External ID bridge is not implemented in Bilko backend High Blocker for Slice A Phase 0 backend task: Entra app registration, token validation, user/org mapping, role mapping, session/API token issuance, logout/revocation behavior.
/travel-orders endpoint does not exist in Kotlin/Ktor backend High Blocks Slice D File backend ticket immediately. Slice D cannot start until endpoint is confirmed or stubbed.
Token accidentally written to AsyncStorage by a library (e.g., React Native MMKV default) Low Critical (security) Audit all third-party library storage usage. No AsyncStorage import in src/services/tokenService.ts. CI lint rule to flag AsyncStorage imports outside explicitly allowed files.
Image upload exceeds backend/documented size limit on a high-res device Medium User-facing error imageService.ts enforces max 1920px + second-pass compression if result > 5 MB. Align backend/docs limit before Slice C; current backend evidence showed 20 MB while older docs say 10 MB.
Travel order screen reachable by RS/BA users via direct URL Low UX confusion Screen-level guard in /travel-order/new.tsx checks org.country and renders "not available" fallback. Navigation entry point also gated.
Expo managed workflow missing a required native capability Low Forces bare ejection Maintain a list of required capabilities before Slice A scaffold. Current Phase 1 requirements (camera, secure store, image manipulator, localization) are all available in managed SDK 52.
expo-secure-store value size limit (2 KB on some Android versions) Low Truncated token Access tokens (JWTs) are typically 600–900 bytes. Refresh tokens are similar. Both fit within the 2 KB limit. Monitor token size from backend.
Bilko design tokens (colors, fonts) not available as an npm-importable package Medium Visual inconsistency Phase 1: copy tokens from apps/web/tailwind.config.ts into src/components/ui/tokens.ts manually. Phase 2: extract packages/design-tokens/ shared package.
HR/RS/BA locale text not reviewed by native speakers Medium Credibility risk in demo Dževad Jahić (Lexicon) must sign off on BS strings before Slice F / TestFlight build. HR strings reviewed by HR market advisor.

11. Backend Dependencies (Pre-Dispatch Checklist)

Before any slice is dispatched, the backend team must confirm the following:

# Dependency Required for Status
B1 Microsoft Entra External ID tenant/app registration for Bilko mobile/web customer login Slice A Required Phase 0
B2 Bilko backend auth bridge validates Entra token and maps/creates Bilko user + organization + role Slice A Required Phase 0
B3 Bilko API/session token issuance works without browser httpOnly refresh-cookie dependency for React Native Slice A Required Phase 0
B4 GET /auth/me returns organization.country and organization.baseCurrency Slice B Likely exists — confirm field names
B5 GET /reports/dashboard field names match mobile dashboard mapping (cashBalance, revenueMTD, etc.) Slice B Existing backend uses web field names — adapt mobile
B6 POST /expenses/{id}/documents accepts multipart { uri, name, type } format from React Native fetch Slice C Likely exists (web uses it) — confirm RN FormData compat
B7 POST /travel-orders endpoint exists with the field schema in section 4.6 Slice D Exists in Kotlin/Ktor; confirm mobile payload shape

12. Open Architecture Questions (Carry Forward to Phase 2)

These are not blockers for Phase 1 but must be resolved before Phase 2 dispatch:

  1. Should the mobile app consume the existing /api/v1/* endpoints or new /mobile/* BFF endpoints? The architecture doc recommends /mobile/* for projection efficiency. Phase 1 uses existing endpoints as a pragmatic shortcut. A decision is needed before Phase 2 to avoid building on the wrong base.
  2. Which OCR provider for Phase 2? Requires product + legal + cost review (GDPR, BA data residency, per-page pricing).
  3. Local database encryption approach under Expo constraints (expo-sqlite + SQLCipher or plaintext with field-level encryption for sensitive columns).
  4. Maximum local retention period for cached financial data and attachment files (legal/data retention policy).
  5. Expo Notifications vs direct FCM/APNs for Phase 3. Expo Notifications is faster but less flexible for compliance markets that may require local notification routing.

Revision #1
Created 2026-06-07 19:43:19 UTC by John
Updated 2026-06-07 19:43:20 UTC by John