Skip to content

Commit c323db4

Browse files
feat(PWA-S8): tracking page /c/[orderId] realtime + incidents (#479)
* test(#459): scaffold tracking decision + getOrderTracking tests PWA-S8 tracking page (/c/[orderId]) — RED tests : - decideTrackingSteps : 6 etapes delivery + 3 etapes C&C, mapping order workflow + delivery status. - decideEtaLabel : pickupEta avant pickup_complete, dropoffEta apres, rounding minute. - decideIncident : refused_post_payment + incident_after_pickup => card refund (customer_absent silent). - getOrderTracking : publicTenantQuery, null cross-tenant (no oracle), projection minimale (jamais customerId). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(#459): /c/[orderId] tracking page realtime + incidents PWA-S8 (PRD 10 §11 Page Tracking, US 50-55) : Backend (packages/backend/convex/lib/orders/tracking.ts) : - getOrderTracking publicTenantQuery (URL non-protégée, partageable — l'orderId non-devinable IS le token). - Projection minimale : status, mode, items, address, total, delivery {status, etas, incidentType}. Jamais customerId / customerPhone / restaurantNote (ADR 0010 MOAT). - Cross-tenant : null indistinctement d'un orderId manquant (pas d'oracle). - 9 tests dont fuzz cross-tenant + foreign customer même tenant (URL share-by-design). Frontend (apps/web/src/lib/tracking/ + components/tracking/ + app/c/[orderId]/) : - decideTrackingSteps : 6 étapes delivery + 3 étapes C&C, mapping order workflow + delivery status, terminal failure laisse seul 'received' done. - decideEtaLabel : pickupEta avant pickup_complete, dropoffEta après ; round-up minute ; 'Bientôt' si passé ; null sur C&C / incident terminal. - decideIncident : refused_post_payment + incident_after_pickup → card refund ; customer_absent SILENT côté client (PRD 40 §5 Cas D). - TrackingView : useQuery Convex sub realtime (re-render <500ms au webhook), placeholders SVG (HITL-3 Lottie pending, swap PR cosmétique), <details> natif pour items collapsible, setInterval 30s pour ETA countdown. - Page RSC : cookie tenant du middleware, dégradé propre si absent, jamais convexAuthNextjsToken (URL doit marcher sans cookie auth). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 154085a commit c323db4

13 files changed

Lines changed: 1813 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* PWA-S8 (#459) — `/c/[orderId]` : the customer-facing tracking page (PRD §10
3+
* PWA Client §11 « Page Tracking », US 50 → 55, decisions-log Q7).
4+
*
5+
* Hybrid render :
6+
* - RSC reads the `__Host-kb_tenant` cookie (set by the PWA edge middleware,
7+
* PWA-S1 #449) → resolves the tenant's name for the heading.
8+
* - RSC hands `tenantId` + `orderId` to the `<TrackingView>` client component
9+
* that subscribes via `useQuery(api.lib.orders.tracking.getOrderTracking)`.
10+
* Convex pushes EVERY backend write (`recordStatus`, `confirmPayment`, the
11+
* Uber webhook applier) → re-render < 500ms (acceptance criterion #459).
12+
*
13+
* Why NO `convexAuthNextjsToken()` here (cf. `/checkout`) : the tracking URL
14+
* is INTENTIONALLY UN-AUTH-GATED (US 55, decisions-log Q7). A customer must be
15+
* able to SHARE the URL (SMS / push deep-link) and have the recipient device
16+
* — even one with no Convex Auth cookie — render the page. The un-guessable
17+
* Convex `orderId` IS the access token, same model as a Stripe receipt URL.
18+
*
19+
* Edge cases :
20+
* - Missing tenant cookie (= matcher mis-config in the middleware) → render
21+
* the « degraded » shell with a back-to-/ link. NEVER crash on `undefined!`.
22+
* - Missing / cross-tenant / deleted order → the client component handles the
23+
* `null` from `getOrderTracking` with a « commande introuvable » empty state.
24+
*/
25+
import { cookies } from "next/headers";
26+
import Link from "next/link";
27+
import { fetchQuery } from "convex/nextjs";
28+
import { api } from "@packages/backend/convex/_generated/api";
29+
import type { Id } from "@packages/backend/convex/_generated/dataModel";
30+
import { TrackingView } from "@/components/tracking/tracking-view";
31+
32+
const TENANT_COOKIE = "__Host-kb_tenant";
33+
34+
export default async function TrackingPage(props: {
35+
params: Promise<{ orderId: string }>;
36+
}) {
37+
const { orderId } = await props.params;
38+
const cookieStore = await cookies();
39+
const tenantId = cookieStore.get(TENANT_COOKIE)?.value as
40+
| Id<"tenants">
41+
| undefined;
42+
43+
if (tenantId === undefined) {
44+
return (
45+
<main className="flex min-h-screen flex-col items-center justify-center bg-zinc-50 p-8">
46+
<div className="flex max-w-md flex-col items-center gap-4 text-center">
47+
<h1 className="text-2xl font-bold text-black">Suivi de commande</h1>
48+
<p className="text-base text-zinc-600">
49+
Nous n&apos;arrivons pas à identifier ton resto. Recharge la page ou
50+
reviens à l&apos;accueil.
51+
</p>
52+
<Link
53+
href="/"
54+
className="text-sm font-medium text-emerald-700 hover:underline"
55+
>
56+
← Retour à l&apos;accueil
57+
</Link>
58+
</div>
59+
</main>
60+
);
61+
}
62+
63+
// Tenant name for the heading (the same minimal projection PWA-S1 uses).
64+
const tenant = await fetchQuery(api.lib.tenants.resolution.byId, {
65+
tenantId,
66+
});
67+
const restoName = tenant === null ? "KitchenBoost" : tenant.name;
68+
69+
return (
70+
<main className="min-h-screen bg-zinc-50">
71+
<div className="mx-auto flex max-w-2xl flex-col gap-6 px-4 pb-16 pt-8 md:px-8">
72+
<header className="flex flex-col gap-2">
73+
<h1 className="text-3xl font-bold text-black">Suivi de commande</h1>
74+
<p className="text-sm text-zinc-600">
75+
Commande chez <strong className="text-zinc-700">{restoName}</strong>
76+
.
77+
</p>
78+
</header>
79+
80+
<TrackingView
81+
tenantId={tenantId}
82+
orderId={orderId as Id<"orders">}
83+
restoName={restoName}
84+
/>
85+
86+
<footer>
87+
<Link
88+
href="/"
89+
className="text-sm font-medium text-zinc-700 hover:underline"
90+
>
91+
← Retour à l&apos;accueil
92+
</Link>
93+
</footer>
94+
</div>
95+
</main>
96+
);
97+
}

0 commit comments

Comments
 (0)