# ADR-091: In-App Onboarding Tour Engine Contract — shared/tour.js Usage, Step Shape, and Dismissal Protocol

## Status

Accepted, 2026-06-27.

## Status History

```yaml
status_history:
  - date: 2026-06-27
    status: Proposed
    changed_by: hkl
    reason: Field staff frequently encounter new features without training time; in-app guided tours reduce support load. Documenting shared/tour.js API contract pre-emptively before 3+ PWAs adopt the pattern (currently 4 PWAs use it — counters, recce, tour-planner, tour-pg). This ADR establishes the standard step shape, storage key versioning, dismissal protocol, and UI contract to ensure consistency across implementations.
    changed_via: adr-kit (360lm)
  - date: 2026-06-27
    status: Accepted
    changed_by: hkl
    reason: shared/tour.js is a platform-wide UI helper (meets ADR-079 criterion: 4 PWAs, no business logic, documented in ADR). Documenting the API contract prevents PWAs from rolling their own tours or using inconsistent step shapes. Contract is stable; implementation is proven across field staff onboarding workflows.
    changed_via: adr-kit (360lm)
```

## Context

Field staff (agents, supervisors, peons, clients) frequently encounter new features without scheduled training. New flows (transfer types, ledger views, approval workflows) introduce friction — staff either skip the feature or contact support.

An in-app guided tour addresses this: sequential, on-demand, dismissible overlay showing 3–8 steps with screenshots, brief explanations, and step navigation. Tours are non-intrusive (highlight the relevant UI element, darken background, show explanation).

**Existing implementation:** `/var/www/360lm/shared/tour.js` is a stable, low-footprint tour engine used by four PWAs:
- **counters** — new balance inquiry flow
- **recce** — official approval workflow
- **tour-planner** — multi-stop route planning
- **tour-pg** — client-facing portal (new for 2026-06-27)

Without a documented contract, PWA developers either skip tours or implement their own (e.g., custodian has a custom tour; finance has none). This creates:
- **Inconsistent UX** — some PWAs guide users, others don't
- **Duplicate code** — each PWA re-invents step rendering, dismissal, storage
- **Maintenance burden** — tour logic scattered across 8+ files

This ADR documents the standard when to use `tour.js`, the API contract, step shape, storage key versioning, dismissal protocol, and UI contract so all PWAs follow a single pattern.

**Affected PWAs:** counters, recce, tour-planner, tour-pg (4 consuming); finance, custodian, vehicle, sales, vendors, admin, activity, dispatch, hr, and others as they add onboarding flows.

**Decision Maker:** hkl

## Decision

### 1. When to Use `tour.js` (vs. Alternatives)

Use `tour.js` when ALL of the following apply:

- **New feature** — The user encounters this flow for the first time or after a major update.
- **Multi-step workflow** — 3–8 sequential steps (single-concept features use tooltips instead).
- **Role-based guidance** — Tours can be triggered per role (e.g., "Approval Officer" gets a different tour than "Agent").
- **Benefits from onboarding** — The flow reduces support load, training time, or error rate when guided.

**Do NOT use `tour.js` if:**

- **Single concept, always visible** — Use contextual tooltips (? icons on form labels) instead. Examples: "What is a rate card?", "What does 'pending approval' mean?". These are one-time explanations, not sequential workflows.
- **Permanent reference material needed** — Use Learning Hub Scene Guide (ADR-065) instead. Examples: complete walkthroughs with screenshots, bilingual audio, and persistent access in `/learn/`. Scene Guides are for features staff refer to repeatedly; tours are for one-time onboarding.
- **Core, critical workflow** — Do not gate critical functionality behind a dismissible tour. Tours are optional guidance; critical paths should be self-explanatory (good button labels, helpful error messages). Tours augment onboarding; they do not replace good UX.

### 2. API Contract: `initTour(steps, opts)`

The tour engine exports a single function. Call it once per feature, early in page load (or when the feature becomes active):

```js
const tour = initTour(steps, { storageKey: 'my-pwa-tour-v1', autoShow: true });
```

**Return value:** An object with two methods:

```js
tour.show()    // Render and display the tour overlay
tour.close()   // Dismiss the tour, mark as seen in localStorage
```

### 3. Step Shape

Each step is a plain JavaScript object with these fields:

```js
{
  emoji:    '🔄',                           // (required) Single emoji, shown at 42px
  title:    'Transfer Money',               // (required) Short title, 17px bold
  body:     'Tap here to start a NEFT or\nUPI transfer to your payee.', // (required) Explanation text, supports \n for line breaks
  screenFn: function() { /* navigate */ }  // (optional) Callback when step becomes active
}
```

**Field details:**

- **`emoji`** — Single character emoji (U+1F300 range). Shown prominently at the top of the card. Helps users visually scan tour steps. No fallback if omitted; field is required.

- **`title`** — 1–4 words, max 50 chars. Noun phrase or imperative (e.g., "View Ledger", "Start a Transfer"). Shown at 17px, font-weight 800, light gray (#f5f5f5).

- **`body`** — 1–3 sentences, max 200 chars. Explain what the button does and why it matters. Use plain English, not jargon. Supports `\n` for manual line breaks (rendered as `white-space: pre-line`). Shown at 13.5px, medium gray (#a0a0a0), line-height 1.68.

- **`screenFn`** — Optional callback invoked when the step becomes active (user navigates to it via Next/Back buttons). Use it to scroll to the relevant UI element or navigate between screens within the PWA. If the function throws, it is silently caught (no error shown). The tour does not wait for `screenFn` to complete; it fires asynchronously.

  Example:
  ```js
  screenFn: function() {
    document.getElementById('transfer-btn').scrollIntoView({ behavior: 'smooth' });
  }
  ```

### 4. Storage Key and Versioning

The `storageKey` option controls whether the tour auto-shows and when it re-shows:

```js
{ storageKey: 'custodian-tour-v1', autoShow: true }
```

**Format:** `'<pwa-short-name>-tour-v<N>'`
- `<pwa-short-name>` — lowercase PWA identifier (e.g., 'custodian', 'recce', 'vehicle'). Not the app title, use the URL path slug.
- `v<N>` — integer version number, starts at 1.

**Behaviour:**

- On `initTour()`, the engine checks `localStorage.getItem(storageKey)`. If the key exists and equals `'1'`, the tour is skipped (already seen).
- If `autoShow: true` and the key does not exist, the tour auto-displays after 900ms (0.9s) on first load. This allows the DOM to settle before rendering the overlay.
- If `autoShow: false`, the tour is loaded but not shown. Call `tour.show()` manually (e.g., from a "Show tour" button in Settings).

**When to increment the version:**

- **Increment `v<N>`** when tour content changes in a way that justifies re-showing the tour to existing users:
  - Steps are added or removed (workflow changed).
  - Steps are reordered significantly.
  - Step emoji, title, or body text changes substantially.
  
  Incrementing the version forces `localStorage` to be cleared for that key, so existing users see the tour again on next load.

- **Do NOT increment** for:
  - Minor wording fixes or typo corrections.
  - Changes to CSS styling or colors (skin changes, not workflow changes).
  - Changes to `screenFn` callback internals that don't affect visible tour flow.

**Storage implementation:**

```js
// Inside tour.js (pseudocode)
function close() {
  if (root) { root.remove(); root = null; }
  try { localStorage.setItem(storageKey, '1'); } catch (e) {}
}

if (autoShow) {
  var seen = false;
  try { seen = !!localStorage.getItem(storageKey); } catch (e) {}
  if (!seen) {
    setTimeout(render, 900);
  }
}
```

### 5. Dismissal Protocol

A tour is dismissed (and marked as seen) when the user:

1. **Completes all steps** — Clicks "Done ✓" on the final step. `close()` is called, marking the tour as seen.
2. **Taps "Skip tour"** — Button in the top-right of the card. `close()` is called immediately.
3. **Clicks outside the card (future)** — Not currently implemented, but the design supports it: clicking the dark overlay outside the card would call `close()`.

Once dismissed, the tour will not auto-show again unless the `storageKey` version is incremented (forcing a reset).

**Manual re-trigger:** If a PWA includes a "Replay tour" or "Show tour" button in Settings, call:

```js
// Clear localStorage to force re-show
localStorage.removeItem(storageKey);
// Or manually invoke:
tour.show();
```

### 6. UI Contract

The tour engine renders a modal card with these visual rules:

**Layout:**
- Dark overlay: `rgba(0, 0, 0, 0.7)` (70% black, slightly transparent to keep app visible behind)
- Card background: `#1c1c1e` (very dark gray, iOS system color)
- Card border: `1px solid #2c2c2e` (slightly lighter gray for definition)
- Card border-radius: `20px` (iOS-style rounded corners)
- Max width: `360px` (readable on portrait devices), with `16px` padding left/right

**Z-index:**
- `z-index: 9990` — Places the overlay above modals (z-index 9000), below system UI (9999+)
- Safe for use alongside maps, popovers, and system notifications

**Typography:**
- Step counter: `"Step 2 of 5"` — 10.5px, gray (#888), uppercase, letter-spacing 0.5px
- Title: 17px, font-weight 800, light gray (#f5f5f5)
- Body text: 13.5px, medium gray (#a0a0a0), line-height 1.68
- Buttons: 13px, font-weight 700, sans-serif (inherited from PWA)

**Buttons:**
- "← Back" — Visible only on steps 2+. Dark gray background (#141414 → #1e1e1e on hover), light text. Left-aligned.
- "Next →" or "Done ✓" — Pink/magenta (`#ec4899` → `#db2777` on hover). Right-aligned. Text changes to "Done ✓" on final step.
- "Skip tour" — Small, text-only, minimal styling. Top-right corner.

**Step indicators (dots):**
- Dot count matches step count (e.g., 5 dots for 5 steps)
- Current step dot: `#ec4899` (pink/magenta, matches accent color)
- Inactive dots: `#333` (dark gray)
- Dots are purely visual; clicking does not jump steps (no dot-click navigation)

**Animation:**
- Card entrance: 180ms ease animation (scale 0.94 → 1.0, opacity 0 → 1)
- Smooth, non-jarring appearance

**Mobile PWA mode:**
- Tested on Chrome Android standalone PWA mode (portrait)
- Overlay is full-screen (inset: 0) and overlays the entire app
- Touch-friendly buttons (min 44px height, implicit 9px padding + line-height)
- Works with notched devices (safe-area-inset aware, no content hidden behind notches)

### 7. Implementation Notes

**Scope:**
- Tour engine is UI-only; it does not navigate PWA screens or change app state.
- `screenFn` callbacks are responsible for navigation. If `screenFn` fails or is omitted, the tour does not break — it displays the step with no side effects.
- Tours do not block user interaction with the app behind the overlay. Users can wait out the tour and click outside (future), or dismiss it explicitly.

**Storage safeguards:**
- `localStorage.getItem()` and `localStorage.setItem()` are wrapped in try-catch blocks to handle private browsing mode (where localStorage is unavailable).
- If localStorage is disabled, the tour will show every time (falls back to `autoShow: true` behaviour on every load). This is acceptable — tours are non-blocking guidance, not critical infrastructure.

**PWA integration checklist:**
1. Add `<script src="/shared/tour.js"></script>` to PWA's `index.html` (before any code that calls `initTour`).
2. Add a comment linking to this ADR: `<!-- tour.js: ADR-091 -->`
3. Define tour steps array with correct shape (emoji, title, body, optional screenFn).
4. Call `initTour(steps, { storageKey: '<pwa>-tour-v1', autoShow: true })` early in page load (in a ready/DOMContentLoaded callback).
5. Optional: add a "Replay tour" button in Settings that calls `tour.show()` or clears localStorage.
6. Test:
   - First load: tour auto-shows after 900ms
   - Reload page: tour does not show (already marked as seen)
   - Clear localStorage: tour shows again on reload
   - Step navigation: Back/Next buttons work, screenFn is called
   - Dismissal: "Done" and "Skip" buttons work; localStorage is updated

## Alternatives Considered

- **Custom per-PWA tours** — Each PWA implements its own tour overlay (e.g., custodian's custom tour). Rejected: duplicate code, inconsistent UX, harder to maintain. Shared helper reduces code per PWA by 400+ lines.

- **Lightbox library (e.g., Intro.js, Shepherd.js)** — Use a third-party library for tours. Rejected: adds npm dependency (violates ADR-013), increases bundle size (~30KB), overkill for simple overlays. shared/tour.js is 114 lines of vanilla JS, zero dependencies.

- **Server-side tour content (JSON from API)** — Tour steps fetched from DB/API, versioned server-side. Rejected: adds latency, complexity, and server coupling. Tours are rare (one per PWA, created once, rarely updated). Static steps in code are simpler and faster.

- **No tours, rely on Scene Guides instead** — All onboarding via Learning Hub Scene Guides (ADR-065). Rejected: Scene Guides are for reference material (screenshots, bilingual audio, permanent library). Tours are lightweight, in-context, dismissible. Both are needed: Scene Guide for learning, tour for onboarding.

- **Beacon-only (no persistent tour overlay)** — Show tour steps as persistent banners or side panels instead of modal overlays. Rejected: difficult to highlight specific UI elements without a full-screen overlay. Modal overlays (with dark background) ensure the user focuses on the current step.

## Consequences

**Positive:**

- **Consistent onboarding UX** — All PWAs follow the same step shape, visual design, and dismissal protocol. New staff see familiar, predictable tours across all apps.
- **Reduced support load** — Field staff can self-serve onboarding, reducing training calls and errors.
- **Code reuse** — PWAs add tours with ~20 lines of code (step definitions + one `initTour()` call). No duplicate tour infrastructure.
- **Maintenance** — A fix to tour.js (e.g., accessibility improvement, visual tweak) applies to all PWAs automatically.
- **Accessibility** — Shared tour.js can be improved once (keyboard navigation, ARIA labels, dark mode contrast) for all PWAs.

**Negative / Trade-offs:**

- **Cross-PWA coupling** — Changes to tour.js API affect all consuming PWAs. Mitigation: tour.js is mature and stable; breaking changes are rare. New major versions would follow ADR-079 (atomic update across all PWAs).
- **Limited customization** — The tour engine is intentionally simple. PWAs cannot customize colors, position, or animation. Mitigation: this uniformity is a feature (consistent UX); PWAs needing highly custom overlays should not use tours (they are not a good fit).
- **Storage conflicts** — Two tours on the same PWA with the same `storageKey` will conflict. Mitigation: clear naming convention (`<pwa>-tour-v<N>`) prevents collisions; only one tour per PWA is recommended.

**Risks and mitigations:**

- Risk: PWA developers misuse `storageKey` (e.g., use the same key for multiple tours, or use uppercase). Mitigation: clear naming convention and examples in this ADR; code review checks.
- Risk: A tour is updated but the version number is not incremented; existing users don't see the change. Mitigation: ADR documents when to increment; code review reminds developers.
- Risk: `screenFn` callback breaks (e.g., target element doesn't exist); tour crashes. Mitigation: tour.js wraps `screenFn()` in try-catch; tour continues even if callback fails.
- Risk: User dismisses tour immediately without reading; support load is not reduced. Mitigation: this is by design — tours are optional guidance. PWAs should invest in good UX (clear labels, helpful errors) as the primary onboarding; tours are a supplement, not a band-aid for poor design.

## Related Decisions

- **ADR-079** (Shared Helper Governance) — tour.js meets the 3+ PWA criterion (counters, recce, tour-planner, tour-pg); this ADR documents the API contract and usage standard per ADR-079 requirement.
- **ADR-065** (Scene Guide Format) — For richer onboarding with screenshots, bilingual audio, and permanent reference. Use Scene Guide when tours are insufficient.
- **ADR-013** (Single HTML File, No Framework) — tour.js is a self-hosted static file, not an npm package. All consuming PWAs link it via `<script src="/shared/tour.js">`.
- **ADR-002** (safe-bottom.css) — Similar shared CSS helper; tour.js follows the same governance and versioning rules.
- **ADR-005** (CACHE_VER String Bump) — When tour.js is updated, consuming PWAs may need to bump `CACHE_VER` to invalidate old cached versions (if the change is significant enough to warrant a new version).
- **ADR-081** (Safe-Area Inset Rendering) — tour.js overlay respects safe-area insets on notched devices; no content is hidden behind notches or rounded corners.
- **ADR-076** (Mobile-First Viewport) — tour.js is designed for mobile-first; tested on portrait Android PWA mode and adapts to viewport size.

## References

- `/var/www/360lm/shared/tour.js` — Source code (114 lines, vanilla JS)
- `/var/www/360lm/counters/index.html` — Consuming PWA (new balance inquiry tour)
- `/var/www/360lm/recce/index.html` — Consuming PWA (official approval workflow tour)
- `/var/www/360lm/tour-planner/index.html` — Consuming PWA (multi-stop route planning tour)
- `/var/www/360lm/tour-pg/index.html` — Consuming PWA (client portal tour)
- `docs/adr/ADR-079-shared-helper-governance.md` — Governance for shared helpers
- `docs/adr/ADR-065-scene-guide-format.md` — Scene Guide alternative for richer onboarding
- `memory/project_arch.md` — All PWAs and dependencies
