Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Djir app icon

Djir

A ride-hailing app for Zagreb: Expo and React Native on a serverless Postgres backend, with Stripe payments and ML-priced fares.

Expo React Native React TypeScript Stripe Tests License: MIT

Live tracking, animated: the simulated car drives to the pickup and on to Trg bana Jelačića while the sheet counts down from Arriving in 7 Mins to Ride complete

One ride from set-off to drop-off, rendered from the app's real tracking components with fixture data. The web build draws the whole route on a grid; on iOS and Android the map shows the current leg on a real map.

🚕 Overview

Riders sign up, find themselves on the map, pick a destination and a pickup time, and choose a driver. The fare comes from a quote the server signs. They pay in the app with Stripe, then watch the driver arrive. Scheduled rides can be cancelled for a refund until the driver sets off, and a local notification reminds the rider shortly before.

The mobile app and its backend live in one codebase: Expo Router serves the screens and the API routes. With a model endpoint set, prices come from two XGBoost models (ML platform). The committed models were trained locally by the same code the Databricks notebooks run. A deterministic fallback prices whenever the endpoint is unset (the default), slow or down.

✨ Features

🚗 Live ride tracking

Your driver has arrived: the car waits at the pickup
Driver arrived
6 Mins to destination: the car halfway along the trip
On the trip
Pickup at 09:00 today: the driver sets off at 08:53, free cancellation until then; the car parked at its start
Scheduled · free cancel
Ride cancelled, the fare refunded to the card
Cancelled · refunded
  • One source of truth. A ride's phase (scheduled → en route → arrived → on trip → completed) is a pure function of the ride row and the clock. No phase is stored and no background job advances it, so reopening the app after a crash picks the ride up where it is.
  • The car you picked is the car you track. The driver card, the Book Ride screen and the first "Arriving in" show the same pickup time: booking stores the minutes the driver card showed.
  • Honest about the simulation. Djir has no driver app, so the car's position along the route is simulated, and the screen says so. The timeline itself comes from the booking: the pickup minutes and trip ETA the rider was shown, and the payment time (or the booked slot, for a scheduled ride).
  • Four ways in. Go Track after paying, the Home banner, a ride in history, or a tapped reminder.

🗓️ Ride scheduling, cancel and reminders

The pickup-time picker over Find ride: today, 09:00, in 15-minute slots on the Zagreb clock
Pick a pickup time
The drivers re-priced for 09:00, marked High demand ×1.4
Re-priced for that time
Ride scheduled over Book Ride: free cancellation until the driver sets off
Ride scheduled
The Home banner (Michael is arriving in 5 min) above the Live and Upcoming ride cards
Home banner + ride cards

Fares in these renders come from the fallback formula (lib/pricing.ts); the chart below uses the ML models. The last image stacks Home's banner over the ride cards.

  • 15-minute slots. From 30 minutes to 7 days ahead, labelled on the Zagreb clock. On the October night the clocks go back, the repeated hour reads "02:15 CEST" and then "02:15 CET".
  • Time-aware prices. Choosing a time re-prices the drivers, and a surge shows as "High demand · ×1.4".
  • Free cancellation. A Stripe refund until the driver sets off.
  • Local reminders. A notification 10 minutes before the driver leaves; tapping it opens the ride. Reminders are rebuilt from ride history each time it loads, so they follow a cancel, a new device or a sign-out.

📈 Priced by time and weather

Fare for Tresnjevka to Donji grad by pickup time, on a clear Tuesday, a clear Saturday and a rainy Saturday

Tresnjevka → Donji grad costs €5.86 on a quiet Tuesday afternoon and €9.74 on a rainy Saturday night. The chart is drawn from the committed models by ml-platform/scripts/plot_price_by_time.py, and a test fails if it goes stale. Every price uses the Zagreb wall clock, even when the request arrives as a UTC instant.

Model Predicts Test MAE Naive baseline Djir's own heuristic
ETA trip duration 3.18 min 7.58 min (model −58%) 3.12 min (model +2%)
Surge price multiplier 0.065× 0.263× (model −75%) 0.193× (model −66%)

The metrics above are what ml-platform/models/metrics.json records; a test ties this table to it. The honest reading: the data is synthetic, and the ETA model does not beat the heuristic that shares the simulator's physics (3.18 vs 3.12 min). The surge model beats it by a wide margin, partly because the heuristic's own surge is miscalibrated. The committed models load cleanly with the pinned scikit-learn and XGBoost versions.

→ ML platform & runbook · Architecture & decision records

🧭 How it works

Booking and payment. The server signs the quote and charges only the signed amount.

