Change list and test notes

Back to the app launcher

Add only, remove nothing. No existing function, screen, field or Firebase configuration was deleted. The three files you supplied are kept byte for byte as the backup masters under /original/. The working copies are the ones the launcher opens, and every change in them is either a new file, a new function, a wrapper around an existing function (the original stays reachable on window.originalRiderFirebase and window.originalDriverFirebase), or an added column, field or section.

Firebase layout, as found

ProjectUsed byHolds
driver-pro-card-loginRider app, Admin app, and now the Driver app for ride data trips, active_trip_request, pricing, reservations, riders, lost_and_found, support_chats, plus the mirrored drivers and new driver_locations
rider-s-drivers-infoDriver app login and approval drivers/{uid} with the full application, documents and bank details

The Driver app also loads the Firebase compat SDK for its wallet screen, pointed at driver-pro-card-login. That was left exactly as it was.

Root cause

The Rider app writes a request to active_trip_request and trips/{id} in driver-pro-card-login. The Driver app was listening to active_trip_request in rider-s-drivers-info. Two different projects, so the request never arrived, no driver was ever assigned, and Admin only ever saw an unassigned request. Everything else downstream (chat, GPS, payment, ratings) failed for the same reason.

Rider app

Driver app

Admin app

Shared layer and tests

Hardening after the code review

A second pass over the bridges found races that only show up when two people act at the same moment. All of these are fixed and covered by tests:

Second review pass: five defects fixed

Real time driver location on the rider map

The Driver app's device GPS was already working and already publishing. The Rider app was reading the wrong copy of it.

What the Driver app does (unchanged): navigator.geolocation.watchPosition with high accuracy, started when the driver goes online and when a ride is accepted, stopped when the driver goes offline with no ride, logs out or is locked. Every fix writes to four places:

The mismatch: the Rider app only ever read the last of those, the throttled copy carried on the trip record. So the car could move at best once every three seconds, only when the whole trip record changed, and it froze completely if that one write was skipped. The live per driver feed that the Driver app was already publishing was not being read by anyone except the Admin dashboard.

The fix: the Rider app now subscribes to driver_locations/{driverId} for the driver assigned to its own trip, so the car moves as the driver moves. Details:

Rider payment information

The payment data was always being written. Nothing in the Rider app ever displayed it: the rider only saw the fare estimate before the ride and the tip window at the end. Traced end to end, the money for a ride lives in one place, trips/{tripId} in the canonical project, mirrored on active_trip_request while the ride runs:

FieldWritten byWhen
fare, estimatedFareRiderwhen the ride is requested
paymentMethodRiderrequest, and whenever the selector changes
paymentStatusRider, DriverPENDING, AUTHORIZED on accept, CHARGED or CASH_DUE on completion, PAID at the end
tipAmount, driverPayoutRiderwhen a tip is added
finalFareDriveron completion, fare plus tip
paymentUpdatedAtRider, Driverevery payment change
paymentReference (new)systemissued once per trip on completion, derived from the tripId
paymentAttempts, paymentFailureReason, lastPaymentAttemptAt (new)systemonly when a payment fails and is retried

What was added, all reading that same record:

Road distance and road time, from the routers already in the project

Two road routing services were already configured here, and both are now used for what they are good at. Nothing new was added, no API key, nothing paid.

The rider quote is now priced only from a road route. Straight line distance and average speed guesses are not used for it at all. If neither router answers, the ride is not created and the rider is told to check the addresses: better than the old behaviour, which quietly used 5 miles, 10 minutes and $18.50.

The driver's pickup distance and ETA are now real. The request card used to read "~5 mins (1.8 miles)" because nothing ever wrote pickupDistance or pickupEta. The road route is worked out from the driver's current GPS to the rider's pickup, shown on the request card, refreshed as the driver gets closer, and saved to the trip once the driver owns it so the rider and Admin see the same numbers.

Estimated and actual stay apart on the one trip record: estimatedRoadMiles and estimatedRoadMinutes from the road route before the ride, actualTripMiles and actualTripMinutes from the driver's GPS and the clock afterwards, alongside the existing fields. The fare formula was not touched.

Upfront pricing, quoted from the road route

What was wrong with the fare. The request carried miles: document.getElementById('miles').value || 5, minutes: ... || 10 and a fare of "$18.50" whenever the fare box still showed $0.00. So any ride where the route lookup did not answer, which is every ride if the map key is restricted or billing is off, was quoted at $18.50 for 5 miles and 10 minutes, whatever the real trip was.

