React Native (Expo) mobile app for Boame — a service marketplace that connects customers to registered providers for domestic and commercial services, starting with car wash and home cleaning.
Built by Asante as a full-stack portfolio project. Pairs with the Boame API.
Customers browse services, pick a provider, book and pay via Mobile Money or card (Paystack), then track the job and chat in realtime. Providers manage bookings, offer services, view earnings, and message customers. Role-based navigation switches the entire shell between customer and provider experiences after login.
| Login | Register |
|---|---|
![]() |
![]() |
| Home | Bookings | Booking detail |
|---|---|---|
![]() |
![]() |
![]() |
| Chat | Profile | Leave a review |
|---|---|---|
![]() |
![]() |
![]() |
| View review |
|---|
![]() |
| Dashboard | My services | Add service |
|---|---|---|
![]() |
![]() |
![]() |
| Layer | Technology | Why |
|---|---|---|
| Framework | Expo SDK 56 + React Native | Fast iteration, EAS builds, native modules when needed |
| Language | TypeScript | Shared types with the API domain model |
| Navigation | React Navigation 7 | Stack + bottom tabs, deep linking (boame://) |
| Server state | TanStack Query | Caching, mutations, and loading/error UX for API calls |
| Client state | Zustand + AsyncStorage | Persisted auth session |
| Forms | React Hook Form + Zod | Schema-first validation aligned with the API |
| HTTP | Axios | JWT attach + global 401 logout |
| Realtime | Socket.io client | Chat and unread message sync |
| Auth extras | expo-auth-session + expo-web-browser | Google OAuth and Paystack checkout |
| UI | Custom theme + Lucide icons | Brand fonts (Abres, Montserrat), light/dark themes |
App.tsx
├── ThemeProvider
├── QueryClientProvider
├── SocketProvider ← connects when JWT is present
└── RootNavigator
├── Auth (no token)
├── Customer tabs (role !== provider)
└── Provider tabs (role === provider)
UI → hooks (TanStack Query) → services (Axios) → Boame API
↓
SocketContext → Socket.io (chat / unread)
↓
Zustand stores (auth, unread)
- Email/password register → email verification → login
- Google Sign-In via Expo Auth Session
- JWT stored in Zustand, persisted with AsyncStorage
- Automatic
Authorizationheader on every request - Global logout on
401responses - Deep links for auth screens (
boame://login,register,verify-email)
- Home: categories and services catalogue
- Provider listing per service, then book with date/time and location
- Paystack checkout opened in an in-app browser session
- Bookings list with status filters and detail screens
- Leave reviews after completed jobs
- Realtime chat with providers
- Profile + Become a Provider onboarding
- Dashboard for incoming and active bookings (confirm / start / complete / cancel)
- Manage offered services (add, edit, pricing)
- Earnings / transaction history
- Shared messages stack with unread badge
- Profile (hidden tab, opened from header)
- Socket authenticated with JWT on connect
- Conversation list + chat thread UI
- Unread badge on the Messages tab (Zustand + conversation sync)
- Light and dark palettes in
constants/theme.ts - Custom fonts loaded before splash hide
- Status bar follows theme
| Role | Tabs |
|---|---|
| Customer | Home · Bookings · Messages · (Profile hidden) |
| Provider | Dashboard · Services · Earnings · Messages · (Profile hidden) |
Stack navigators nest under tabs for detail flows (booking detail, chat, book service, add/edit service, reviews, become a provider, etc.).
client/
├── App.tsx — providers, fonts, splash
├── app.json / eas.json — Expo config + EAS build profiles
├── assets/ — icons, splash, fonts
├── components/
│ ├── chat/ — bubble, input, header, date separator
│ └── shared/ — buttons, inputs, cards, badges, headers
├── constants/ — API URLs, theme tokens
├── context/ — ThemeContext, SocketContext
├── hooks/ — React Query hooks (auth, bookings, chat, …)
├── navigation/ — root, auth, customer, provider, stacks
├── screens/
│ ├── auth/
│ ├── customer/
│ └── provider/
├── services/ — Axios API modules
├── store/ — authStore, unreadStore
├── types/ — shared TypeScript models
├── utils/ — JWT parse, dates, greeting
└── validators/ — Zod schemas for forms
- Node.js 18+
- Expo Go or a development build (
expo-dev-clientis in the project) - Running Boame API on a machine reachable from your phone/emulator
- For physical devices: use your LAN IP (not
localhost) in.env
cd client
npm install
cp .env.example .env
# set YOUR_LAN_IP so a physical device can reach the API
npx expo startThen press a / i for Android / iOS simulator, or scan the QR code with Expo Go / your dev client.
Native scripts (when using a prebuild / local Android folder):
npm run android
npm run iosCreate a .env in client/ (gitignored):
EXPO_PUBLIC_API_URL=http://YOUR_LAN_IP:3000/api/v1
EXPO_PUBLIC_SOCKET_URL=http://YOUR_LAN_IP:3000| Variable | Purpose |
|---|---|
EXPO_PUBLIC_API_URL |
REST base URL (must include /api/v1) |
EXPO_PUBLIC_SOCKET_URL |
Socket.io origin (no /api path) |
Restart Expo after changing env vars so Metro picks them up.
Profiles in eas.json:
- development — internal distribution, development client
- preview — internal distribution
- production — auto-increment version
npx eas build --profile development --platform android| Concern | How the client handles it |
|---|---|
| API auth | Axios interceptor + persisted JWT |
| Google OAuth | expo-auth-session → backend token → store |
| Payments | Booking flow opens Paystack URL via expo-web-browser |
| Chat | SocketProvider + conversation/message services |
| Location | Text-based location on bookings (map picker planned) |
| Deep linking | Scheme boame in app.json |
Payment verification via webhook, not redirect. Paystack redirects after checkout can't reliably reach a mobile app's custom scheme, so the client doesn't trust the redirect. After the in-app browser closes, it polls the booking status — which the backend flips only after verifying Paystack's server-to-server webhook. The webhook is the source of truth; the client confirms against it.
Cursor-based pagination for chat. Message history loads newest-first in fixed pages using a compound cursor (created_at + id) rather than OFFSET, which avoids duplicated/skipped messages when new messages arrive mid-scroll. Live socket messages merge with the paginated history, deduplicated by id.
Realtime + paginated history, reconciled. The chat screen derives its message list from two sources — an infinite query (server history) and live socket state — merged and deduped, so pagination and live delivery coexist without overwriting each other.
Role-based app shell. Registering as a provider issues a new JWT with the updated role; the root navigator switches the entire tab structure reactively when the token changes — no manual navigation.
- Map-based location picker (react-native-maps + geocoding) — coordinate columns and location libraries are already in place; pin-drop UI pending
Backend-planned features that will land in this client over time:
- Push notifications (Expo / FCM)
- Cloudinary image uploads (provider profile / ID)
- Dispute flows
- Apple Sign-In
Pairs with the Boame API — DigitalOcean (API) + Expo EAS (mobile) for deployment.