sequenceDiagram
    autonumber
    participant App
    participant API as Expo API routes
    participant Stripe
    participant DB as Postgres (Neon)
    App->>API: POST /predict-price {pickup, dropoff, scheduled_at?}
    API-->>App: fare_cents · ETA · surge · quote_token (HMAC, 10 min)
    App->>API: POST /ride/book {quote_token, driver, card} + Clerk JWT
    API->>DB: INSERT ride (pending): money and rider from the token and the JWT
    API->>Stripe: create PaymentIntent (unconfirmed, card only)
    API->>DB: save the PaymentIntent id
    API->>Stripe: confirm → succeeded | 3-D Secure
    App->>API: POST /ride/confirm → the ride is paid
    Note over API,DB: GET /rides settles anything left pending (a crash, a lost connection)
Loading

Ride phases, computed from the ride row and the clock (no phase is stored):

stateDiagram-v2
    direction LR
    state "en route" as en_route
    state "on trip" as on_trip
    [*] --> scheduled: booked for later
    [*] --> en_route: booked for now
    scheduled --> en_route: slot − pickup minutes
    scheduled --> cancelled: rider cancels (refund)
    en_route --> arrived: + pickup minutes
    arrived --> on_trip: + 1 min
    on_trip --> completed: + trip minutes
Loading

🛡 Built to be trusted

  • You pay exactly the price you saw. The server signs each quote; booking charges the signed amount and ignores any amount or user id in the request.
  • Every succeeded payment has exactly one visible ride. A reserve-first booking flow and a reconcile step keep that true through 11 crash, race and backlog scenarios, tested against a real Postgres (PGlite).
  • Authenticated API. Clerk session tokens are checked on the server without a network call; only the driver list, quotes and a setup check (it names missing settings, never their values) are public.
  • One clock. Hours and weekdays come from a dependency-free Zagreb clock that matches the tz database for every hour from 2020 to 2035.
  • One price formula, two languages. The TypeScript fallback prices to the cent like the Python library on 442 golden trips (30 of them straddling the DST switches, 12 with an ETA on a rounding tie) plus 30 rounding cases, all exported from Python.
  • Tests that bite. Each critical v1.0 bug, put back into the code by npm run mutants, makes its tests fail.
  • Enforced gates. 100% line coverage for lib/, server/ and the API routes, and 90% for hooks, services and the tracking, scheduling and payment components.
  • Enforced layers. ESLint keeps lib/ pure (no React, no I/O, no current time) and keeps server code out of the app bundle.

The full story is in docs/REVIEW.md (what was broken), docs/BUILD_PLAN.md (how v1.1 was specified and graded) and docs/architecture.md (the decisions).

🛠️ Tech stack

Layer Technology
App React Native 0.86 (New Architecture) · React 19 · Expo SDK 57 · Expo Router 57 (screens and API routes) · TypeScript
UI NativeWind 2 (Tailwind) · Plus Jakarta Sans · @gorhom/bottom-sheet 5 · react-native-maps · Reanimated 4
State Zustand
Auth Clerk via @clerk/expo (email + password with email verification, Google sign-in), verified server-side with WebCrypto
Payments Stripe Payment Sheet (deferred intent) · stripe on the server
Database Neon serverless Postgres
Reminders expo-notifications (local)
ML platform Databricks · Delta Lake · MLflow · Unity Catalog · XGBoost · FastAPI
Quality Jest (server + client projects) · Testing Library · PGlite · pytest · ESLint · GitHub Actions

📁 Project structure

app/          screens and API routes: (api)/ · (auth)/ · (root)/ incl. track-ride
components/   UI: props in, JSX out (TrackingSheet, ScheduleModal, Payment, RideCard, …)
hooks/        React state and effects (useRides, useRideTracking, useDriverQuotes, …)
lib/          pure logic: Zagreb clock, pricing, schedule, ride phases, reminders
services/     client I/O: API calls, the payment-sheet handler, reminders, auth
server/       server only: auth, signed quotes, payments and reconcile, validation
store/        Zustand stores, each with reset()
db/           migrations/001_v1_1.sql (a fresh database uses the root schema.sql)
docs/         review · build plan · architecture · gallery (render source) · images
ml-platform/  simulator · Databricks notebooks · models · FastAPI serving · pytest
__tests__/    lib · server · api · db · services · store · components · hooks · screens · meta

🚀 Getting started

Prerequisites: Node.js 24 (see .nvmrc; 22.13 or newer also works), and Expo Go (SDK 57) on a device or an iOS or Android simulator. The maps and Stripe (including 3-D Secure returns) work best in a native build: npx expo run:ios or npx expo run:android (Android maps then need GOOGLE_MAPS_ANDROID_API_KEY).

