Goal
Close the journey: add welcome (skip-easy entry) and project-tour (multi-node walkthrough wired to swap in Roman's incoming interior 360°s), assemble everything into /projects/[slug] + /saved sub-route, ship the buyer-profile API stub + CRM-CONTRACT.md spec, and add a Playwright happy-path smoke test.
Council Review (CONV-30) amendments apply — see
plans/sales-app-react-module-sequence.md§Council Review. Phase 5 amendments: A-IMPL-1 (Server Component + JourneyShell + Suspense pattern), A-RES-2 (ProjectTour gates behind "Coming soon" if <3/5 panos real), A-RES-4 (validate searchParams against fixture; redirect on miss), A-SEC-2/S-T5 (CRITICAL — ADGM DPR 2021 + retention TTL + erasure endpoint + privacy notice in CRM-CONTRACT.md), A-SEC-4 (CSRF + rate limit + origin allow-list), A-SALESUX-1 (PresenterBar new module), A-DEMO-4 (visual register reconciliation pass), A-SALESUX-5 (welcomeDefault per project), A-BUYUX-5 (Welcome shows only on first visit; returning identified buyers via?profile=<lookupId>skip Welcome).Per-module 4-step design loop (A-DEVUX-1): Welcome + ProjectTour are photo-heavy → real-page-stub. PresenterBar is a density module → use
/wireframe.Note: Per A-SEQ-2, the basic
/projects/[slug]/page.tsxServer Component was shipped in sales-app-react-anchor (Phase 2) as minimal mount of<Exterior360>. This phase ENRICHES it with Welcome routing, URL state, and the JourneyShell wrapper.
Tasks
JourneyShell + URL state (A-IMPL-1)
- [ ] PAUSE: Brief Roman on URL-state model — what
?stage=,?layout=,?unit=,?profile=,?presenter=,?welcome=mean; deep-link semantics; share-link patterns. - [ ]
src/app/projects/[slug]/page.tsxextension — Server Component doesconst { slug } = await params; const { stage, unit, layout, profile, presenter, welcome } = await searchParams;then passes as props to<JourneyShell>. Wrap in<Suspense fallback={...}>. - [ ]
src/components/journey-shell.tsx—'use client'; owns all stage routing viauseSearchParams()+useRouter(). Mounts the right module per?stage=value:exterior→ Exterior360,select→ FloorplateSelection,layout→ LayoutDetail,unit→ UnitDetail,tour→ ProjectTour,saved→ SavedUnits. A-RES-4 validation: ifsearchParams.unitdoesn't match a fixture unit, redirect to?stage=selectwith toast "Unit B-1402 is no longer available — browse the floorplate"; same for invalid?layout=. - [ ] A-BUYUX-5 / A-SALESUX-5 routing logic: Welcome shows only when (no
oo:welcome-seen:v1localStorage flag) AND (no?profile=param) AND (fixture.welcomeDefault !== 'hidden').?welcome=skip|begin|hiddenis explicit override. Returning identified buyers (with?profile=<lookupId>) land on?stage=selectwith their saved filters pre-applied via Zustand store. - [ ] Document JourneyShell pattern in
docs/next16-tailwind4-notes.mdunder new "URL state in journey" section. - [ ] PAUSE: Live-iterate JourneyShell with Roman — verify deep-links work,
?presenter=1toggles broker mode, browser back/forward respects?stage=history. - [ ] AU English check on touched files.
Welcome module (photo-heavy)
- [ ] PAUSE: Brief Roman on Welcome before coding — hero treatment, skip-intro affordance prominence, cinematic-vs-snappy register.
- [ ]
src/features/welcome/Welcome.tsx— Skeleton White marketing surface; hero copy from fixture (project name + architect + developer credits + 1-line lede); NOT a forced gate — "Skip intro" link always visible top-right (per Nadezhda + audit "no forced welcome gate"); buttons: "Begin tour" →?stage=exterior, "Skip intro" →?stage=select. A-SALESUX-5: respectfixture.welcomeDefault('begin'/'skip'/'hidden') as initial CTA emphasis. - [ ]
src/features/welcome/Welcome.stories.tsx+README.md - [ ] PAUSE: Live-iterate Welcome with Roman.
- [ ] AU English check on touched files.
ProjectTour module (photo-heavy + A-RES-2 gating)
- [ ] PAUSE: Brief Roman on ProjectTour before coding — node graph layout, navigation chrome, placeholder card copy when interior panos absent.
- [ ]
src/features/project-tour/ProjectTour.tsx— multi-node walkthrough reusing<Panorama>; switchespanoramaUrlbetween nodes fromtourNodes[]in fixture; whenpanoramaUrlset → real panorama; when missing →<AssetOrPlaceholder kind="panorama">(use A-DEMO-3 copy "Interior walkthrough — available on request from your advisor"). A-RES-2 gating: if real-pano coverage <3/5 nodes (i.e. <50%), gate the entire module behind a "Coming soon — interior walkthrough" full-module placeholder (single screen, marketing copy, "Notify me" CTA wired to BuyerProfile capture modal). Promote node-graph rendering only when ≥3 panos real. Document threshold in component header. Structurally complete from day one — Roman's incoming interior 360° panos drop in via fixture-only edit, no component changes. - [ ]
src/features/project-tour/ProjectTour.stories.tsx(rewrite from stub) - [ ] Rewrite
src/features/project-tour/README.md - [ ] PAUSE: Live-iterate ProjectTour with Roman.
- [ ] AU English check on touched files.
PresenterBar module (density — A-SALESUX-1, NEW)
- [ ] PAUSE:
/wireframe2-3 lo-fi variants for PresenterBar — bottom bar vs floating; density of controls (Prev/Next/Jump/keyboard hint); when visible (always when?presenter=1vs hover-reveal). - [ ] PAUSE: Brief Roman + pick variant before coding.
- [ ]
src/features/presenter-bar/PresenterBar.tsx— slim bottom bar visible when?presenter=1or persisted broker preference; Prev/Next stage buttons (cycles?stage=…), current stage label, "Jump to..." combobox, keyboard arrow nav (← / →). Bake into URL-state. Includes "Copy presenter link" action that builds a shareable URL with?presenter=1+ chosen?welcome=…for sending to brokers per A-SALESUX-5. - [ ]
src/features/presenter-bar/PresenterBar.stories.tsx+README.md - [ ] PAUSE: Live-iterate PresenterBar with Roman.
- [ ] AU English check on touched files.
API stub + CRM-CONTRACT.md (A-SEC-2 ADGM compliance + A-SEC-4 auth)
- [ ]
src/app/projects/[slug]/saved/page.tsx— sub-route renders<SavedUnits>. - [ ]
src/app/api/buyer-profile-stub/route.ts— Next 16 async route handler. POST validates BuyerProfile JSON shape, A-SEC-2 redacts email/phone beforeconsole.log(e.g.r***@domain), returns{ok: true, lookupId: <uuid>, magicLinkSent: true}. A-BUYUX-2 lookup endpoint:GET /api/buyer-profile-stub?email=…returns{profile: BuyerProfile | null, lookupId, savedUnits}. A-SEC-4 reference rate-limiter: ship a 60-req/min in-memory limiter as reference for the eventual real endpoint. NO persistence — in-memory map only. - [ ]
os/design-system/sales-app/CRM-CONTRACT.md— full spec covering:- §a POST shape (TS type), status flow (created → contacted → qualified → converted | lost)
- §b Idempotency on email match; OfferSummary artifact TS type
- §c Existing-buyer lookup contract (
GET /buyer-profile?email=…returnsBuyerProfile | null) - §d Saved-units mutation (
PATCH /buyer-profile/:lookupId/saved-units) - §e Per-session price-snapshot persistence (denormalised so historical offers stay correct after price changes)
- §f Auth model: broker token in header
- §g (A-SEC-2 CRITICAL) ADGM Data Protection Regulations 2021 applicability (separate from federal UAE PDPL, stricter, GDPR-aligned per Sonar Pro 2026); lawful basis = explicit consent; consent text shown in capture modal; data residency: UAE region only; retention TTL: 90 days default if no purchase (Sonar recommendation); privacy policy link in capture modal
- §h (A-SEC-2 CRITICAL) Erasure endpoints:
DELETE /buyer-profile/:id+DELETE /buyer-profile/by-email - §i (A-SEC-4) Same-origin or
Origin/Refererallow-list; CSRF double-submit token ORSameSite=Strictcookie + custom header; per-IP + per-email rate limit (5/min, 50/day); bot mitigation note (Turnstile/hCaptcha as v1.1) - §j Out-of-v1 list (real-time bi-directional CRM sync deferred)
- §k v1 marker: "subject to revision once backend implementer reviews"
- [ ] Note for Roman: before launching the demo to Nadezhda, recommend a separate UAE legal advisor pass on the ADGM DPR 2021 + PDPL specifics. Sonar's verdict has weak primary-source citations; treat as advisory-with-strong-domain-precedent.
Visual register reconciliation (A-DEMO-4)
- [ ] A-DEMO-4 visual reconciliation pass: Roman walks all 10+ modules in Storybook side-by-side against
os/docs/design/visual-register.md, files diffs as/inspecttweaks, applies via/apply-tweaksbefore the Playwright smoke runs. Block phase-complete on this pass.
E2E smoke + final verification
- [ ]
tests/e2e/riviera-journey.spec.ts— Playwright smoke: open/projects/riviera, dismiss Welcome (per A-BUYUX-5 logic), click 1 hotspot, navigate to selection, pick a layout, pick a unit, save FIRST unit (no modal — A-BUYUX-1), save SECOND unit (modal opens; fill; submit), confirm/projects/riviera/savedshows both units. A-RES-4 path: assert/projects/riviera?stage=unit&unit=DOES-NOT-EXISTredirects to?stage=selectwith toast. - [ ] Verify:
pnpm dev→ walk full journey end-to-end;pnpm exec tsc --noEmitclean;pnpm exec playwright test riviera-journey.spec.tspasses; deep-link/projects/riviera?stage=unit&unit=B-1402lands on UnitDetail directly via JourneyShell. - [ ] A-DEVUX-3 phase-gate handoff record: /handoff at end of Phase 5 writes
Gate-passed: phase-5to session, lists Storybook URLs Roman walked, appends signed-off note to this workstream's Session Log. - [ ] Final Australian English sweep as backstop (per A-DEVUX-4 — primary AU English check happens per-phase, this is the safety net).
What's Next
Confirm Phase 4 (sales-app-react-capture) is complete — buyer-profile chip + capture modal must work in Storybook before this phase wires them into the route. Then start with src/app/projects/[slug]/page.tsx (the integration point that pulls everything together — surfaces wiring problems early). Then Welcome (simplest content module). Then ProjectTour. Then API stub + Playwright smoke. Finish with CRM-CONTRACT.md (best written after seeing the full flow work, so the spec reflects actual UX). Australian English sweep last.
Key Context
- Plan:
plans/sales-app-react-module-sequence.mdPhase 5 - Phase 4 must be DONE; this phase wires the BuyerProfile chip + modal into the actual route
- Next 16 async-params is non-negotiable:
const { slug } = await params— syncparams.slugwill throw at runtime perdocs/next16-tailwind4-notes.md - Roman will provide interior 360° walkthrough panos later (after this plan is built).
tourNodes[]shape supports drop-in delivery — only fixture edits required, noProjectTourcomponent changes - CRM-CONTRACT.md is v1 marked "subject to revision once backend implementer reviews"; intended to be shared with Ilya/backend before the Nadezhda demo
- Eval pack: plan §Phase 5 evaluation table (artefact = walkable demo + Playwright HTML report + CRM-CONTRACT.md + screenshot reel; 2-min check =
pnpm dev+ walk full Nadezhda flow without dead-ends)
Session Log
- CONV-30 (2026-05-09): Workstream created via
/planratification ofsales-app-react-module-sequence - CONV-30 (2026-05-11, buyer-journey track close): Plan integrated post-council (7 CRITICAL + 22 HIGH + 7 MEDIUM amendments inline), renamed from path2- slug to sales-app-react-, parallel brand-language-and-identity track established. State ratified, ready for /build (or /plan plans/brand-language-and-identity.md for the brand track).