Skip to content

Production rebuild: Expo SDK 56 app + hardened backend + Next.js web + CI/CD#8

Merged
aashir-athar merged 62 commits into
masterfrom
feat/web-app-2026-06-17
Jun 17, 2026
Merged

Production rebuild: Expo SDK 56 app + hardened backend + Next.js web + CI/CD#8
aashir-athar merged 62 commits into
masterfrom
feat/web-app-2026-06-17

Conversation

@aashir-athar

Copy link
Copy Markdown
Owner

Takes BludStack to production grade across the whole stack, plus a new web app.

Mobile (Expo SDK 56)

  • From-scratch rebuild of all 14 screens on the locked stack: Tamagui + Zustand + TanStack Query + react-hook-form/zod + Reanimated v4 + NativeTabs, under src/app/.
  • MapLibre + free OSM/CARTO tiles (no Google Maps, no key, no cost).
  • Live donor heartbeat via expo-persistent-background-location (foreground service, survives swipe-to-kill) + expo-location fallback.
  • Donor reputation tiers + verified-badge program over the existing total_donations / is_verified fields.
  • Legacy context/component layer deleted; push registration wired. tsc / lint / expo-doctor (21/21) / Metro export all green.

Backend (Express 5 / Supabase)

  • CRON_SECRET fail-closed, per-user heartbeat rate limiting (keyed per donor+request), cooldown moved into the accept RPC (race-free), pagination + notification fixes.
  • Code review + security review on the live-location flow; findings fixed, open items documented in HARDENING_NOTES.

Web app (Next.js 16, web/)

  • Full mobile parity: auth (OTP), onboarding, feed, post-request (map picker), request detail (accept/decline/complete), donors, my-requests, history, profile, edit-profile, donor detail, live tracking map, realtime chat.
  • Same backend, same design language (crimson/onyx/bone, real logo, no emojis), ported logic (api, schemas, queries), Skeleton-only loading, toast over alert, a11y.
  • Static export for GitHub Pages + deploy workflow; 18 routes prerender to HTML.

CI/CD + tooling + tests

  • GitHub Actions: mobile-ci, backend-ci, schema-check (Postgres + verify), eas-build, deploy-web-pages.
  • Prettier, .editorconfig, .gitattributes, .nvmrc, husky pre-commit guard (blocks emoji / em-dash).
  • Jest: 36 mobile + 14 backend tests (blood-compat, geo, zod, reputation, supertest boot).
  • README + zero-to-deploy + web README.

Owner action items: apply the Supabase migration in Studio; set CI secrets (EXPO_TOKEN, NEXT_PUBLIC_* for Pages); run EAS builds.

aashir-athar and others added 30 commits April 15, 2026 04:58
Preserves in-flight files (utils/api.ts, donor/[id].tsx, history.tsx,
chat.tsx, CustomAlert.tsx, PressableScale.tsx) and the in-progress
geo-fencing service changes as a stable starting point for the
production-hardening branch. Adds *.zip ignore rule to repo root.
Closes 28 flaws from the security/correctness/architecture audit.
See HARDENING_NOTES.md for the full flaw->fix map and apply steps.

Critical security:
- supabase_schema.sql: tables + RLS + guard triggers + indexes +
  realtime publication + atomic RPCs (accept_blood_request,
  complete_blood_donation). RLS now blocks mobile from reading any
  other user's push_token, GPS, last_donation_date and from mutating
  privileged columns (total_donations, role, is_verified, etc.).
- Deleted client-side notifyCompatibleDonors + direct exp.host pushes.
- All mutating flows route through backend API (no direct Supabase
  writes for blood_requests / request_responses / privileged profile
  fields).

Backend hardening:
- DB-persisted geo-fence job state (blood_requests.geofence_*) with
  CAS-style claim; restart-safe, multi-instance-safe.
- Per-ring eligibility refresh; declined donors excluded.
- Race-free accept via SELECT FOR UPDATE inside accept_blood_request
  RPC.
- Server-side age gate on register + DB CHECK constraint.
- trust proxy on; authRateLimiter wired on /register; morgan path-only
  in production to stop logging GPS query strings.
- Top-of-file expo-server-sdk import; debug acceptRequest console.log
  removed.

Mobile migration:
- utils/api.ts now exports the full typed backend surface; every
  privileged mutation goes through it.
- AuthContext.updateProfile -> backend PATCH /profiles/me.
- onboarding -> apiRegister (handles age downgrade server-side).
- useRequests, useNotifications, useLocation, StatsBanner all
  migrated off direct Supabase writes/reads for sensitive paths.
- _layout.tsx: deep-link whitelist (request, donor, map, chat) and
  one-shot first-mount redirect ref.
- request/[id]: accept/decline/complete via API; map/live throttled
  location writes via API.

Vercel deployment:
- backend/api/index.js (Express adapter), api/cron/tick-geofence.js,
  api/cron/expire-requests.js, vercel.json.
- server.js skips listen() + worker when running under Vercel.
- Railway/Render path unchanged.

Production chat (per realtime-chat-expo-54-55 skill):
- messages table with strict RLS (sender-only insert gated by
  accepted donation relationship; receiver-only read-flag flip;
  guard trigger blocks immutable-column tampering).
- useChatMessages: paginated load, realtime INSERT/UPDATE, optimistic
  send with client_id idempotency + reconciliation + retry-on-failed.
- useChatTyping: typing indicator via Supabase Realtime broadcast.
- chat.tsx: FlashList v2 with maintainVisibleContentPosition
  (replaces deprecated `inverted`), memoized renderItem, stable
  keyExtractor on client_id.
- components/Skeleton.tsx: shimmer placeholder replacing
  ActivityIndicator in LoadingScreen + chat older-loader.
- expo-crypto + react-native-keyboard-controller installed via expo
  install.
Resolves PR conflicts so the production-hardening pass can land:

- README.md: kept origin/master's project README (added in 1d481c9).
- backend/src/server.js: kept hardening version (trust proxy, morgan
  path-only, Vercel adapter, worker bootstrap) and DELETED the
  /debug-env endpoint added in f9b2b3e. That endpoint leaked the
  service-role key length + first 20 and last 10 chars to anyone who
  could reach the URL — a credential disclosure that must not ship.
- backend/src/controllers/donationController.js: kept hardening
  version (atomic accept via accept_blood_request RPC with
  SELECT FOR UPDATE). Functionally supersedes the maybeSingle +
  units_needed checks added in 8b22078 and c753009, eliminating the
  read-then-write race those commits did not fix.

ACTION REQUIRED before deploying: rotate the Supabase service-role
key in Supabase Studio (Project Settings → API → Reset). The current
key has been observable in /debug-env on origin/master and must be
treated as compromised. Update SUPABASE_SERVICE_ROLE_KEY in every
deployment target (Vercel / Railway / Render / local .env).
Vercel Hobby caps cron at one run per day, which makes the geo-fence
ring expansion useless (rings need ~30s–1min cadence). The endpoints
themselves still exist; they're driven externally now.

- vercel.json: removed `crons` array. Functions, rewrites, memory
  budgets unchanged.
- api/cron/tick-geofence.js + expire-requests.js: accept the shared
  secret in either Authorization: Bearer <CRON_SECRET> OR
  ?secret=<CRON_SECRET>. The query-param form lets cron-job.org and
  similar simple schedulers authenticate without custom headers.