git clone https://github.com/FilipKalcic1/djir-fullstack.git
cd djir-fullstack
npm install
cp .env.example .env.local      # then fill it in: each variable is explained there
Variable Where to get it
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY · CLERK_JWT_KEY Clerk → API Keys (the JWT public key, PEM)
QUOTE_SIGNING_SECRET any random string of 32+ characters (openssl rand -hex 32)
DATABASE_URL Neon connection string
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY · STRIPE_SECRET_KEY Stripe (test keys)
EXPO_PUBLIC_PLACES_API_KEY · EXPO_PUBLIC_DIRECTIONS_API_KEY · GOOGLE_MAPS_ANDROID_API_KEY Google Cloud
EXPO_PUBLIC_GEOAPIFY_API_KEY Geoapify (history thumbnails; optional)
EXPO_PUBLIC_API_ORIGIN where the API routes are served. Leave it empty for npx expo start and Expo Go: the app then calls the dev server that served it. Required for release iOS and Android builds, set to your deployed origin (left empty, their API calls fail closed); the web build can leave it empty
ML_ENDPOINT_URL · EXPO_PUBLIC_TRACKING_SPEEDUP optional: the model endpoint (unset, the fallback formula prices), a demo speed-up for tracking

Database. Run schema.sql on a new database, or db/migrations/001_v1_1.sql on a v1.0 one (psql "$DATABASE_URL" -f … or the Neon SQL editor).

npx expo start                  # then i / a, or scan the QR code with Expo Go

📱 Run it on your iPhone

  1. On the PC: npm install, then npx expo start, and leave it running.
  2. On the iPhone: install Expo Go from the App Store (this project needs SDK 57), then scan the QR code in the terminal with the Camera app, or from inside Expo Go.
  3. The phone and the PC must be on the same Wi-Fi. If the app cannot load (a guest network, a VPN, Windows Firewall blocking Node.js: allow it on private networks), run npx expo start --tunnel instead.

With .env.local still empty, the app opens on Setup needed instead of crashing. It lists each app key as Set, Missing or Not a valid key, with what it unlocks and where to get it, and asks the dev server which server settings are still missing (Check again asks once more). After adding keys, stop the server (Ctrl+C), start it again with npx expo start -c and reload the app (shake the phone → Reload).

Add to .env.local What it unlocks
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY (pk_test_…) the app itself: sign-up and sign-in. It is the only key the app needs to open
DATABASE_URL (Neon), then run schema.sql on that database once the drivers on the confirm list and, with CLERK_JWT_KEY, your ride history
QUOTE_SIGNING_SECRET (32+ characters) + CLERK_JWT_KEY prices and booking
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY + STRIPE_SECRET_KEY (test keys) paying for a ride (test card 4242 4242 4242 4242); until then Book Ride names the key to add
EXPO_PUBLIC_PLACES_API_KEY optional: address search. Without it, From and To offer a list of popular Zagreb places

In Expo Go the map is Apple Maps (no key needed), EXPO_PUBLIC_API_ORIGIN stays empty, and ride reminders arrive as Expo Go notifications.

📜 Scripts

Command What it does
npm start · npm run ios · npm run android · npm run web run the app
npm test both Jest projects (server: Node + PGlite; client: jest-expo + Testing Library)
npm run check typecheck, lint (with the layer rules), tests with coverage gates: what CI runs
npm run mutants put each critical v1.0 bug back and require its tests to fail
npm run docs:shots re-render the README images from docs/gallery (needs Chrome, Chromium or Edge; CHROME_PATH picks one); its manifest lets a test fail when a render is stale or edited by hand
cd ml-platform && pip install -r requirements-dev.txt && pytest the ML platform's tests (Python 3.12, as in CI; a venv is advised, see ml-platform)

🎨 Design

The app was designed in Figma first. These are design screens for the flows not rendered above; the images above are the built components.

Onboarding: best car in your hands
Onboarding
Get started
Get started
Current location
Find a ride
Choose a driver
Choose a driver
More design screens
Onboarding: the perfect ride
Onboarding 2
Onboarding: your ride, your way
Onboarding 3
Ride details
Ride details
Ride history
Ride history

🗺️ Roadmap

  • Real-time driver tracking (simulated driver position), awaiting on-device QA
  • Ride scheduling with free cancellation and refunds, awaiting on-device QA
  • Push notifications: local ride reminders ship today; remote push needs a sender and an EAS project
  • In-app chat with drivers: needs a driver app, so it is not faked with a bot

📄 License

Released under the MIT License.

👤 Author

Filip Kalčić — @FilipKalcic1

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages