Step 3 (production-readiness): PII storage, validated reads, seams
Implement-now:
- G1: keep PII out of persistent storage — never persist BSN (only `naam`);
move both wizard drafts (address/email, work data) localStorage → sessionStorage
so they clear on tab close.
- G2: validate storage reads before trusting the cast — shape/tag guard in every
restore() (mirrors the parse* HTTP boundary); corrupt/foreign shape → start fresh.
- G3: already satisfied (debug-state redacts via mask.ts).
Show-the-seam (hook + doc, not fully built):
- G4: problemFieldErrors() maps a server validation envelope (ASP.NET
ValidationProblemDetails `errors`) to the field-keyed map the wizards already
render; returns {} until the backend sends it. +spec.
- G5: documented the retry/backoff seam at the adapter GET loader; reads may
retry, mutating submits never do.
Out of scope (named): unsaved-changes warning (persistence prevents data loss),
real auth/tokens, axe-core in CI.
Gate green: lint, check:tokens, build, test 79/79.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
|
||||
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
||||
draft via an effect; the DUO diploma list renders through <app-async>; choosing
|
||||
a diploma reveals its server-derived beroep. The draft is persisted to
|
||||
localStorage so a reload keeps the user's progress. Built entirely from existing
|
||||
sessionStorage so a reload keeps the user's progress (cleared on tab close). Built from existing
|
||||
atoms/molecules. */
|
||||
@Component({
|
||||
selector: 'app-registratie-wizard',
|
||||
@@ -294,14 +294,15 @@ export class RegistratieWizardComponent {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// An explicit seed (stories) wins; otherwise resume from localStorage.
|
||||
// An explicit seed (stories) wins; otherwise resume from sessionStorage.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() => this.dispatch({ tag: 'Seed', state: seeded !== initial ? seeded : (this.restore() ?? initial) }));
|
||||
// Persist only while filling in; clear once the flow is done.
|
||||
// Persist only while filling in; clear once the flow is done. G1: sessionStorage
|
||||
// (not localStorage) — the draft holds address/email and must not outlive the tab.
|
||||
effect(() => {
|
||||
const s = this.state();
|
||||
if (s.tag === 'Invullen') localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||
else localStorage.removeItem(STORAGE_KEY);
|
||||
if (s.tag === 'Invullen') sessionStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||
else sessionStorage.removeItem(STORAGE_KEY);
|
||||
});
|
||||
// Prefill the address from the BRP lookup as it arrives. Track only the resource
|
||||
// value; untrack the dispatch (it reads the state signal, which would otherwise
|
||||
@@ -325,10 +326,11 @@ export class RegistratieWizardComponent {
|
||||
}
|
||||
|
||||
private restore(): RegistratieState | null {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as RegistratieState;
|
||||
const parsed = JSON.parse(raw) as RegistratieState;
|
||||
return parsed?.tag === 'Invullen' ? parsed : null; // G2: only resume a known shape.
|
||||
} catch {
|
||||
return null; // ponytail: corrupt entry -> start fresh, no migration.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user