What it does now. The rider is quoted before the ride from the expected ROAD ROUTE, using the pricing table Admin already controls. No second pricing system: the arithmetic is the one the Rider app has always used, base plus distance plus time, floored at the minimum, plus the booking fee, times the period multiplier, with an airport flat rate replacing all of it.

For the reported trip, Riviera Beach to Fort Lauderdale airport with no route service answering, the quote is $74.26 for an estimated 60.97 miles and 73 minutes, against the old $18.50.

Why a 57 mile drive still read 4.2 miles

The first version of the odometer only ran between Start Trip and Complete Trip. Two things followed from that, and both produced the old placeholder numbers:

Tracking now runs for the whole time the driver owns the ride, from the moment they accept. The single track is marked at arrival and at Start Trip, so it splits into the drive to the rider and the ride itself without a second GPS system. The ride is measured from Start Trip when it is pressed, and from the arrival when it is not, so a forgetful driver still produces a real distance. If startedAt is missing at completion it is filled in from the arrival time and the record says startedAtEstimated, so the duration is real rather than zero.

The trip card is also refreshed every second from the live measurement, in every stage of the ride, so the app's own placeholders can never be what the driver reads.

Measured, not displayed. The engine is checked against the reported trip: Riviera Beach to Fort Lauderdale airport, a road route of 57.45 miles where the straight line between the two addresses is 48.78. Fed a realistic GPS trace of that drive, the odometer reports 57.45 miles, an error of 0.00%. A ten mile leg reports ten miles. Nothing was hard coded to display 57: the same code measures whatever is driven.

Real mileage and real trip time

What was wrong. Nothing in any of the apps ever measured how far a car drove. The driver screen read activeTrip.tripMiles || "4.2 mi" and activeTrip.tripDuration || "12 mins", and nothing anywhere wrote tripMiles or tripDuration. So every trip displayed those two constants. The only distance in the system was the route estimate the rider's map produced before the ride, which is not the distance driven, and no duration was calculated at all.

Mileage. While a ride is in progress the driver app now accumulates real distance between consecutive GPS fixes using the haversine formula, on the same fixes the live map already publishes. No second GPS system was created. Bad data is filtered: movements under 6 m are jitter, fixes accurate to worse than 100 m are not measured with, and anything implying more than about 139 mph is rejected. A long gap counts the straight line rather than losing the distance. Two impossible steps in a row re-anchor the odometer, so one bad fix, or a stale position left over from before the ride, cannot wedge it at zero for the whole trip.

Time. startedAt is stamped when the driver presses Start Trip and completedAt when they end it, and the duration is completedAt - startedAt. GPS frequency cannot shorten it: a twenty minute ride reads twenty minutes even if only three fixes arrived.

Both are saved to the canonical trips/{tripId} record as distanceMeters, tripMiles, tripMilesValue, tripDurationSeconds, tripDurationMinutes and tripDuration, with the GPS fix counts kept for auditing. The driver sees them live on the trip card and again on the completion summary, the rider sees them on the payment card, and Admin shows them on the live ride and in each rider's trip table.

Double clicking the wrong file

The package contains two files that both look like the Admin app. Only one of them can be opened by double clicking, and the other one failed silently, which is indistinguishable from a broken login.

Telling you why a login failed

The Admin login card has a Run connection test button. It reports the build that is loaded, whether the page was opened as a local file or over http, whether the shared code loaded, whether the email is on the admin list, whether each Firebase project can actually be reached, what the last sign in attempt returned from each project, and whether the browser thinks it is online. One press answers "why can I not log in" without another round trip.

Running Admin on your own computer

Copying admin.html to a computer and opening it did not work, for two reasons.

So there is now a download page with two forms of the same working app: admin-standalone.html, one file that opens by double click because the shared code is inlined and no local import remains, and riders-admin-package.zip, the normal layout with the shared folder and a one command local server. The Firebase configuration, both projects, the login and every feature are the working code, copied as is.

No cash on the rider screen, even on an old trip

Trips taken before the card only change still carry paymentMethod: "Cash" in the database, and the Rider app was faithfully printing it. A rider should not be shown a payment type they can no longer use, so:

Card only, and the driver never handles money

Riders pay by credit or debit card. Cash is gone from the Rider app, and the driver is never asked to collect anything.

Rider app

Driver app

Trip record

driverEarnings now sits next to driverPayout, carrying the same number under the name the dashboards ask for. Payment state still only moves forward, and a card trip that has been charged or paid can never fall back to a cash state, even from a stale mirror. Older cash trips still read and settle exactly as they always did: history is not rewritten.

The stuck ride, the wrong payment status and the zero total

Three symptoms, three separate causes, none of them a Firebase project mismatch. Rider, Driver and Admin all read the ride data from driver-pro-card-login, and Admin additionally reads registrations from rider-s-drivers-info. That part was correct.

"Another ride is active right now"

active_trip_request is a single shared node. If a tab closes mid ride, or a ride is finished only in the trip record, the pointer is left aiming at a ride that is over, and then nobody can book. Forever. The stored trip is the authority now: a pointer to a finished ride is released, and a request with no driver that has been sitting for more than half an hour is treated as abandoned, marked cancelled in history and released. A genuine ride in progress still blocks, and the rider is told which ride, what state it is in, which driver has it and how long it has been running.

Admin said PENDING while the Rider app said AUTHORIZED

Both were reading the correct field on the correct record. They were reading different rides: Admin quoted the newest trip by timestamp, the Rider app showed the ride that device was on. Admin now quotes the ride that is happening now when there is one, names the trip id beside the status, and marks it as a live ride. With nothing running it falls back to the newest finished ride, as before.

Trips: 2 / $0.00

The riders table showed only money that had settled, and neither of that rider's rides had completed. It now reads "2 rides, $50.90 in fares" with the settled amount beneath, so a fare is never invisible just because the ride has not closed out yet.

Payment Method: Not specified

That rider record carries no card because it is the record the Rider app creates on the device, not the registration record that holds the card. Where the account has no card but the rider's rides do, Admin now shows the method used on the ride and says plainly that it came from a ride rather than from the account.

Fares in the Rider Information view

The payment summary only added up rides that had finished, so a rider with two rides still running read $0.00 for everything and looked as though the fare was missing. It now separates what is settled from what is not:

In the trip table inside the view, a ride that has not finished now shows its fare marked "not settled" instead of a bare $0.00 in the Final Fare column.

Who and how much, in both trip tables

Live Active Rides & Trip Requests and Ride Request & Trip History now carry, for every ride: the rider name with the rider id underneath, the driver name with the driver id (and plate) underneath, and the fare split into three columns.

A $32.40 fare reads $12.96 to the driver and $19.44 to the company, and the two always add back to the fare. Tips sit on top and go entirely to the driver, so they never reduce the company share. These are the same numbers the Driver app pays out and the dashboard totals already used, now from one shared calculation rather than three copies of the arithmetic.

Payment method on the rider record, ready for Stripe

The registration page always wrote safe card metadata, and it never wrote a card number, a security code or an expiry date. Those stay in the form and are thrown away. What was missing was one agreed shape, so the rider record now carries:

riders/{uid}
  savedCard             "Visa \u2022\u2022\u2022\u2022 4242"
  paymentMethodType     "card"
  paymentMethodLabel    "Credit/Debit Card"
  paymentMethodStatus   "pending"  (no provider yet)  or  "ready"  (Stripe holds it)
  paymentMethodId       ""  or  "pm_..."
  paymentMethodAddedAt  timestamp
  cardBrand, cardLast4  kept, nothing was removed

Admin reads that shape whatever wrote the record, shows the Stripe id and marks a Stripe backed method as ready. When a rider row has no card at all it now says which fields it looked for and which source the row came from, instead of leaving "None saved at registration" hanging.

How Stripe drops in

shared/stripe-config.v1.js holds one value, the publishable key. Paste it there and the registration page loads Stripe.js, mounts Stripe's own card field, hides its own card inputs (they stay in the page), and turns the card into a PaymentMethod. The card is typed into an iframe served by Stripe and never touches this site. Firebase then stores only the brand, the last four, the pm_ id and a status.

Leave the key empty and nothing changes: the current form keeps working and keeps storing only brand and last four.

The secret key never goes in this repository. Creating a PaymentMethod needs only the publishable key. Charging it, refunding and paying drivers need the secret key and must run on a server. The trip record is already shaped for that: PENDING, AUTHORIZED, CHARGED, PAID, one payment reference per trip.

Locked out of a login

Both login screens can now get you back in without anyone deleting or recreating a Firebase account:

The floating trip messenger

A 💬 button now sits in the top right corner of the Rider app and the Driver app. It stays hidden until a driver accepts the ride, then it appears with an unread badge, and it opens a popup messenger for that trip.

Every place a rider can exist

