Skip to content

v3: realtime order changes, reassignment, order chat and bookings - #114

Merged
roncodes merged 7 commits into
feature/v3-redesignfrom
feat/v3-realtime-order-changes
Oct 14, 2026
Merged

roncodes merged 7 commits into
feature/v3-redesignfrom
feat/v3-realtime-order-changes

Conversation

@roncodes

@roncodes roncodes commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

What and why

Navigator v3's side of the order operations program (shared contract: realtime order.changed, reassignment, order chat, service bookings, per-item meta). An audit of v3 found realtime switched off, order screens that never refetched, no v3 push handling, reassigned orders left on the device, an unreachable Offers screen, and a proof-capture loop. This PR fixes those and adds the contract's driver-facing surfaces.

1. Realtime was off

  • App.tsx now passes the same SocketCluster config v2 builds (resolveConnectionConfig('SOCKETCLUSTER_*')) to V3App; before, SocketProvider mounted with enabled=false.
  • SocketProvider holds the socket in state. Child effects run before the provider's, so useDriverChannel got a no-op and never subscribed again; listen now changes identity once the socket exists and subscribers re-run. A reconnect bumps live refresh. (Test: SocketProvider.test.tsx mounts a subscriber before the socket and receives an event.)

2. Event map

Added order.changed (summary, changes), order.updated, order.driver_unassigned, order.assigned_notification (OrderAssigned notification), and chat message events. The order id is also read from data.id. Unknown events are still dropped but logged in __DEV__. The open order is followed on order.{id}.

3. Order detail refetches

The order (GET orders/{id}), tracker and next step reload on order events and on focus.

4. v3 push handling

V3App takes subscribeToPush (App.tsx adapts v2's NotificationProvider). A foreground push is applied as the socket event it duplicates. A tapped push opens the order (order_changed, order_assigned, …), the Orders list (order_unassigned) or the conversation (chat_message_received) through a root navigation ref. If the container isn't ready yet, the navigation waits for it.

5. Reassignment

order.driver_unassigned and order_unassigned remove the order. Driver-scoped list queries now reconcile the store: rows they listed before and no longer return (within the page's window) are removed. Active ones are marked reassigned. Screens holding the order pop to the top of their stack, and Orders and Today show "Order FLE… was reassigned".

6. Changed-order banner and navigation

The order shows a "This order changed" banner with the server's summary. When changes includes dropoff, the banner offers Re-open navigation. Navigate targets the current stop (current_waypoint, else the drop-off). The hand-off now takes the order id and reads the order's current destination from the store, so it no longer relies on a snapshot in route params.

7. On the order

  • Item quantities (meta.quantity), item notes, unavailable items and substitutes (meta.substitute_for).
  • Cash on delivery: the amount is payload.cod_amount, shown as "to collect" per meta.payment.state. Orders without meta.payment (older ones) treat the amount alone as due. The completing step waits for I collected ….
  • Delivery instructions (drop-off place meta.instructions / instructions) and order notes.
  • The scheduled window or booking slot.
  • Customer Call and Message, and Message dispatch.

8. Offers

Only adhoc orders are offered: they appear on the Offers screen and are taken with start + assign, as before. An order assigned to the driver is simply theirs; there is nothing to accept. Offers is reachable from the Orders screen. (Direct offers with a countdown were built here and removed in d6282b1, together with fleetbase/fleetops#398.)

9. Chat

  • Channels are followed on chat.{id} and chat_channel.{id}, and chat pushes are routed.
  • The Inbox badge is v3's own unread total; v2's ChatContext count is no longer used.
  • Order-linked threads (meta.order_public_id) are labelled in the Inbox and the conversation header. On the order, the driver thread backs "Message dispatch" and the group thread backs "Message customer".
  • System messages (meta.system) render as notices, and senders carry role labels (Store, Customer, You).

10. Flow

  • "Finished" comes from the order config's complete flag, falling back to the old sets. This replaces the hard-coded sets in orderStore, OrdersScreen and useOrderRecord.
  • Configs are cached outside React so the store's views can use them, and list queries prefetch them.
  • The next step comes from GET orders/{id}/next-activity when the server answers. It has already evaluated the flow's logic, so a branch is offered as one button per option instead of "ask dispatch". Otherwise the app falls back to the client-side sequencer.
  • Proof loop: a capture is recorded against its order and activity (persisted). Returning from capture advances once, with the proof id, instead of reopening capture.

11. Bookings

Orders with meta.is_booking read as a Job, with the slot and a Bring to the job checklist built from the order's entities. booked and provider_enroute have registry entries and labels, and confirmed ranks second in the lifecycle.

Blocked on / depends on server work

  • order.changed broadcast and the order_changed / order_unassigned pushes, plus order.driver_unassigned on driver.{public_id} (Storefront edit service and Fleet-Ops, 1.1–1.3). Until then the banner never shows and reassignment relies on list reconciliation.
  • Order chat: the app can't open an order thread. POST v1/chat-channels accepts no meta, and no driver-side orders/{id}/chat route exists, so "Message customer" uses a group thread only when one exists (else SMS to the customer's phone) and "Message dispatch" falls back to a new conversation. Channel meta is already returned. Participant meta.role and message meta.system are not yet on core-api's public ChatParticipant / ChatMessage resources, so role labels and system notices appear once those ship.
  • meta.payment (contract 3) for the "to collect" state. Older orders fall back to payload.cod_amount.
  • Bookings: meta.is_booking / meta.booking and the storefront-booking flow (contract 6).

Validation

  • yarn typecheck (tsc -p tsconfig.v3.json): 15 errors, all pre-existing in v2 files pulled in by v3 (src/components/CameraCapture.tsx, src/hooks/use-app-theme.ts). The base branch has the same 15 and there are none in v3.
  • yarn lint: 600 errors / 419 warnings, against 600 / 411 on the base. The errors are all pre-existing (v2 tree, scripts, jest setup), and running eslint on the files changed here reports no errors.
  • yarn test: 69 suites, 1016 tests pass, up from 965. New tests:
    • events, push, applyEvent and SocketProvider (subscribe-before-connect, unassignment)
    • order store reconcile, and reassignment
    • config-driven finished status
    • proof ledger
    • order detail: change banner, re-open navigation, current-stop navigate, refetch on event, items, COD gating, server next-activity, no proof re-capture, reassignment, booking, contact and threads, instructions
    • chat: system notice, role labels, order thread in the inbox
  • Not run on a device or simulator.

Docs

No docs or API spec changes from this PR. The endpoints and events it uses are defined by the order-operations contract and documented with the server PRs (fleetops, storefront, core-api).

Copy for the order-operations work that follows: the "this order changed"
banner, item quantity / note / unavailable / substitute, cash on delivery,
delivery instructions and schedule, customer and dispatch contact, booking
jobs and the bring-to-the-job checklist, direct offers (countdown, expired,
unsupported), reassignment notices, and order-thread roles. Booking statuses
`booked` and `provider_enroute` get labels. Spanish kept in parity.
…signed orders

- Which status finishes an order now comes from the order config's
  `complete` activity (data/orderStatus), falling back to the old hard-coded
  sets. The store's "active" view, Orders' segments and the stop screens all
  use it, so a booking flow that ends on its own code finishes correctly.
  Configs are cached outside React (data/orderConfigCache) so the store can
  read them, and a config arriving later recomputes the views.
- Driver-scoped list queries stamp their rows and reconcile the store after
  each run: an order the query no longer returns (within its window) is
  removed; active ones are recorded as reassigned with a notice for the
  driver. upsertMany only ever added, so a reassigned order stayed on the
  device and could be advanced into a server refusal.
- remove(id, 'reassigned') records why, for screens holding the order to
  leave; notices() / dismissNotice() back the "order was reassigned" banner.
- useOrderRecord refetches on every live-refresh bump and on a reload token
  (focus), and treats an order now naming another driver as reassigned.
- pendingOffers(driverId): orders whose meta.offer is `offered` to this driver.
- List queries prefetch the configs behind their rows (once each).
…-ops and chat events

- App.tsx passes the SocketCluster config v2's provider builds (env defaults,
  overridden by a linked instance) to V3App. Without it SocketProvider
  mounted disabled and the app heard nothing.
- SocketProvider holds the socket in state, not a ref. Children subscribe in
  effects that run before the provider's, so the driver channel was handed a
  no-op and never subscribed; `listen` now changes identity when the socket
  exists and every subscriber re-runs. A reconnect bumps live refresh, since
  events broadcast while disconnected are gone.
- Event map gains order.changed (with summary/changes), order.updated,
  order.driver_unassigned, order.offer_expired, the OrderAssigned
  notification's `order.assigned_notification`, and chat message events.
  The order id is also read from data.id. Unknown events are still dropped
  but logged in development.
- applyRealtimeEvent is the one handler for socket and push: unassignment
  removes the order (reassigned), order.changed keeps its summary for the
  order screen's banner, order events bump live refresh, chat events bump a
  separate chat counter.
- useOrderChannel follows `order.{id}` for the open order;
  useChatChannelSubscriptions follows `chat.{id}` / `chat_channel.{id}`.
- Push plumbing: V3App takes `subscribeToPush` (App.tsx adapts v2's
  NotificationProvider); parsePush / pushToEvent / destinationOf describe what
  a push means, and a root navigation ref lets a tapped push open an order or
  conversation from any tab, even before the container is ready.
Contract 1.4: an order put in a driver's name starts with meta.offer
`offered` and an `expires_at`; the driver answers with
POST v1/orders/{id}/accept or /decline. Ad-hoc offers keep start + assign.

- useRespondToOffer posts accept/decline through the adapter, never queued
  (added to NEVER_QUEUE — an answer replayed after the window is not an
  answer). Outcomes: accepted, declined (the order leaves the store, no
  reassignment notice), expired, unsupported (404/405: the server does not
  have the endpoint yet), failed.
- useCountdown / formatCountdown tick down to expires_at.
- OfferCard (previously unused) now translates its copy, shows an expired
  state that withdraws Accept, and wires Decline and Details.
- DirectOfferCard wraps it with the countdown and the answer, for Today,
  Offers and the order itself.
- OffersScreen lists direct offers above the nearby ad-hoc ones and reloads
  on focus. Also fixes a pre-existing test that never unmounted its tree.
…xt step, proof loop

- The order, tracker and next step are refetched on order events (live
  refresh, `order.{id}`) and on focus; previously the store copy was read
  once and the tracker loaded once.
- "This order changed" banner from order.changed / order_changed using the
  server's summary; when the changes include the drop-off it offers
  "Re-open navigation" to the order's current stop.
- Navigate targets the stop being headed to (current_waypoint, else the
  drop-off), not payload.dropoff.
- Items show quantity (meta.quantity), the item note, unavailable items and
  substitutes (meta.substitute_for), per contract 4.
- Cash on delivery: payload.cod_amount, "to collect" per meta.payment.state
  (absent on older orders: the amount alone means cash is due; paid /
  refunded owe nothing). Completing the order waits for "I collected …".
- Delivery instructions (drop-off place meta/instructions), order notes, the
  scheduled window or booking slot, and customer Call / Message plus Message
  dispatch — through the order's own chat threads when they exist.
- Next step from GET orders/{id}/next-activity when the server answers; it
  has evaluated the flow's logic, so a branch becomes a choice of buttons
  instead of "ask dispatch". Falls back to the client sequencer otherwise.
  "Finished" is the config's `complete` flag.
- Proof loop: a capture is recorded against its order and activity
  (proofLedger, persisted); returning from capture advances once, sending the
  proof id with the update, instead of opening capture again.
- Booking orders (meta.is_booking) read as a Job with their slot and a
  "Bring to the job" checklist; booked / provider_enroute get registry
  entries and `confirmed` ranks second in the lifecycle.
- A pending direct offer shows on the order and holds the flow until
  accepted. A reassigned order calls onRemoved and says so.
- Orders and Today show reassignment notices; Orders links to Offers with a
  waiting count, Today shows the first direct offer; Orders' completed
  segment uses the config-driven finished check.
- Chat data: channel meta (order_public_id, kind), participant meta.role,
  message meta.system; orderThreadsOf picks the dispatch (`driver`) and
  customer (`group`) threads; lists refresh on chat events; an unread store
  for the Inbox badge.
…eads, v3 inbox badge

- PushRouter: a foreground push is applied as the socket event it duplicates
  (refresh; remove a reassigned order; keep an order change's summary); a
  tapped push opens the order (order_changed, order_assigned, …), the Orders
  list (order_unassigned) or the conversation (chat_message_received).
- Screens holding a reassigned order (detail, item, timeline, proof capture,
  destination, hand-off) pop to the top of their stack; the list there says
  the order was reassigned.
- The navigation hand-off takes the order id and reads the order's current
  destination from the store, so a drop-off moved after the tap (or while
  the picker is open) still sends the driver to the right place.
- Order detail refetches on focus, opens order threads in the Inbox, and
  falls back to a new conversation for "Message dispatch". Offers is
  reachable from Orders and Today.
- ChatWatcher keeps the driver's channels loaded and followed on the socket;
  the Inbox badge is v3's own unread total instead of v2's ChatContext.
- Conversation: system messages (meta.system) render as notices, order
  threads label each side (Store, Customer, You), and the header and inbox
  row name the order the thread is about.
An order assigned to a driver is simply theirs. Only ad-hoc orders are
offered, through Fleet-Ops' existing ping, and taken with `start` + `assign`
(the Offers screen and its Accept / Dismiss). The direct-offer layer built
on `meta.offer` duplicated that and is removed with its Fleet-Ops side:

- Reverts "Offers: direct offers with a countdown, accept and decline".
- Today, Orders and the order no longer show a direct offer or hold the flow
  for one; the Orders screen keeps a plain button to the ad-hoc Offers screen.
- The order store, realtime (`order.offer_expired`) and strings lose the
  direct-offer pieces.
@roncodes roncodes changed the title v3: realtime order changes, reassignment, direct offers, order chat and bookings v3: realtime order changes, reassignment, order chat and bookings Oct 13, 2026
@roncodes
roncodes merged commit de5e772 into feature/v3-redesign Oct 14, 2026
@roncodes
roncodes deleted the feat/v3-realtime-order-changes branch October 14, 2026 09:12
@roncodes roncodes mentioned this pull request Oct 14, 2026
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