A premium React Native + Expo SDK 54 + TypeScript mobile application for Saloon Vero, serving customers, stylists, and admins from a single codebase with role-based experiences. Connected to a shared Supabase backend, the app brings real-time booking, stylist reviews & ratings, role dashboards, Google authentication, and gorgeous styling details to everyone's fingertips.
-
π₯ Role-Based Experiences (Customer Β· Staff Β· Admin):
- π Customers browse, book, manage their schedules, and review stylists.
- βοΈ Staff (stylists) get their own area: today's chair, weekly schedule, booking status actions (complete / no-show / cancel), and a "My Reviews" view with engagement stats.
- π οΈ Admins get a full management dashboard plus a walk-in booking desk.
- π§ The correct area is resolved automatically from the signed-in user's profile role at the navigation layer (see
lib/auth/routing.ts), with route guards in each group's_layout.tsx.
-
π Secure Authentication & Auto-Migration:
- π§ Email and Password signup/login with in-app 6-digit OTP verification (Supabase Auth).
- π "Continue with Google" OAuth integration via
expo-auth-sessionandexpo-web-browser, returning to the app through thesaloonveroapp://auth/callbackdeep link. - π Password reset via emailed OTP code, then in-app new-password entry.
- π In-app password change from every account screen (customer, staff, admin): email users re-enter their current password to confirm, while Google-only accounts can set a password to also sign in by email. Raw Supabase errors are mapped to friendly copy (
lib/auth/friendly-error.ts). - πΆοΈ Guest mode: Browse services and book without an account. Bookings are kept locally in
AsyncStorageand automatically claimed and merged into the user's account upon sign-up or login.
-
π¨ Aesthetics & Theming ("Warm Luxe"):
- π Curated light and dark brand modes driven automatically by the device's system settings.
- βοΈ Refined Poppins typography (weights from
400Regularto800ExtraBold). - β¨ Fluid animations and interactive press-feedbacks (0.97x button/card spring-scaling via a shared
PressableScaleprimitive), staggered list entrances, skeleton loaders, and centered safe-area-aware layouts β all powered byreact-native-reanimated. - π A first-run coach mark on the welcome screen points at the theme toggle (a pointer bubble plus a pulsing accent ring), shown only to first-time users and dismissed on tap or after a few seconds (
components/ui/coach-tooltip.tsx).
-
π
Booking Wizard:
- πΆββοΈ Stepped flow: Service Selection
$\rightarrow$ Stylist Choice (or "Any Stylist")$\rightarrow$ Date selection in a gorgeous 3-column square card grid (complete withTODAY/TOMORROWbadges and custom amber branding for Poson Poya day)$\rightarrow$ Real-Time Available Time Slots$\rightarrow$ Contact Details$\rightarrow$ Success.
- πΆββοΈ Stepped flow: Service Selection
-
π€ Schedules, Cancel & Reschedule (For All Users):
- π Schedules tab displaying upcoming and past bookings styled as receipt-style records, with vertical left color status strips (gold for upcoming, green for completed, red for cancelled), ticket badges for reference codes, and flexible action rows.
- π Reschedule upcoming bookings: Select a new date and time directly from the schedules list (available to both logged-in users and guests).
- β Cancel upcoming bookings: Cancel bookings with simple confirmation prompts. Authenticated users verify via JWT, while guests verify via phone number ownership checks.
-
β Stylist Reviews & Ratings:
- π Customers leave star ratings and written reviews for stylists from the home screen and the booking flow; a stylist's running average rating is recomputed via a single shared helper (
lib/utils/reviews.ts). - β€οΈ Engagement: like/heart and report reviews, with per-device state persisted in
AsyncStorageand optimistic UI that rolls back on failure. - π‘οΈ Admin moderation: a dedicated admin Reviews screen lists all reviews, filters by stylist, surfaces report counts, and supports deletion (which re-derives the affected stylist's rating).
- π§° Graceful fallbacks: when the database is unseeded or unreachable, screens fall back to local datasets so the UI never breaks.
- π Customers leave star ratings and written reviews for stylists from the home screen and the booking flow; a stylist's running average rating is recomputed via a single shared helper (
-
π¬ Real-Time Chat (Customer β Stylist):
- β‘ One-to-one threads between a customer and a stylist, with live message delivery and live unread counts powered by Supabase Realtime (postgres-changes subscriptions on
messagesandconversations). - π¨ A floating chat button (
ChatFab) on the customer home and schedules screens, plus the staff area, opens a unified inbox (app/messages/) that works for both sides β customers see stylists, stylists see their booked customers. - π· Photo messages: pick & crop one or more images, sent to a private Supabase Storage bucket and served via short-lived 1-hour signed URLs, with a full-screen image viewer.
- π Booking cards in chat: customers can deep-link from a booking to auto-attach its reference card to the thread; staff can attach any of that customer's bookings via a picker sheet.
- π’ Per-side unread tracking and message previews are maintained by database triggers (
on_message_insert), reset through amark_conversation_readRPC (with a client-side fallback). - π‘οΈ RLS limits each thread to its two participants; customers can start a conversation freely, while stylists may only open one with a customer who has booked them (enforced by the
is_stylist_userhelper).
- β‘ One-to-one threads between a customer and a stylist, with live message delivery and live unread counts powered by Supabase Realtime (postgres-changes subscriptions on
-
π Push Notifications & Permission Priming:
- π² Expo push notifications for booking events (confirmed / cancelled / rescheduled β the customer) and new chat messages (β the other participant). The device's Expo push token is registered after sign-in and stored in a
push_tokenstable (multi-device). - βοΈ Sends are server-side: the booking Edge Functions push directly, and a dedicated
notifyEdge Function (participant-verified) handles chat, both POSTing to the Expo Push API via a sharedsendExpoPushhelper. - πͺ Branded pre-permission priming β before the OS prompt, a friendly modal (
PermissionPrimer) explains why the app needs photos, camera, or notifications, shown once per permission. If permanently denied, the app offers an Open Settings shortcut. - π· Camera capture ("Take Photo") sits alongside library picking everywhere photos are chosen β avatar, chat, and the admin photo picker β so the camera permission is genuinely used.
- πΆοΈ Guests are unaffected (no account β no push registration), and notification permission is only requested for signed-in users.
- π² Expo push notifications for booking events (confirmed / cancelled / rescheduled β the customer) and new chat messages (β the other participant). The device's Expo push token is registered after sign-in and stored in a
-
π οΈ Admin Dashboard:
- π Today overview with revenue, completion %, capacity and the next booking.
- πΆ Walk-in desk to create bookings on behalf of customers.
- π Bookings management (search, filter by status/stylist, cancel).
- π§βπ€βπ§ People screen to manage user roles and link staff accounts to stylist profiles.
- πΌοΈ Manage services, stylists, lookbook gallery items, and blocked slots directly in-app.
- π· Custom Photo Uploads: upload headshots/service images (stored in Supabase Storage with the correct MIME type) or paste custom URLs, updating customer-facing lists in real time.
- β Bottom Floating Action Buttons (FABs): quick actions float cleanly above list views, automatically accounting for safe-area layout heights on iOS and Android.
-
π€ User Profile Dashboard:
- πΌοΈ Avatar source switcher β choose between a generated DiceBear cartoon (tap to shuffle a fresh one), your email/Google account photo (shown only when available), or a custom upload to Supabase Storage. The choice is saved on the user's auth metadata and applied everywhere the avatar appears.
- βοΈ Profile details (name, mobile) save through a dedicated form whose Save / Cancel buttons appear only when something is edited, with a confirmation on save; email is shown read-only.
- π A compact stats strip surfaces total and upcoming booking counts (upcoming computed by the same future-and-not-cancelled rule as the Schedules tab).
- π A Security section to change (or, for Google accounts, set) the password without leaving the app.
- π Theme preference (light / dark / system) persisted across sessions. Guests, who have no account page, get an inline theme toggle on the customer screens instead.
- β¨ New Things tab highlighting fresh services and salon updates.
- π§ Expo Router Navigation: The app uses Expo Router's file-based routing through the root
app/directory, with grouped routes for auth, customer tabs, staff, admin, and booking flows. The signed-in user's role determines which group they land in, enforced by guards in each group's_layout.tsx. - π‘οΈ Direct Database Access (Governed by RLS): Public data (services, stylists, gallery, business hours, reviews) and authenticated user records (bookings, profiles) are fetched directly using the anonymous Supabase client key, protected by Row Level Security (RLS) policies.
- β‘ Secure Edge Functions: Operations requiring the Supabase
service-rolekeyβcomputing availability and mutating appointments safely without double-bookingsβare delegated to serverless Supabase Edge Functions:get-availabilityβ Checks real-time schedules, including shop business hours, active stylists, confirmed bookings, and blocked slots.create-bookingβ Validates client forms, guards against double-booking race conditions, inserts rows, and issues confirmation references.reschedule-bookingβ Moves an existing booking to a new date/time with the same conflict guarding.cancel-bookingβ Cancels a booking, verifying ownership (JWT for users, phone match for guests).notifyβ Participant-verified chat push trigger; the client calls it after sending a message and it pushes to the other side via the Expo Push API.
- π¬ Realtime Chat Layer: Conversations and messages live in their own RLS-guarded tables, with
postgres-changessubscriptions driving live threads and unread badges, database triggers maintaining unread counts and previews, and a private Storage bucket (signed URLs) for photo messages. - π Notifications & Permissions: Push tokens are stored per-device in a
push_tokenstable; sends run server-side from the booking functions and thenotifyfunction (sharedsendExpoPushhelper). On the client, a smalllib/permissions/layer pairs the OS prompts with a branded priming modal mounted at the app root. - π§© Feature-Oriented Organization: Shared UI, booking logic, API wrappers (per role), auth helpers, review utilities, business constants, validation schemas, and database types are separated into focused folders so the app is easy to extend.
- π Root Error Boundary: The root layout exports an Expo Router
ErrorBoundary(app/_layout.tsx) that catches uncaught render errors and shows a friendly, retryable fallback instead of a white screen. It is intentionally self-contained (no theme/session context) so it still renders even if a provider is what threw β showing the error details in development and a generic message in production.
Saloon_Vero_App/
app/ # π Expo Router file-based screens and layouts
_layout.tsx # βοΈ Global providers, fonts, splash handling, route setup, root ErrorBoundary
index.tsx # β¨ Branded splash/onboarding entry screen
access.tsx # πͺ Sign-in / guest-mode gateway screen
(auth)/ # π Auth stack: login, signup, forgot-password (OTP)
(tabs)/ # π± Customer bottom tabs: Home, New Things, Book, Schedules, Account
(staff)/ # βοΈ Staff area: today, weekly schedule, account, my reviews
(admin)/ # π οΈ Admin area: today, bookings, walk-in, and a `more/` hub
more/ # β³ services, stylists, gallery, blocked-slots, people, reviews
auth/ # π OAuth callback + password-reset routes
booking/ # π
Booking flow routes
[serviceId].tsx # πͺ Stepped booking wizard
success.tsx # π Booking confirmation screen
messages/ # π¬ Chat inbox + threads (customer & stylist)
index.tsx # β³ inbox list of conversations
[conversationId].tsx # β³ live thread (text, photos, booking cards)
assets/images/ # πΌοΈ App icons, splash images, logo, onboarding artwork
components/ # π§© Reusable React Native components
ui/ # π
Warm Luxe UI primitives: buttons, cards, inputs, loaders
admin/ # π οΈ Admin-specific UI (photo picker, dashboard primitives)
staff/ # βοΈ Staff-specific UI (booking cards, metrics)
auth/ # πΆοΈ Auth-related UI such as the guest-mode header
booking/ # π
Booking-specific UI such as SlotPicker
chat/ # π¬ Chat UI: ChatFab, image bubble/viewer, booking cards
permissions/ # πͺ PermissionPrimer (branded pre-permission modal)
services/ # βοΈ Service presentation components
stylists/ # π€ Stylist presentation components
reviews/ # β Shared review UI (e.g. StarRow)
config/ # π§ Environment and app configuration helpers
env.ts # π EXPO_PUBLIC_* environment reader
constants/ # π¨ Brand constants and design tokens
theme.ts # π Colors, spacing, radius, shadows, typography
salon.ts # πͺ Business config: salon phone, Poya holiday calendar
context/ # π§ React providers for app-wide state
session.tsx # π Supabase session provider and auth state
theme.tsx # π Theme preference (light/dark/system) provider
hooks/ # πͺ Shared custom hooks
use-theme.ts # π Theme token access by color scheme
use-countdown.ts # β±οΈ Resend-code cooldown timer
lib/ # π§° App logic separated by responsibility
api/ # β‘ Supabase client, queries, admin/staff/chat APIs, Edge wrappers
admin/ # π οΈ Admin helpers (slugify, stats, profile rules)
auth/ # π Google OAuth, routing, signup/error & change-password helpers
booking/ # π Booking reducer and availability calculations
chat/ # π¬ Chat helpers (image paths, message previews)
notifications/ # π Push token registration (register.ts)
permissions/ # π‘οΈ Photo/camera permission helpers (priming + settings fallback)
staff/ # βοΈ Staff bookings grouping/view helpers
storage/ # πΆοΈ Local guest-booking persistence (AsyncStorage)
utils/ # π Formatters, references, avatar, time & review helpers
validation/ # β
Zod schemas for booking and customer details
types/ # π§Ύ Shared TypeScript/domain types
database.ts # ποΈ Supabase database type definitions
supabase/ # ποΈ Backend definitions kept alongside the app
migrations/ # π§± SQL migrations (ratings, reviews, reactions, chat, avatar storage, push tokens, β¦)
functions/ # β‘ Edge Functions: get-availability, create/reschedule/cancel-booking, notify
_shared/ # β³ cors, service client, sendExpoPush push helper
__tests__/ # π§ͺ Jest unit and component test suites (local-only, git-ignored)
.env.example # π Safe placeholder env file for setup
app.json # π± Expo app configuration (scheme: saloonveroapp)
eas.json # βοΈ EAS build profiles (development / preview / production)
package.json # π¦ Scripts and dependencies
tsconfig.json # π§ TypeScript configuration
Ensure you have Node.js and npm installed.
Create a .env file in the root directory:
cp .env.example .envFill in the values from your Supabase dashboard:
EXPO_PUBLIC_SUPABASE_URL: Your Supabase Project URL.EXPO_PUBLIC_SUPABASE_ANON_KEY: Your Supabase Anonymous API key.
npm installRun the Expo CLI server:
npm startYou can choose to open the app using:
- π± Expo Go on a physical phone.
- π€ Android emulator (
ain terminal) or π iOS simulator (iin terminal). - π Web browser (
win terminal). - π οΈ A development build (recommended for testing Google login on a physical Android device):
npx eas-cli build --profile development --platform android
In the Supabase dashboard under Authentication β URL Configuration, add these Redirect URLs:
saloonveroapp://auth/callbackβ development/production builds.https://<your-web-domain>/auth/callbackβ web.
β οΈ Note: Supabase rejects redirect URLs whose host is a LAN IP (e.g.exp://192.168.x.x:8081/...) even when allow-listed, and silently falls back to the Site URL. The app works around this in Expo Go by rewriting the redirect host tolocalhost(seelib/auth/google.ts), which Supabase always allows.
The chat feature relies on two extra Supabase configuration steps (also documented in supabase/migrations/0009_chat.sql):
- Realtime: enable replication for the
messagesandconversationstables (Dashboard β Database β Replication, orALTER PUBLICATION supabase_realtime ADD TABLE β¦). - Private
chatStorage bucket: create a bucket namedchatwith Public = OFF, then apply the participant-only RLS policies onstorage.objects(provided at the bottom of the migration) so only the two participants can read/write a thread's images.
Profile-photo uploads land in a public avatars bucket at the path <user_id>/<timestamp>.jpg. A public bucket only governs reads β uploads still need RLS INSERT policies on storage.objects, or they fail with new row violates row-level security policy. Run supabase/migrations/0010_avatar_storage_policies.sql (or paste it into the SQL Editor) to grant each authenticated user write access to their own folder only.
Booking and chat pushes need three pieces wired up:
- DB: run
supabase/migrations/0011_push_tokens.sqlto create thepush_tokenstable (owner-only RLS). - Edge Functions: deploy
notify(plus the existingcreate-booking/cancel-booking/reschedule-booking, which now send pushes):supabase functions deploy notify. They use the standardSUPABASE_URL,SUPABASE_ANON_KEY, andSUPABASE_SERVICE_ROLE_KEYenv vars. - EAS project ID: push tokens require an EAS
projectId. Runeas init(or your firsteas build) β it writesextra.eas.projectIdintoapp.json. Until then the app skips token registration gracefully (logs a warning, never crashes).
β οΈ Push notifications cannot be tested in Expo Go on Android (SDK 53+) β use an EAS development or preview build. The photo/camera permission priming works in any dev build. iOS push also requires running on a physical device with a development build.
The codebase includes an automated test suite containing 153 tests across 41 suites, covering state transitions, availability logic, Google OAuth redirect handling, password-change & friendly auth-error mapping, environment validation, onboarding behavior, staff status actions, admin/people role guards, and custom UI components (buttons, cards, loaders, skeletons, empty states, and entrance/press animations).
βΉοΈ The
__tests__/folder is git-ignored and kept local-only β it is not uploaded to GitHub. The tests still run on your machine for local verification.
To run the tests:
npm testTo run the full local quality check before pushing:
npm run lint
npm test -- --runInBand
npx tsc --noEmit