The rider panel now collects riders from four sources and merges them, richest record first:

  1. riders/{uid} in the registration project, written by the registration page;
  2. riders/{riderId} in the canonical project, written by the Rider app;
  3. users/{uid} with role: "Rider", in either project, which the registration page also writes;
  4. riders rebuilt from the trips they took.

The note under the table now names each source with its count, or NOT READABLE and the Firebase error code when the rules refuse a read. adminRiderSources() in the browser console returns the same thing as data. An empty panel can now be explained in one glance instead of guessed at.

The registration app writes to the other project

The registration page (registrationlogin.html) creates the Firebase Authentication account and then writes riders/{uid}, drivers/{uid} and users/{uid} into rider-s-drivers-info. The Admin dashboard was reading riders from driver-pro-card-login only. Drivers appeared because the driver list was already merged across both projects; riders were not. That is the whole reason the Registered Passengers table stayed empty while people were registering.

Admin now listens to riders in the registration project as well and merges three sources: registered riders, the records the Rider app saves, and riders rebuilt from the trips they took. The note under the table counts each source.

The registration record also carries safe card metadata, which is now shown: cardBrand, cardLast4, paymentMethodType, paymentMethodStatus, paymentMethodId and paymentMethodAddedAt. The registration page deliberately does not store the card number or the security code, and Admin displays only the brand and the last four digits.

One thing to fix in the registration page: it also writes the rider's and driver's password into the database in plain text. Admin never displays it and warns when it finds one, but it should not be there at all: the Firebase Authentication account is what signs people in, so that field is redundant.

Why the rider panel looked empty

The panel was correct, the data was not there. Two causes, and the old code could not tell them apart:

On top of that, the passenger list no longer depends on the riders node existing at all. Every trip carries riderId and riderName, so Admin rebuilds the list from ride history and merges the saved profiles on top when they are readable. A rider who has taken a trip is always listed, with their trips, fares and payments, and the note under the table says which source the rows came from.

Rider information and payments in Admin

What the rider record already holds. One writer, one path: the Rider app writes riders/{riderId} in the canonical project with riderId, fullName, email, phone, createdAt, lastSeen and source. The rider id is generated on the device and kept, so it survives page loads. Admin was already listening to that exact path, so there was no database mismatch to fix: the data was simply thin, and the dashboard only showed four columns of it.

What the payment system holds. There is no payment provider connected, so there is no card brand field, no last4 field, no payment method id and no processor transaction id. What exists per trip is paymentMethod (the label the rider chose), paymentStatus, paymentReference (derived from the tripId), paymentUpdatedAt, estimatedFare, finalFare, tipAmount and, after a failure, paymentAttempts. Reporting that is the honest answer: no card storage was invented to fill the gap.

Added to Admin, reading those same records:

The Rider app also gained a small My Details box (name, email, phone) that writes into the same riders/{riderId} record, because nothing in the apps ever collected an email or a phone number. It merges, so a blank field never wipes something already stored, and no second rider record is created.

The missing driver profile

A Firebase Authentication account and a database record are two different things. Creating the user, in the Firebase console or anywhere else, does not create drivers/{uid}. Nothing in these three apps ever created it: every drivers/{uid} write in the whole project is an update() for approval, presence or GPS that assumes the record already exists. There has never been a registration flow in the Rider, Driver or Admin file, only error messages that mention one. That is why a driver can sign in successfully and still be told there is no profile.

Added, without deleting or resetting anything:

Run driverProfileDiagnostics() in the browser console on the Driver app, after signing in, for a straight answer about the live database: the account, the UID, which project holds the session, whether the profile exists, every path that was checked with found, absent or denied, and any other record carrying the same email.

Driver sign in

Same shape as the admin problem. The Driver app signs in against rider-s-drivers-info and nowhere else, so a driver whose Firebase account was created in driver-pro-card-login could never get past the login screen, whatever they typed. What changed:

Run driverSyncStatus() in the browser console on the Driver app to see the signed in account, which project holds the session, where the profile was found, the approval state, the current trip and the last GPS fix.

Admin sign in

The Admin dashboard used to sign in against one project only: driver-pro-card-login. If the admin user was created in rider-s-drivers-info instead, the login could never succeed, and the only feedback was a bare "Login failed" box. Two changes:

The authorized address is still driverandridersapp@gmail.com. Typing anything else says so instead of failing silently. If a second admin address is needed, it goes in the ADMIN_EMAILS list in the Admin app.

What to check on the live databases

Back to the app launcher