- README: documents three free external scheduler options
  (cron-job.org, Upstash QStash, Supabase pg_cron). Recommended
  interval is 1 minute for tick-geofence, 10 minutes for expire.
Mobile (rn-expo-2026-architect pass)
  • Distinct BludStack palette (crimson/saline/plasma on warm onyx/bone)
  • Semantic typography roles + Motion tokens + pill-shaped radius
  • BrandMark vector replaces emoji brand glyph
  • Surface component: iOS 26 glass / iOS blur / Android flat
  • Pill-shaped Button (Reanimated press, label-pulse loading state)
  • Pill-shaped Input with animated focus ring + inline error/hint
  • Error stack: errorReporter (no Sentry), ToastContext, ErrorBoundary,
    NetBanner — wired into root layout
  • Auth: Uber-style two-step, field-level errors, Toast on failure
  • Onboarding: role-first, conditional DOB required for donor/both,
    animated step transitions, role-aware confirm card
  • Tab bar: theme-tokenised pill, role-aware visibility, Ionicons
  • Request detail: chat button wiring on both donor + recipient sides,
    peak-end completion state, vector icons, cancel via API
  • Chat: scrubbed of emoji/text arrows for Ionicons (back, send, ticks)
  • app/index.tsx — explicit redirect gate, no more group-route default
    race that landed users on /onboarding pre-auth
  • Root layout: push-tap deep linking (/request/[id] from notification),
    explicit initialRouteName='index', clearer flow comments
  • AuthContext: validates cached session with getUser() against the
    server, signs out locally if the token is stale / project switched

