React Native + Expo (SDK 57) tenant portal: rent payments, maintenance ticketing, notices and lease details. Styled with NativeWind, navigated with Expo Router.
| Concern | Choice |
|---|---|
| Runtime | Expo SDK 57 · React Native 0.86 · React 19.2 |
| Navigation | Expo Router (stack + bottom tabs, typed routes) |
| Styling | NativeWind 4.2 (Tailwind 3.4) with dark: variants |
| Data | Axios + useQuery / useMutation hooks (no query library) |
| Session | expo-secure-store (tokens) + AsyncStorage (cached profile) |
| Media | expo-image-picker (camera + library, up to 4 photos) |
src/
app/ # Expo Router file-based routes
_layout.tsx # Providers, splash gate, root stack, auth guard
index.tsx # Entry redirect (dashboard vs sign-in)
sign-in.tsx # Email/password auth
(tabs)/
_layout.tsx # Bottom tab navigator (5 tabs)
dashboard.tsx # Rent status, quick actions, active requests
payments.tsx # Billing history + filters
maintenance.tsx # Ticket list + status filters
notices.tsx # Announcement feed
profile.tsx # Profile & lease details
ticket/
new.tsx # Create ticket (modal): category, priority, photos
[id].tsx # Ticket detail + activity timeline
checkout.tsx # Mock secure payment gateway (modal)
receipt/[id].tsx # Invoice / receipt
api/ # One module per resource; all HTTP lives here
client.ts # Axios instance, auth header, refresh, ApiError
auth.ts dashboard.ts payments.ts tickets.ts announcements.ts profile.ts
hooks/
use-query.ts # Loading / error / refresh / refetch-on-focus
use-mutation.ts # Writes with pending + error state
use-dashboard.ts use-tickets.ts use-payments.ts ...
providers/auth-provider.tsx # Session restore, sign in/out, 401 handling
components/ # Feature components (rent card, ticket row, ...)
ui/ # Card, Badge, Button, Field, Chip, Screen, states
constants/
config.ts # API URL resolution
domain.ts # Status → label/colour metadata (single source)
lib/ # storage.ts, format.ts
types/api.ts # Wire types mirroring the API
api/never imports fromcomponents/. Screens depend on hooks, hooks depend on services, services depend on the client. Swapping the transport touches one file.- Status metadata lives in
constants/domain.ts. A badge, a filter chip and a timeline row cannot drift apart on wording or colour. useQuerytakes a memoised fetcher as its dependency. Wrap the call inuseCallbackand the query re-runs exactly when its inputs change.
A root stack owns the auth screen, the tab group and the modal/detail
screens; the (tabs) group owns the five primary destinations.
Stack (root)
├── index → redirect
├── sign-in → headerShown: false
├── (tabs) → Dashboard · Rent · Requests · Notices · Profile
├── ticket/new → modal
├── ticket/[id] → pushed detail
├── checkout → modal
└── receipt/[id] → pushed detail
The guard lives in src/app/_layout.tsx: it holds the splash screen until the
stored session is restored, then bounces between /sign-in and /dashboard.
npm install
npm start # then i / a, or scan with a dev buildPoint the app at the API with EXPO_PUBLIC_API_URL:
# .env
EXPO_PUBLIC_API_URL=http://192.168.1.9:4000When it is unset the app derives the host from the Metro connection, so a phone
on the same Wi-Fi reaches the dev machine without configuration. localhost
only works on a simulator.
The API is expected at <origin>/api and must be reachable for anything past
the sign-in screen.
npm start # Metro
npm run ios # native build + run
npm run android
npm run lint # ESLint (expo config)
npx tsc --noEmit # typecheck
npx expo-doctor # dependency/config validationexpo-secure-storehas no web implementation; the storage adapter falls back to AsyncStorage forexpo start --web. A browser build shipping to users should use the API's httpOnly cookie flow instead.- Photo uploads are capped at 4 images / 5MB each and compressed to
quality: 0.7before upload. - The payment screen is a mock gateway. The request → confirm → receipt flow is
what survives swapping in a real provider; only the
pay()call changes.