# Checklist d'activation — design (phase 1 du système d'onboarding)

Date : 2026-07-02 · Statut : validé (brainstorming) · Phase 2 (tours guidés par page) : hors périmètre, extension prévue.

## Objectif

Amener un nouvel utilisateur à la valeur du produit via une checklist d'activation
persistante dans la sidebar : ajouter son site → connecter GSC → importer des
mots-clés → lancer un premier export d'enrichissement → inviter un coéquipier
(optionnelle). La carte affiche la progression (« 2/5 »), un panneau détaille les
étapes avec un CTA chacune, et tout disparaît une fois l'activation faite.

## Décisions actées

- **Moteur : OnboardJS** (`@onboardjs/core` 1.0.0-rc.4 + `@onboardjs/react` 1.0.0-rc.5,
  MIT, peer deps React 18/19 — compatible React 19.2.7). ⚠️ La lib est en release
  candidate, un seul mainteneur (~70 étoiles) : l'isolation derrière un adaptateur
  (§ Anti-lock-in) est la **condition** de ce choix, pas une option.
- **La vérité, c'est l'état produit, jamais un état stocké.** La complétion des
  étapes est dérivée de la DB à chaque requête. OnboardJS ne persiste rien
  (aucun `localStoragePersistence`, aucun `customOnDataPersist`) : il reçoit
  l'état frais en `initialContext` et ne sert qu'à l'UX du panneau (étape
  suggérée, navigation, events).
- **UI : carte sidebar** (option A du brainstorming), au-dessus de la jauge quota.
- **Dérivé ⇒ zéro backfill** : les orgs existantes déjà actives ont toutes les
  étapes cochées, la carte ne s'affiche jamais chez elles.

## Étapes et dérivation

Scope : **org active** (les faits sont org/site-scoped). Une requête serveur
(`derive.ts`) calcule les cinq flags :

| # | id | Fait dérivé (org active) | CTA (navigation) | Obligatoire |
|---|----|--------------------------|------------------|-------------|
| 1 | `add-site` | ≥ 1 site dans l'org | page Sites (dialog d'ajout) | oui |
| 2 | `connect-gsc` | ≥ 1 site avec `sites.gscProperty` non nul | Settings → Intégrations | oui |
| 3 | `import-keywords` | ≥ 1 keyword sur un site de l'org | page Keywords (import) | oui |
| 4 | `first-enrichment` | ≥ 1 ligne `enrichment_exports` sur un site de l'org | Recherche → export | oui |
| 5 | `invite-teammate` | ≥ 1 membre ou invitation au-delà du owner | Settings → Team | non |

« Activation complète » = les 4 obligatoires. L'étape 5 est affichée, marquée
optionnelle, et ne bloque pas la disparition de la carte.

## Architecture

```
src/lib/onboarding/activation/
  steps.ts      — définition des 5 étapes : id, clés i18n, href CTA, required
  derive.ts     — serveur : deriveActivationState(orgId) → ActivationState (les 5 flags)
  actions.ts    — server actions : dismissActivation(), restoreActivation()
  engine.ts     — SEUL fichier qui importe @onboardjs/* : mappe nos types vers
                  la config OnboardingProvider (steps + initialContext + events)
  types.ts      — ActivationStep, ActivationState (l'interface que voit tout le reste)

src/components/onboarding/
  activation-card.tsx   — carte sidebar (titre, progress n/5, croix dismiss)
  activation-panel.tsx  — popover : liste des étapes, check, description, CTA
```

Flux : le RSC de la sidebar (layout `(app)`) appelle `deriveActivationState` +
lit le dismissal → si visible, rend `<ActivationCard state={...}>` (client).
La carte monte le provider OnboardJS via `engine.ts` avec l'état en
`initialContext.flowData` (pattern officiel Next.js App Router — les hooks
OnboardJS sont strictement client). `condition`/`nextStep(context)` des steps
lisent les flags pour pointer la première étape incomplète.

## Anti-lock-in

UI, actions et derive ne connaissent que `types.ts`. `engine.ts` est le seul
point de contact avec `@onboardjs/*`. Si la RC meurt, casse ou devient payante,
on réécrit `engine.ts` (~100 lignes) sans toucher au reste.

## Données nouvelles (une migration)

```sql
activation_dismissals (
  user_id  text REFERENCES "user"(id) ON DELETE CASCADE,
  org_id   text REFERENCES organization(id) ON DELETE CASCADE,
  dismissed_at timestamp NOT NULL DEFAULT now(),
  PRIMARY KEY (user_id, org_id)
)
```

Dismiss = par (user, org), avant complétion. Ré-ouvrable depuis Settings
(action `restoreActivation` = delete de la ligne). Pas d'autre donnée : la
progression n'est jamais stockée.

## Règles d'affichage

- Visible si : org active non complète (4 obligatoires) **et** pas de dismissal
  (user, org).
- Disparaît automatiquement quand les 4 obligatoires passent au vert.
- i18n complet `messages/{en,fr,es,de}.json` (le test de parité l'impose).
- Portails : appliquer le fix connu `pointer-events-auto` si le popover base-ui
  est monté dans un contexte radix.

## Erreurs

`deriveActivationState` en échec → la carte ne s'affiche pas, log serveur.
L'onboarding ne doit jamais casser l'app : tout est fail-silent côté UI.

## Tests

- `derive.test.ts` (DB de test) : un fixture par étape, org vide → 0/5, org
  complète → 5/5, org pré-existante riche → jamais visible.
- `actions.test.ts` : dismiss/restore, idempotence, scoping (user, org).
- `engine.test.ts` : mapping `ActivationState` → config OnboardJS (étape
  suggérée = première incomplète, étape 5 skippable).
- Gate habituel : tsc, biome, `bun run test:all`.

## Extensions prévues (hors périmètre)

- **Phase 2 — tours guidés par page** (réservoir, recherche, backlinks) :
  primitive de spotlight à choisir le moment venu, branchée sur les events du
  moteur.
- **Analytics** : plugin PostHog officiel OnboardJS sur les events
  (step viewed/completed/dismissed) — le projet a déjà PostHog.