Killed-state notifications
  • Android emergency channel: MAX importance, bypassDnd, lights
  • iOS interruptionLevel: 'time-sensitive' (no critical-alerts entitlement)
  • ttl: 0 on emergency pushes (drop stale, don't wake at 3 AM)
  • Dead push tokens auto-pruned in notificationService
  • Battery-optimization deep-link helper for Android OEMs
  • PUSH_NOTIFICATIONS.md playbook with per-OEM troubleshooting

Theme tri-state
  • Direct Appearance.getColorScheme() + addChangeListener
  • Defaults to system (with dark fallback when Appearance is null)
  • Profile screen exposes Dark / Light / System with Ionicons

Schema hardening
  • supabase_schema.sql: explicit GRANTs for the authenticated role on
    profiles / blood_requests / request_responses / messages. Postgres
    checks table-level grants BEFORE RLS; without them the user gets
    "permission denied for table X" even with correct policies.
  • messages table now ships with client_id idempotency column
  • verify_schema.sql produces a single-row PASS/FAIL audit table

Backend error visibility
  • errorHandler now surfaces pgCode/pgHint/details to the client when
    DEBUG_ERRORS=1 (or NODE_ENV != production). Production with the flag
    unset still returns the generic message.
  • ApiError class no longer extends Error (Hermes wrapNativeSuper bug
    crashed module load). Plain class with isApiError discriminator.
  • errorReporter.error routes through console.warn to avoid Expo HMR
    client constructing a NamelessError extends Error (same bug).

Skipped intentionally
  • No Tamagui, Zustand, TanStack Query, react-hook-form/zod, Jest,
    Maestro — documented deviations from the agent's default stack.
  • README rewrite, zero-to-deploy.md, icon-prompts.md — not generated
    in this pass.
feat(2026-ui): full redesign + RLS grants fix + debug-surfaceable errors
Locks the (tabs)/request.tsx visual + interaction language as the canonical
reference (REDESIGN_PLAN.md §14), then propagates it through every screen,
modal, and shared component. Adds the one-active-commitment rule, mutual-
disclosure RLS, monochrome dead-state / green-fulfilled cards, silent
pull-to-refresh, in-app chat (no email), unified ToggleSwitch pill, address
editing in profile/edit, and a 21-section production README.

Mobile
------
- (auth)/index.tsx, onboarding.tsx, profile/edit.tsx rewritten to the
  Request-screen sheet pattern: hero zone, -Spacing[5] sheet overlap, brand
  accent strip + handle, spring entrance, inline CTA with summary row +
  footer micro-copy, Haptics on every Pressable.
- (tabs)/index, donors, my-requests, history, profile aligned: SectionLabel
  pattern (uppercase / black / widest tracking / textMuted), Haptics, toast-
  wired error paths.
- (tabs)/_layout: my-requests now visible to all roles (any user who can
  post a request must be able to manage their posts).
- (tabs)/index home: ACTIVE COMMITMENT banner (Uber "ride in progress"
  pattern), Nearby-requests feed-empty Section kind under the feed header
  when compatible.length === 0.
- Truly silent pull-to-refresh on all four tabs: refreshing hard-coded
  false so no native spinner ever activates and no layout space is
  allocated; gesture still fires onRefresh.
- chat.tsx: KeyboardAvoidingView now handles Android via behavior='height';
  composer inset baked into paddingBottom so the input never hides behind
  the keyboard or sits on the Android gesture indicator. Single Chat pill
  per profile via messageContact (no more email mailto:).
- request/[id].tsx: openMaps declaration order fixed; per-donor live-GPS
  status pill on recipient view; aggregate "Track donor live" CTA with
  waiting fallback when no heartbeat; "View full profile" CTA on both
  sides; one-active-commitment guard surfaces a warning pill + "Open my
  active commitment" deep-link instead of letting the donor tap Accept.
- donor/[id].tsx: Message button opens in-app chat (no mailto), accepts
  optional requestId param from upstream callers, discovers the mutual-
  commitment thread when not passed.
- map/live.tsx: emojis replaced with Ionicons, theme.glass/accent legacy
  tokens swapped to theme.surface/primary, Haptics on close/fit/maps.
- Unified ToggleSwitch as the canonical 2026 pill (Reanimated v4 thumb +
  interpolated track colour + Haptics). Replaces three different inline
  toggle implementations across profile, profile/edit, and onboarding.
- Donor settings card uses ToggleSwitch variant='inline' + hairline divider
  so nested rounded rectangles don't compete.
- RequestCard status colorway: active = urgency tone, fulfilled = success
  green ("Fulfilled · life saved"), cancelled = grey ("Cancelled"),
  expired = grey ("Expired") with 0.78 opacity and soft blood badge.
- profile/edit adds address (multiline) and DOB is now nullable so a donor
  can't silently bypass the age gate via the 2000-01-01 default.
- Pill/rectangle radius audit across 7 files: 24 offenders promoted from
  Radius.sm/md/lg/xs to Radius.xl (cards/banners) or Radius.pill
  (chips/badges/action buttons); chat bubbles keep their iMessage tail.
- SelectSheet + CustomAlert get the brand accent strip + handle so modals
  match the Request-screen sheet language.

Backend
-------
- accept_blood_request RPC adds the one-active-commitment rule: a donor
  with any 'accepted' response on an 'active' request cannot accept
  another until the prior commitment completes or cancels.
- profileController PATCH /profiles/me now exercises avatar_url + address
  in ALLOWED_FIELDS and re-validates the donor-age gate when role flips.

Schema
------
- supabase_schema.sql + migrations/2026-05-12-one-active-commitment-and-
  mutual-disclosure.sql:
    * accept_blood_request RPC (one-active-commitment guard).
    * profiles_select_mutual_commitment RLS policy — opens the donor↔
      recipient profile join the moment an accepted/completed response
      exists between them. Fixes "name / call / message buttons silently
      empty on the request detail screen".

Docs
----
- README.md: full 21-section production rewrite per architect spec —
  badges, Mermaid architecture + N-donors + geo-fence diagrams, FAQ,
  contributing guide, author footer keyed to aashir-athar, zero emojis.
- REDESIGN_PLAN.md §14 locks the canonical Request-screen design
  language as the reference + the 100%-wired mandate.
feat(uber-sweep): Request-screen design language across every surface
…w findings 1-8

Verified all 8 review findings against current code. All still valid; fixed
each with minimal, targeted changes. No skips.

#1 — Heartbeat TOCTOU (backend/.../donationController.js)
   The previous flow did SELECT-then-UPDATE keyed only by id, leaving a
   window where the response could flip to completed/declined between the
   two queries while the heartbeat still wrote location. Collapsed into a
   single conditional UPDATE gated on .eq('status','accepted'). When zero
   rows affected, we probe once to distinguish 404 (no response) from 409
   (response no longer accepted) so the client's existing 400/404 fast-path
   in useDonorHeartbeat extends cleanly to 409.

#2 — Donor-location coherence CHECK (supabase_schema.sql + new migration)
   Added a named CHECK constraint request_responses_donor_location_coherent
   that enforces "all three null OR all three not null" across donor_lat,
   donor_lon, donor_location_updated_at. Stops partial-coordinate writes
   from ever reaching mobile/app/map/live.tsx + request/[id].tsx as
   off-globe markers.

#3 — RPC OWNER pinned to service_role (defense-in-depth)
   Both SECURITY DEFINER RPCs (accept_blood_request, complete_blood_donation)
   now ALTER FUNCTION OWNER TO service_role. The system already worked via
   the JWT-claim fallback in the trigger guards; this makes the
   current_user = 'service_role' short-circuit fire too, so the guards are
   robust even if PostgREST's claim shape ever changes.

#4a — tg_set_fulfilled_at trigger in migration
   The canonical schema had it but the standalone migration did not — fresh
   databases applying only the migration would miss fulfilled_at automation.
   New 2026-05-14 migration is self-contained.

#4b — Drop donor_id write in N-donor fulfillment path
   complete_blood_donation previously did
       update blood_requests set status='fulfilled', donor_id = p_donor_id
   which only records 1 of N donors when units_needed > 1. Dropped the
   donor_id write — request_responses (one row per donor) is the single
   source of truth. fulfilled_at is set by the trigger.

#5 — Typed router.push everywhere (no more `as any`)
   typedRoutes is enabled in app.json. Replaced every `router.push('...' as
   any)` and ``router.push(`/request/${id}` as any)`` with the typed
   `router.push({ pathname: '/request/[id]', params: { id } })` form.
   Audited and fixed: (tabs)/donors, index, my-requests, history, profile,
   request, _layout, chat, donor/[id], request/[id].

#6 — profile/edit late-hydration sync
   Form state was only set by useState initialisers at mount. If the auth
   context landed the profile after mount (cold-start, push deep-link, OTA
   refresh), the form stayed on empty defaults. Added a sync useEffect
   gated on syncedForIdRef so each profile.id is synced exactly once; the
   `saving` and `pickerOpen` gates suppress sync mid-flight. A realtime
   UPDATE on the profile row can no longer clobber an in-progress edit.

#7 — useDonorHeartbeat watcher race
   The async setup inside useEffect could land
   Location.watchPositionAsync's result into subRef.current AFTER cleanup
   had already run, leaking a background GPS subscription on unmount /
   deps change. Added a local `cancelled` flag checked at every async
   continuation (permission prompts, after watchPositionAsync resolves,
   inside the location callback). If cancelled, the freshly-created
   subscription is removed immediately and never stored. Also extended
   the fatal-stop fast path to include 409 (matches #1's new status).

#8 — accept_blood_request donor-profile lock
   The one-active-commitment check only locked the target blood_requests
   row, so two concurrent accepts on DIFFERENT requests by the SAME donor
   each locked only their own row and both could observe
   v_other_commitment = null. Added `perform 1 from public.profiles
   where id = p_donor_id for update;` before the check — serialises on
   the donor.

Files touched:
  - backend/src/controllers/donationController.js  (#1)
  - mobile/hooks/useDonorHeartbeat.ts              (#7)
  - mobile/app/profile/edit.tsx                    (#6)
  - mobile/app/(tabs)/{donors,index,history,
       my-requests,profile,request}.tsx            (#5)
  - mobile/app/{_layout,chat}.tsx                  (#5)
  - mobile/app/{donor,request}/[id].tsx            (#5)
  - supabase_schema.sql                            (#2 #3 #4b #8)
  - migrations/2026-05-14-hardening-pass.sql       (#2 #3 #4a #4b #8 — NEW)

Validation:
  - TS: `npx tsc --noEmit` clean.
  - SQL: migration ends with a single-row PASS/FAIL diagnostic.
fix(hardening): TOCTOU, race, typed routes, schema invariants — revie…
…clear-address with 400)

The validator on PATCH /api/v1/profiles/me had
    body('address').optional().isString()…
which, in express-validator, only treats `undefined` as "missing". The
mobile profile/edit modal sends `address: address.trim() || null` so the
recipient can clear the column. `null` then failed `.isString()` and the
server returned a confusing
    { field: 'address', message: 'Invalid value' }
visible in the user-reported toast when changing role.

Match the same `optional({ nullable: true })` pattern already used by
auth/register's `phone` and `date_of_birth` validators so explicit nulls
are accepted as "clear this column".
…e-2026-05-14

fix(backend): PATCH /profiles/me accepts null address (was rejecting …
… + onboarding/splash polish

Five tightly related fixes shipping together — all tighten the contact +
eligibility surface and the onboarding flow.

1. No email anywhere as a contact channel
   - request/[id].tsx `callContact`: was `phone ?? email` piped into `tel:`
     (which Android sometimes routes to the default mail handler).
     Now phone-only; if no phone, toast "No phone shared — use chat".
   - donor/[id].tsx `handleCall`: was `donor.phone ? tel: : mailto:`
     fallback. Now phone-only with the same "no phone shared" alert.

2. Splash screen no longer crashes on auth state flips
   - app/_layout.tsx: `hideAsync()` was firing every time `loading`
     transitioned false (sign-out → re-auth, profile refresh, etc.). SDK
     54's splash module unregisters itself on first hide, so the second
     call threw "No native splash screen registered for given view
     controller". Added a one-shot ref guard + `.catch(() => {})`.

3. Contact channels close together when a request seals
   - components/ProfileCard.tsx: new `interactive` prop (default true).
     When false, hides the Call / WhatsApp / Chat pills row entirely.
   - request/[id].tsx: passes `interactive={isActive}` to both
     ProfileCard call-sites (donor seeing recipient, recipient seeing
     accepted donors). Also gates the per-donor live-GPS pill + the
     aggregate "Track donor live" CTA on `isActive`.
   - donor/[id].tsx: new `commitmentActive` state resolved from the
     discovered mutual-commitment request's `status`; subscribed to
     realtime UPDATEs so it flips false instantly. Call + Message
     buttons get `disabled={!channelsOpen}` plus a "Channel closed"
     banner. Handlers also early-return with a kinder alert. Bonus:
     removed the ✉ emoji from the Message button (no-emoji rule),
     relabelled to "Call" / "Message".

4. DOB onboarding now shows for every role
   - app/onboarding.tsx step ordering: previously skipped 'dob' for
     recipients, but the identity-step CTA still read "Next · Date of
     birth" → recipients tapped Next and landed on Blood group,
     thinking the DOB screen was broken.
     Now: recipient flow is role · identity · dob · blood · contact ·
     confirm. The DobStep component already adapts its copy
     ("Optional — helps us serve you better"). Recipients can leave
     DOB blank and continue; donor/both still must enter a valid date.

5. Donor age window is now 18-65 (was 18+)
   - app/onboarding.tsx: new `MAX_DONOR_AGE = 65` constant. Validator
     rejects donor/both above 65 with a kind downgrade prompt. DobStep
     eligibility pill distinguishes too-young vs. too-old. Soft-
     downgrade toast: "Donors must be 18–65".
   - app/profile/edit.tsx: same `MAX_DONOR_AGE`. Validator returns
     "Donors must be 65 or younger" on save.
   - backend/authController.js: silent downgrade fires for `age < 18
     || age > 65`. Response message reads "Donor role requires age
     18–65".
   - backend/profileController.js: PATCH /profiles/me rejects role
     flip to donor/both when stored age is outside the window.
   - supabase_schema.sql: profiles_donor_age CHECK tightened to require
     `date_of_birth > current_date - interval '66 years'` AND
     `<= current_date - interval '18 years'` for donor/both.
   - migrations/2026-05-14-donor-max-age.sql: delta with a guarded
     pre-flight — aborts loudly if any existing donor/both row is
     already older than 65 so the operator decides (downgrade or
     exception) before the constraint lands.

Validation:
  - `npx tsc --noEmit` clean.
  - Migration ends with a single-row PASS/FAIL diagnostic.
…ules-2026-05-14

fix: contact-channel closure on request seal + donor age window 18-65…
…Libre + OSM tiles

react-native-maps + PROVIDER_DEFAULT on Android requires a Google Maps API
key — release APKs crashed with the native MapView throwing during
surfaceCreated when the key wasn't set. The project's free-only constraint
makes a billable Google Cloud key a non-starter, so this branch swaps the
entire map stack for @maplibre/maplibre-react-native with raw OSM raster
tiles (light) and CARTO Dark Matter (dark) — both completely free, no API
key, no account, no per-request quota at hobby scale.

Every business-logic feature is untouched:
  • Geo-fence ring escalation (1 → 5 → 15 → 30 → 50 km → country-wide)
    still runs server-side via geoFencingService + the /internal/cron/
    escalate handler.
  • Compatible donor matching, push fanout, atomic accept RPC, 90-day
    cooldown, N-donors-per-N-units, /donations/heartbeat, realtime live
    tracking — all untouched.
  • Recipient pinning the hospital on the map still works — `onMapPress`
    now reads MapLibre v11's `event.nativeEvent.lngLat` and feeds the
    same `setPin({ latitude, longitude })` flow.

What changed
------------

Mobile
  • Added @maplibre/maplibre-react-native ^11.2.1 via `npx expo install`.
  • Removed react-native-maps from package.json / lockfile.
  • app.json:
      - Added the @maplibre/maplibre-react-native config plugin.
      - Added the expo-location plugin with a friendly permission rationale.
      - Added android.permissions: ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION
        explicitly so release APKs don't drop them at build time.
  • New utils/mapStyles.ts:
      - OSM_LIGHT_STYLE  → tile.openstreetmap.org raster tiles, free.
      - OSM_DARK_STYLE   → basemaps.cartocdn.com Dark Matter, free.
      - getMapStyleJSON(isDark) helper for the four map-using screens.
      - zoomFromRadiusKm(radiusKm) helper (replaces deltaFromKm framing).
      - lonLat(lat, lng) helper since MapLibre is [lng, lat] (GeoJSON).
  • Migrated all four map-using screens to MapLibre v11's named exports:
      - (tabs)/request.tsx     → tap-to-pin + pulsing donor dots + hospital pin
      - (tabs)/donors.tsx      → request-pin clustering on the discovery map
      - request/[id].tsx       → static mini-map with hospital marker
                                 (gestures disabled via dragPan/touchZoom/
                                 touchRotate/touchPitch/doubleTapZoom = false)
      - map/live.tsx           → live donor tracking + dashed route line via
                                 GeoJSONSource + Layer (replaces Polyline) +
                                 fitBounds([west, south, east, north]) on
                                 the CameraRef for "fit to markers".

API mapping (react-native-maps → MapLibre v11)
  MapView                      → Map (imported `as MapLibreMap`)
  Marker                       → Marker (imported `as MlMarker`)
                                 coordinate={obj}   → lngLat={[lng, lat]}
                                 anchor={{x,y}}     → anchor="center"|"bottom"
  Polyline                     → GeoJSONSource + Layer { type: 'line' }
  Circle                       → (dropped on donors.tsx; could be reintroduced
                                 via GeoJSONSource + Layer line if needed)
  Region {lat,lon,*Delta}      → Camera initialViewState { center, zoom }
  ref.fitToCoordinates(...)    → cameraRef.fitBounds(bounds, options)
  ref.setCamera({...})         → cameraRef.flyTo({ center, zoom, duration })
  showsUserLocation            → <UserLocation animated />
  onPress(e.nativeEvent.       → onPress(e.nativeEvent.lngLat) — same
    coordinate)                  [lng, lat] order as GeoJSON

Assets refresh
  • icon.png + android-icon-foreground.png + android-icon-background.png
    + android-icon-monochrome.png + splash-icon.png + favicon.png all
    regenerated from the Nano Banana Pro JSON prompts produced earlier.
  • Removed unused react-logo*.png and partial-react-logo.png templates
    that ship by default with create-expo-app.

Build config
  • Added mobile/eas.json with development / preview / production build
    profiles. Required for the post-migration rebuild — `npx expo prebuild
    --clean && eas build --profile preview --platform android` is the
    one-shot to produce a working APK now that MapLibre's native module
    needs to be in the binary.

Validation:
  - `npx tsc --noEmit` clean across all migrated files.
  - MapLibre v11 prop shapes verified against the installed
    @maplibre/maplibre-react-native v11.2.1 typings.

User action required:
  Rebuild the dev client / APK before installing:
      cd mobile
      npx expo prebuild --clean
      eas build --profile preview --platform android
  Without a fresh native build, you'll see
      'MLRNCameraModule' could not be found
  because the previous binary doesn't contain MapLibre's native code.
…test stable

Security & correctness:
- cron endpoints fail closed in production when CRON_SECRET is unset (was open)
- add per-donor heartbeat rate limiter (40/min) on POST /donations/heartbeat
- move 90-day cooldown + availability into accept_blood_request RPC under the
  donor-profile row lock (race-free); keep app-layer pre-check for fast feedback
- fix listRequests pagination total under radius filter (scan->filter->paginate,
  distance-sorted, with honest scanCapped flag instead of a wrong count)
- notificationService: read dead-token from the same chunk as its ticket
  (removes the global-cursor index-drift bug) and prune in bounded batches
- statsController returns null (not 0) for active jobs when the DB read fails
- collapse duplicate geofence "still searching" notification branch

Tooling & deps:
- Express 4 -> 5; express-rate-limit 7 -> 8 (ipKeyGenerator); expo-server-sdk
  3 -> 6; node-cron 3 -> 4; helmet/cors/morgan/express-validator/dotenv latest
- drop uuid dependency for Node's built-in crypto.randomUUID()
- add backend ESLint flat config (ESLint 10) and .nvmrc (Node 20); 0 vulns
- fix deprecated render.yaml `env` -> `runtime`; document CRON_SECRET/TRUST_PROXY

Scrub all emojis from backend code + push-notification copy (non-negotiable);
rewrite notification strings cleanly. Migration: 2026-06-16-cooldown-in-rpc.sql.
- expo 54.0.33 -> 56.0.12; `expo install --fix` aligns all SDK-managed deps
  (RN 0.85.3, React 19.2.3, expo-router 56, reanimated 4.3.1, worklets 0.8.3,
  keyboard-controller 1.21.6, gesture-handler/screens/svg, datetimepicker 9,
  netinfo 12, typescript 6.0.3, eslint-config-expo 56); maplibre -> 11.3.4
- add .npmrc legacy-peer-deps (datetimepicker -> react-native-windows peer
  conflict) so SDK-pinned versions install cleanly in local + CI
- app.json: drop removed-in-56 fields newArchEnabled + android.edgeToEdgeEnabled
  (both are always-on defaults now)
- expo-router 56 dropped react-navigation compat: remove the three
  @react-navigation/* deps; derive the custom tab-bar prop type from
  expo-router's own <Tabs> tabBar callback instead
- tsconfig: drop baseUrl (deprecated in TS 6, removed in TS 7); paths still
  resolve relative to tsconfig and Metro still resolves @/ (verified via export)
- ThemeContext: normalise system scheme to 'light'|'dark' (RN 0.85 narrowed
  Appearance.getColorScheme()); inline absoluteFillObject (dropped from RN types)
- delete dead Expo-template files: themed-text/view, parallax-scroll-view,
  hello-wave, external-link, haptic-tab, ui/collapsible, ui/icon-symbol(.ios),
  use-theme-color, use-color-scheme(.web)

Verified: tsc --noEmit clean; expo-doctor 21/21; expo export (android Hermes
bundle) succeeds. expo lint surfaces 55 issues, almost all from the stricter
React-Compiler-era rules in eslint-config-expo 56 applied to the legacy screens
that the Phase D from-scratch rebuild replaces; the final tree lints clean.
… + zod)

Built alongside the existing app (parallel dirs) so nothing breaks until the
Phase D screen rebuild swaps over.

- tamagui.config.ts: full BludStack design system as a Tamagui config - palette,
  spacing, radius, and dark/light themes imported from the existing constants
  (single source of truth), verified to bundle on SDK 56 / RN 0.85 / New Arch.
- stores/ (Zustand): themeStore, toastStore, authStore replace the 3 contexts.
  Applied zustand-store-ts skill - subscribeWithSelector middleware + separated
  State/Actions interfaces + individual selectors. Self-initialising (no providers).
- queries/ (TanStack Query): client + typed key factory, domain hooks over
  utils/api.ts, and a Supabase-realtime -> invalidateQueries bridge. Aligned with
  tanstack-query-expert skill (custom-hook pattern, global staleTime, invalidate-
  after-mutation; optimistic updates land in Phase D where cache shape is known).
- schemas/ (zod): auth, onboarding/profile, request forms with the donor age gate
  (18-65) and empty-string -> null handling per zod-validation-expert skill.
- ui/ (atomic components): Text/Heading + Card via Tamagui styled() ($token
  themes); Button (label-pulse loading, never a spinner), Surface (glass/blur/flat
  decision tree), Skeleton, PressableScale, BrandMark, layout - Reanimated v4 for
  animation. utils/age.ts shared age helpers.
- Wire QueryClientProvider + a TamaguiThemeBridge (Tamagui theme synced to the
  isDark store) into the root layout.
- eslint: disable only react-hooks/immutability (false-positive on Reanimated
  sharedValue.value writes); all other React-Compiler rules stay on.

Verified: tsc clean, eslint clean (new files), expo export (android) bundles.
Contrast audit (ui-ux-2026 / design-engineering law) caught three sub-AA pairs
on the warm-bone light background:
- textTertiary bone500 2.71:1 -> #7A756C 4.32:1 (clears 3:1 UI floor)
- placeholder  bone500 2.71:1 -> bone600 4.58:1 (clears 4.5:1 text floor)
- warning      plasma600 2.65:1 -> #A8620A 4.49:1 (clears 3:1 UI floor)
Dark theme already passed. Both themes now AA-clean.
Expo Router routes now live in src/app/ (the SDK 56 default, never root app/);
all source dirs (components, contexts, hooks, constants, lib, utils, stores,
queries, schemas, ui, tamagui.config.ts) move under src/. The @/* path alias
now maps to ./src/* so every @/ import stays valid.

Verified: tsc clean, expo-doctor 21/21, android export resolves src/app routing
and the @/ alias.
Adds Input (label-above, animated focus ring, RHF-ready), BloodGroupBadge,
EmptyState (verb-led, one action), ScreenHeader (pill back), the Toast viewport
(reads the store, Reanimated enter/exit, reduced-motion honored), NetBanner,
LoadingScreen (skeleton, never a spinner), and a render-phase ErrorBoundary.

Design-law applied: borderCurve continuous, WCAG-AA palette, a11y on every
pressable, useReducedMotion. tsc + eslint clean.
… pivot)

Root layout now drives navigation from the Zustand stores (initAuth on mount,
realtime->query invalidation bridge, ToastViewport mounted). Tabs rebuilt as
NativeTabs (role-aware, Ionicons via VectorIcon, crimson tint, iOS glass,
scroll-minimize). The 3 old context providers stay wrapped so screens not yet
migrated keep working; they are removed once every screen reads the stores.
Auth store realtime channel renamed to avoid colliding with the old AuthContext
during the transition.
Hero + ascending sheet + step indicator + inline CTA (request-screen parity),
on Tamagui ui + the toast store + zod validation (emailSchema/otpSchema), with
reduced-motion honored on the entrance + brand breath. eslint: scope
no-unescaped-entities to '>' and '}' so natural apostrophes in copy are allowed.
…tatsBanner, SelectSheet)

RequestCard (urgency stripe via semantic token, blood badge, inline accept/
decline), ProfileCard (parent-controlled contact pills, expo-image avatar),
UrgencyBanner (pulsing live-dot, reduced-motion aware), StatsBanner (tabular-
nums, skeletons), SelectSheet (bottom-sheet single-select). tsc + eslint clean.
my-requests: status filter pills + FlashList of RequestCard (control/closure
lever). history: identity-reinforcement stats hero (donations / lives helped /
eligibility) + donation timeline. Both on TanStack Query + the stores + ui
composites, verb-led empty states, a11y, FlashList. tsc + eslint clean.
Add .gitattributes (store LF, check out native — kills the CRLF churn),
.editorconfig, .prettierrc/.prettierignore, and a Node 20 .nvmrc at the root
and in mobile/ (backend already had one). Shared formatting baseline before
CI lands so lint/format stay deterministic across machines.
Mobile (jest, babel-jest transform, @jest/globals imports): blood-compat
matrix inverse-consistency + Rh safety, geo haversine/format/ETA + radius
filter, and the zod schemas (email/OTP gates, donor 18-65 age window with
recipients exempt, post-request bounds). 36 tests.

Backend (jest + supertest): geo + compatibleDonorGroups parity with the
mobile client, and a boot-and-route smoke test (/health 200, 404 JSON, auth
refuses an unauthenticated protected route) with expo-server-sdk stubbed.
14 tests. Backend eslint gains a jest-globals override for __tests__.
- mobile-ci: npm ci -> typecheck -> lint -> jest -> expo-doctor -> Metro export.
- backend-ci: npm ci -> eslint -> jest (geo units + supertest boot smoke).
- schema-check: applies supabase_schema.sql twice (idempotency) against a
  Postgres 16 service behind a CI-only Supabase prelude (auth schema, auth.uid(),
  anon/authenticated/service_role roles, supabase_realtime publication), then
  runs verify_schema.sql and fails on any FAIL row. ON_ERROR_STOP catches DDL,
  RLS, and trigger regressions before Studio.
- eas-build: owner-triggered (tag v* or manual) cloud build via EXPO_TOKEN.

Path filters scope each workflow; concurrency cancels superseded runs.
Root workspace package (private) adds husky + lint-staged. The pre-commit hook
runs scripts/check-forbidden.mjs over staged files and blocks any em/en dash or
emoji from landing - the two rules a linter does not catch. It does not
auto-format (the source is hand-aligned). Repo-wide scan via npm run
check:forbidden. ESLint, typecheck, and tests remain the CI gate.
Enforce the no-em-dash and no-emoji non-negotiables across the existing tree:
replace every U+2013/U+2014 with an ASCII hyphen (box-drawing headers, bullets,
and arrows kept) and strip emoji from the docs. Touches source comments, copy,
and markdown only - no logic changes. tsc, eslint, and both jest suites stay
green; the new pre-commit guard keeps it that way.
The plan was a working artifact, not a deliverable. The README is the single
living document for the app going forward.
Bring the README current: Expo SDK 56 / RN 0.85 / React 19.2, Tamagui +
Zustand + TanStack Query + react-hook-form/zod, MapLibre/OSM maps, the
expo-persistent-background-location live heartbeat, the src/ layout, and the
testing + GitHub Actions gates. Drop the stale REDESIGN_PLAN references.

Add the donor reputation and verified-badge program to the roadmap, and a
zero-to-deploy.md that walks a fresh clone through Supabase, backend, mobile,
EAS build/submit, and CI with an owner action-item checklist.
Add a reputation layer over the server-managed total_donations and is_verified
fields - no new backend surface needed:

- lib/reputation.ts: pure tier mapping (New donor -> Lifesaver -> Regular ->
  Champion -> Guardian -> Legend), next-tier + goal-gradient progress math, and
  a lives-helped estimate (3 per donation). Fully unit-tested (19 cases).
- ui/ReputationBadge: a tone-coloured tier pill with an optional verified check.
- Wired into the donor detail hero, the ProfileCard meta row (replacing the raw
  count), and the user's own profile - which now shows their tier, a verified
  badge, and a progress bar toward the next tier as motivation.

Also fixes the profile 'Lives helped' stat (was showing raw donations) and
tightens utils/api.ts off 'any' onto 'unknown' at the HTTP boundary.
…emnants

- babel: switch to react-native-worklets/plugin (Reanimated v4 moved it there;
  the old react-native-reanimated/plugin path is deprecated on SDK 56). The
  Metro export was re-verified after the change.
- Delete src/constants/theme.ts - leftover Expo-template colours, imported nowhere.
- gitignore: stop ignoring .env.example so the key-free templates are tracked;
  still ignore real .env / *.local secrets.
- mobile/.env.example: drop the misleading Google Maps API key block (maps use
  MapLibre + free OSM/CARTO tiles, no key, no cost) and add EXPO_PUBLIC_API_URL.
- Reword two stale react-native-maps comments now that maps are MapLibre.
The only interactive element left without an accessibility role/label. Every
Pressable in the app now carries both.
From the Phase F review pass:
- live map missed donors who accept after it opens: the realtime listener was
  UPDATE-only, so an INSERT (a fresh acceptance) never repainted the pins. Listen
  on '*'.
- a stationary donor stopped heart-beating: the native tracker only delivers
  fixes on movement, so the 30s cadence never fired when parked. Add a keepalive
  timer that re-pushes the last fix (pushFix's throttle de-dupes real fixes).
- re-arm the location-denied toast once tracking resumes.
- profile: show the coalesced donation count so the stat and the reputation tier
  can't diverge on a partial profile.
- heartbeat rate limiter: tighten 40 -> 15/min and key per (donor, request) so one
  token can't flood a row or fan a flood across requests.
- heartbeat 409 no longer echoes the internal response status back to the client.
Document the verified controls on the live-location flow and the three open
items: confirm Realtime RLS on request_responses, an optional heartbeat velocity
sanity check, and optional local JWT verification.
…sories

Non-breaking fixes only (no core-dependency downgrades). Cleared the critical
shell-quote advisory and the babel/xmldom highs. The remaining advisories are all
in build-time tooling (Tamagui's esbuild, Expo's xcode/uuid config-plugins) that
never ships in the Hermes app bundle; their fixes are breaking and not worth a
core-dep downgrade. tsc, 55 tests, expo-doctor (21/21), and the Metro export all
re-verified clean.
Complete the repo-wide sweep the earlier commit missed: replace em/en dashes in
supabase_schema.sql, verify_schema.sql, every migration, and the backend package
description (comments/strings only, no DDL or logic change). Delete the leftover
Expo-template scripts/reset-project.js (emoji + console output, and it only
resets the app to a blank state). The whole tree now passes the forbidden-content
guard.
Next.js 16 (App Router, React 19, Tailwind v4) in web/, with the app's exact
palette (crimson/onyx/bone), the blood-drop brand glyph, and a fully responsive,
no-emoji landing: hero with geo-fence visual, how-it-works, features, blood
compatibility, reputation tiers, stats, and CTA. Static build is green.

Add mobile/.eas/workflows/create-production-builds.yml (android + ios) and an
explicit apk buildType on the preview profile for installable Android builds.
…-in, app shell, feed

Port the app's logic to the web (NEXT_PUBLIC env): supabase browser client, the
backend API client, zod schemas, blood-data, geo, age, reputation, error-reporter,
and a TanStack Query layer + hooks. Add an AuthProvider (session + own profile +
role helpers), a toast system (replaces alert), and a Skeleton-only UI kit
matching the app (Button, Card, Input, badges, ReputationBadge). Build the email
OTP sign-in (RHF + zod + Supabase), the authed app shell (responsive top nav +
mobile bottom tabs, role-aware, auth guard), and the nearest-first feed. The real
blood-drop logo (from the app splash asset) is the brand + favicon. Build is green.
Build out the functional web app to mirror the mobile app on the same backend:
onboarding (RHF + zod), feed, post-request with a MapLibre click-to-place
picker, request detail (accept/decline/complete, role-aware), my-requests,
donation history, nearby donors, donor detail, profile (reputation + standing),
a live tracking map (donor browser-geolocation heartbeat + recipient realtime
donor pins over free OSM/CARTO tiles), and realtime chat (Supabase, optimistic
send). All on TanStack Query + zod + react-hook-form, Skeleton-only loading,
toast over alert, theme tokens, a11y, no emoji/em-dash. tsc + lint clean.
0.1.1 failed the Android release build with four Kotlin compile errors
(ActivityRecognitionHelper setActivityTransitionType + type inference,
BackgroundLocationService internal-type exposure, module Location? mismatch).
0.1.2 fixes them. App typecheck and expo-doctor stay green.
Add a branded global-error boundary (also gives Next an explicit global-error
module to bundle), a branded not-found page, and a Skeleton route-loading UI for
the authed app (never a spinner). Remove the unused create-next-app default
SVGs. tsc + lint clean.
…availability)

Closes the last mobile-parity gap. RHF + editProfileSchema prefilled from the
current profile, donor age-gate re-validated, reachable from the profile page.
Web now mirrors every mobile screen.
Make the app a fully static SPA so it can host on GitHub Pages: convert the four
dynamic [id] routes (request, donor, map, chat) to query-param routes
(/request?id=...) wrapped in Suspense, since a static host cannot server-render
arbitrary runtime ids. next.config now uses output: export, env-driven basePath
(/BludStack for project pages), unoptimized images, and trailingSlash. Landing
CTAs become next/link so basePath applies, add .nojekyll, and update the feed and
donor links.

Add .github/workflows/deploy-web-pages.yml: builds web/ and publishes out/ to
Pages on push. The static build is green - all 18 routes prerender to HTML.
…cture entry

Replace the create-next-app default README with one documenting the web app, its
stack, dev/build, and the GitHub Pages deployment (secrets, basePath, enabling
Pages). Add web/ to the root README structure.
The web build is the app, not a download page, so the call to action takes you
straight into it. Updated the nav, hero, and final CTA.
0.1.2 still failed the Android build (the ActivityRecognition builder called a
non-existent setActivityTransitionType; the real API is setActivityTransition).
0.1.3 corrects it. tsc + expo-doctor (21/21) green.
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blud-stack-rn Ready Ready Preview, Comment Jun 17, 2026 5:20am
blud-stack-rn-8fin Ready Ready Preview, Comment Jun 17, 2026 5:20am

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 250 files, which is 100 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b69e92aa-6c73-4ca6-8bc9-ff8202318424

📥 Commits

Reviewing files that changed from the base of the PR and between 4a586f5 and 5713624.

⛔ Files ignored due to path filters (7)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • mobile/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
  • web/app/favicon.ico is excluded by !**/*.ico
  • web/app/icon.png is excluded by !**/*.png
  • web/package-lock.json is excluded by !**/package-lock.json
  • web/public/logo.png is excluded by !**/*.png
📒 Files selected for processing (250)
  • .editorconfig
  • .gitattributes
  • .github/ci/supabase-test-prelude.sql
  • .github/workflows/backend-ci.yml
  • .github/workflows/deploy-web-pages.yml
  • .github/workflows/eas-build.yml
  • .github/workflows/mobile-ci.yml
  • .github/workflows/schema-check.yml
  • .gitignore
  • .husky/pre-commit
  • .nvmrc
  • .prettierignore
  • .prettierrc
  • HARDENING_NOTES.md
  • PUSH_NOTIFICATIONS.md
  • README.md
  • REDESIGN_PLAN.md
  • backend/.env.example
  • backend/.nvmrc
  • backend/README.md
  • backend/__tests__/geo.test.js
  • backend/__tests__/health.test.js
  • backend/api/cron/expire-requests.js
  • backend/api/cron/tick-geofence.js
  • backend/api/index.js
  • backend/eslint.config.js
  • backend/jest.config.js
  • backend/package.json
  • backend/render.yaml
  • backend/src/controllers/authController.js
  • backend/src/controllers/donationController.js
  • backend/src/controllers/notificationController.js
  • backend/src/controllers/profileController.js
  • backend/src/controllers/requestController.js
  • backend/src/controllers/statsController.js
  • backend/src/middleware/auth.js
  • backend/src/middleware/rateLimiter.js
  • backend/src/middleware/requestLogger.js
  • backend/src/routes/auth.js
  • backend/src/routes/donations.js
  • backend/src/routes/profiles.js
  • backend/src/routes/requests.js
  • backend/src/server.js
  • backend/src/services/cronService.js
  • backend/src/services/geoFencingService.js
  • backend/src/services/notificationService.js
  • backend/src/utils/countryBounds.js
  • backend/src/utils/supabaseAdmin.js
  • lint-staged.config.mjs
  • migrations/2026-05-12-one-active-commitment-and-mutual-disclosure.sql
  • migrations/2026-05-12-uber-redesign.sql
  • migrations/2026-05-14-donor-max-age.sql
  • migrations/2026-05-14-hardening-pass.sql
  • migrations/2026-06-16-cooldown-in-rpc.sql
  • mobile/.eas/workflows/create-production-builds.yml
  • mobile/.env.example
  • mobile/.npmrc
  • mobile/.nvmrc
  • mobile/README.md
  • mobile/app.json
  • mobile/app/(auth)/index.tsx
  • mobile/app/(tabs)/_layout.tsx
  • mobile/app/(tabs)/donors.tsx
  • mobile/app/(tabs)/history.tsx
  • mobile/app/(tabs)/index.tsx
  • mobile/app/(tabs)/my-requests.tsx
  • mobile/app/(tabs)/profile.tsx
  • mobile/app/(tabs)/request.tsx
  • mobile/app/_layout.tsx
  • mobile/app/chat.tsx
  • mobile/app/donor/[id].tsx
  • mobile/app/index.tsx
  • mobile/app/map/live.tsx
  • mobile/app/onboarding.tsx
  • mobile/app/profile/edit.tsx
  • mobile/app/request/[id].tsx
  • mobile/babel.config.js
  • mobile/components/BloodGroupBadge.tsx
  • mobile/components/Button.tsx
  • mobile/components/Card.tsx
  • mobile/components/CustomAlert.tsx
  • mobile/components/EmptyState.tsx
  • mobile/components/ErrorBoundary.tsx
  • mobile/components/Input.tsx
  • mobile/components/LoadingScreen.tsx
  • mobile/components/NetBanner.tsx
  • mobile/components/PressableScale.tsx
  • mobile/components/ProfileCard.tsx
  • mobile/components/RequestCard.tsx
  • mobile/components/ScreenHeader.tsx
  • mobile/components/SelectSheet.tsx
  • mobile/components/Skeleton.tsx
  • mobile/components/StatsBanner.tsx
  • mobile/components/Surface.tsx
  • mobile/components/ToggleSwitch.tsx
  • mobile/components/UrgencyBanner.tsx
  • mobile/components/external-link.tsx
  • mobile/components/haptic-tab.tsx
  • mobile/components/hello-wave.tsx
  • mobile/components/parallax-scroll-view.tsx
  • mobile/components/themed-text.tsx
  • mobile/components/themed-view.tsx
  • mobile/components/ui/collapsible.tsx
  • mobile/components/ui/icon-symbol.ios.tsx
  • mobile/components/ui/icon-symbol.tsx
  • mobile/constants/theme.ts
  • mobile/contexts/AuthContext.tsx
  • mobile/contexts/ThemeContext.tsx
  • mobile/contexts/ToastContext.tsx
  • mobile/eas.json
  • mobile/eslint.config.js
  • mobile/hooks/use-color-scheme.ts
  • mobile/hooks/use-color-scheme.web.ts
  • mobile/hooks/use-theme-color.ts
  • mobile/hooks/useBackgroundDelivery.ts
  • mobile/hooks/useDonorHeartbeat.ts
  • mobile/hooks/useRequests.ts
  • mobile/jest.config.js
  • mobile/package.json
  • mobile/scripts/reset-project.js
  • mobile/src/__tests__/blood-compatibility.test.ts
  • mobile/src/__tests__/geo.test.ts
  • mobile/src/__tests__/reputation.test.ts
  • mobile/src/__tests__/schemas.test.ts
  • mobile/src/app/(auth)/_layout.tsx
  • mobile/src/app/(auth)/index.tsx
  • mobile/src/app/(tabs)/_layout.tsx
  • mobile/src/app/(tabs)/donors.tsx
  • mobile/src/app/(tabs)/history.tsx
  • mobile/src/app/(tabs)/index.tsx
  • mobile/src/app/(tabs)/my-requests.tsx
  • mobile/src/app/(tabs)/profile.tsx
  • mobile/src/app/(tabs)/request.tsx
  • mobile/src/app/_layout.tsx
  • mobile/src/app/chat.tsx
  • mobile/src/app/donor/[id].tsx
  • mobile/src/app/index.tsx
  • mobile/src/app/map/live.tsx
  • mobile/src/app/onboarding.tsx
  • mobile/src/app/profile/edit.tsx
  • mobile/src/app/request/[id].tsx
  • mobile/src/constants/BloodData.ts
  • mobile/src/constants/Colors.ts
  • mobile/src/constants/Typography.ts
  • mobile/src/hooks/useChatMessages.ts
  • mobile/src/hooks/useChatTyping.ts
  • mobile/src/hooks/useDonorHeartbeat.ts
  • mobile/src/hooks/useLocation.ts
  • mobile/src/hooks/useNotifications.ts
  • mobile/src/lib/errorReporter.ts
  • mobile/src/lib/reputation.ts
  • mobile/src/queries/client.ts
  • mobile/src/queries/donations.ts
  • mobile/src/queries/profiles.ts
  • mobile/src/queries/realtime.ts
  • mobile/src/queries/requests.ts
  • mobile/src/queries/stats.ts
  • mobile/src/schemas/index.ts
  • mobile/src/stores/authStore.ts
  • mobile/src/stores/themeStore.ts
  • mobile/src/stores/toastStore.ts
  • mobile/src/tamagui.config.ts
  • mobile/src/ui/BloodGroupBadge.tsx
  • mobile/src/ui/BrandMark.tsx
  • mobile/src/ui/Button.tsx
  • mobile/src/ui/Card.tsx
  • mobile/src/ui/EmptyState.tsx
  • mobile/src/ui/ErrorBoundary.tsx
  • mobile/src/ui/Input.tsx
  • mobile/src/ui/LoadingScreen.tsx
  • mobile/src/ui/NetBanner.tsx
  • mobile/src/ui/PressableScale.tsx
  • mobile/src/ui/ProfileCard.tsx
  • mobile/src/ui/ReputationBadge.tsx
  • mobile/src/ui/RequestCard.tsx
  • mobile/src/ui/ScreenHeader.tsx
  • mobile/src/ui/SelectSheet.tsx
  • mobile/src/ui/Skeleton.tsx
  • mobile/src/ui/StatsBanner.tsx
  • mobile/src/ui/Surface.tsx
  • mobile/src/ui/Text.tsx
  • mobile/src/ui/Toast.tsx
  • mobile/src/ui/UrgencyBanner.tsx
  • mobile/src/ui/index.ts
  • mobile/src/ui/layout.tsx
  • mobile/src/utils/age.ts
  • mobile/src/utils/api.ts
  • mobile/src/utils/geo.ts
  • mobile/src/utils/helpers.ts
  • mobile/src/utils/mapStyles.ts
  • mobile/src/utils/supabase.ts
  • mobile/tsconfig.json
  • package.json
  • scripts/check-forbidden.mjs
  • supabase_schema.sql
  • verify_schema.sql
  • web/.env.example
  • web/.gitignore
  • web/README.md
  • web/app/(app)/chat/page.tsx
  • web/app/(app)/donor/page.tsx
  • web/app/(app)/donors/page.tsx
  • web/app/(app)/feed/page.tsx
  • web/app/(app)/history/page.tsx
  • web/app/(app)/layout.tsx
  • web/app/(app)/loading.tsx
  • web/app/(app)/map/page.tsx
  • web/app/(app)/my-requests/page.tsx
  • web/app/(app)/post/page.tsx
  • web/app/(app)/profile/edit/page.tsx
  • web/app/(app)/profile/page.tsx
  • web/app/(app)/request/page.tsx
  • web/app/global-error.tsx
  • web/app/globals.css
  • web/app/layout.tsx
  • web/app/not-found.tsx
  • web/app/onboarding/page.tsx
  • web/app/page.tsx
  • web/app/signin/page.tsx
  • web/components/brand-mark.tsx
  • web/components/hero.tsx
  • web/components/map-view.tsx
  • web/components/providers.tsx
  • web/components/reputation-badge.tsx
  • web/components/request-card.tsx
  • web/components/sections.tsx
  • web/components/site-footer.tsx
  • web/components/site-nav.tsx
  • web/components/ui.tsx
  • web/eslint.config.mjs
  • web/lib/age.ts
  • web/lib/api.ts
  • web/lib/auth.tsx
  • web/lib/blood-data.ts
  • web/lib/error-reporter.ts
  • web/lib/geo.ts
  • web/lib/queries.ts
  • web/lib/query-client.ts
  • web/lib/reputation.ts
  • web/lib/schemas.ts
  • web/lib/supabase.ts
  • web/lib/toast.tsx
  • web/lib/types.ts
  • web/lib/use-geolocation.ts
  • web/next.config.ts
  • web/package.json
  • web/postcss.config.mjs
  • web/public/.nojekyll
  • web/tsconfig.json
  • zero-to-deploy.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-app-2026-06-17

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

origin/master holds the pre-session history (merged PRs 1-7). This branch is the
full from-scratch rebuild that supersedes that tree plus the new web app, so the
rebuilt tree is kept wholesale ('ours'); origin/master is recorded as a parent so
its history stays reachable and this PR merges cleanly.
@aashir-athar aashir-athar merged commit 67c1427 into master Jun 17, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant