Migrating from 3.5.x to 3.8.x
Six releases landed between 3.5.x and 3.8.x. Every change is backward-compatible — this guide tells you what's new, what's worth adopting, and the one consolidated upgrade path most consumers actually need.
Who this is for
If you're on any 3.5.x release of @tantainnovative/ndpr-toolkit and want to adopt 3.8.x, this is your single source. Six releases (3.5.4, 3.5.5, 3.5.6, 3.6.0, 3.6.1, 3.6.2, 3.7.0, 3.8.0, 3.8.1) landed between them, all of them additive — no breaking API changes, no required code edits. Upgrading is pnpm add @tantainnovative/ndpr-toolkit@latest and then optionally adopting the new surface area.
If you're still on 3.3.x or 3.4.x, read Upgrading from 3.3.x first — that guide covers the BEM stylesheet import, the only required change in the 3.3 → 3.5 jump.
At a glance
| Bucket | Landed in | Action required |
|---|---|---|
| Accessibility | 3.5.4 | None — automatic |
| Type fixes | 3.5.5 | None — corrects existing contracts |
| SEO / landing pages | 3.5.6 | None — site-only |
| Developer-experience features | 3.6.0 | Optional — adopt to simplify integration |
| Tooling / CLI | 3.6.1, 3.6.2 | None — companion packages |
| Content (templates + recipes) | 3.7.0 | Optional — opt in via template prop |
| Bundle size (Lite variants) | 3.8.0 | Optional — switch read-only views to /lite |
| DSR submission contract | 3.8.1 | Optional — adopt typed onSubmitSuccess |
Net: nothing breaks. Pin the new version, your app continues to compile and run. The rest of this guide tells you what you can opt into and how.
3.5.4 — Accessibility
What changed
Two WCAG-related additions. The stylesheet now neutralises every toolkit animation and transition under @media (prefers-reduced-motion: reduce) (WCAG 2.3.3). A new shared useFocusTrap hook is exported from / and /hooks; it captures document.activeElement on activation, traps Tab cycling, optionally handles Escape, and restores focus on deactivation (WCAG 2.4.3). <ConsentBanner> now uses it internally — closing the banner returns focus to the trigger element instead of resting on <body>.
Action required
None. Both improvements are automatic. If you have your own modal surfaces that need the same behaviour, you can now reuse the hook:
import { useFocusTrap } from '@tantainnovative/ndpr-toolkit/hooks';
function PreferencesModal({ open, onClose }: Props) {
const ref = useFocusTrap<HTMLDivElement>({ active: open, onEscape: onClose });
return open ? <div ref={ref} role="dialog" aria-modal="true">{/* ... */}</div> : null;
}3.5.5 — Type fixes
What changed
Three corrections to type surface area that previously misrepresented runtime behaviour:
useDSR.submitRequest— itsOmit<...>argument previously referencedsubmittedAtandestimatedCompletionDate, which don't exist onDSRRequest. Corrected toOmit<DSRRequest, 'id' | 'status' | 'createdAt' | 'updatedAt' | 'dueDate'>.useDSR.getRequestsByStatusnow acceptsDSRStatus | RequestStatus— modern callers using literals like'awaitingVerification'no longer get a type error.- DPIA type leak —
DPIAProviderand<NDPRDPIA>props now use the newly exportedDPIAAnswerMapandDPIAAnswerValueinstead ofRecord<string, any>, restoring callsite type-safety ononComplete,initialAnswers, andadapter.
Also: 8 new tests on the 72-hour NDPC breach-notification deadline path (useBreach.getBreachesRequiringNotification) covering the 1h / 24h / 48h / 71.5h / expired / sort-by-urgency / already-notified cases.
Action required
None for most consumers. If you were typing your DSR payload manually with the broken Omit, your code becomes more type-safe automatically. If you were casting your DPIA answers as any, you can now drop the cast:
import type { DPIAAnswerMap } from '@tantainnovative/ndpr-toolkit';
const initial: DPIAAnswerMap = {
'data-categories': ['name', 'email'],
'has-children-data': false,
};
<NDPRDPIA initialAnswers={initial} onComplete={(answers) => save(answers)} />3.5.6 — SEO + landing pages
What changed
Site-only release. Three long-tail landing pages were added under ndprtoolkit.com.ng, page metadata was tightened to disambiguate Nigeria from generic data-protection queries, and application/ld+json structured-data blocks were added to the highest-traffic doc pages.
Action required
None. The library bundle is unchanged.
3.6.0 — Developer feedback
What changed
The biggest functional release in the 3.5 → 3.8 window. Four discrete improvements, all responses to integration feedback from production deployments:
apiAdapteris production-ready. Addscredentials(defaults'same-origin', set'include'for cross-origin), dynamicheaders(function form for runtime CSRF token lookup),loadMethod/saveMethodoverrides,unwrap(for{ data: ... }envelopes), configurableretrywith exponential backoff and ashouldRetrypredicate, andonError/onSuccesshooks for telemetry.<NDPRConsent copy={...} />— overridetitle/description/acceptAll/rejectAll/customize/savestrings without dropping to the lower-level<ConsentBanner>.<NDPRSubjectRights submitTo="..." />— public sites can now POST submissions to a backend route instead of being state-managed by an adapter. Pairs withsubmitOptions(credentials, headers) andonSubmitError. The existing state-managedadaptermode is unchanged.- Per-preset subpath entries —
/presets/consent,/presets/dsr, and/presets/policyship only one preset each (~4 KB vs ~8 KB for the full/presetsbarrel).
Action required (optional)
All four are opt-in. The most impactful for production deployments is the apiAdapter upgrade — recommended diff:
// Before — 3.5.x style, no telemetry, no retry
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
const adapter = apiAdapter<ConsentSettings>({ url: '/api/consent' });
// After — 3.6.0+, production-ready
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
const adapter = apiAdapter<ConsentSettings>({
url: '/api/consent',
credentials: 'include',
headers: () => ({ 'x-csrf-token': getCsrfToken() }),
unwrap: (body) => body.data,
retry: { maxAttempts: 3, backoffMs: 250 },
onError: (err, ctx) => telemetry.warn('consent_sync_failed', { ctx, err }),
});For bundle-size-sensitive landing pages that only need one preset, switch to the narrower subpath:
// Before
import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets';
// After — ships only NDPRConsent (~4 KB vs ~8 KB)
import { NDPRConsent } from '@tantainnovative/ndpr-toolkit/presets/consent';3.6.1 — create-ndpr fixes
What changed
Companion-CLI release. @tantainnovative/create-ndpr@0.2.0 stops emitting broken Prisma imports when you pick ORM=None, and a new unscoped create-ndpr alias lets npm create ndpr@latest and bun create ndpr work alongside the scoped form. README now ships StackBlitz + CodeSandbox badges.
Action required
None for library consumers. Main package is unchanged.
3.6.2 — Templates + StackBlitz
What changed
Phase D+E+F patch. The 9 remaining create-ndpr route templates now support all three ORM choices (Prisma, Drizzle, none) via conditional blocks — picking ORM=none now scaffolds a fully working in-memory app for every endpoint, not just consent. Eight per-module StackBlitz scaffolds shipped under examples/stackblitz/{consent,dsr,dpia,breach,policy,lawful-basis,cross-border,ropa}. The /score lead-magnet email capture now uses a real Web3Forms backend with a mailto fallback.
Action required
None. Main library has no code changes.
3.7.0 — Org templates + recipes
What changed
Phase B — the template + recipe library. Five organisation-specific privacy-policy templates (saas, ecommerce, school, healthcare, procurement) are exposed via a new templateContextFor(id, overrides?) factory and a matching template prop on <NDPRPrivacyPolicy />. Each template pre-fills sector-appropriate data categories, lawful-basis defaults, and the right flags for children / sensitive / financial / cross-border / automated-decisions processing.
Five production-tested recipe pages also landed at /docs/recipes/*: ecommerce-consent, newsletter-consent, contact-form-disclosure, careers-rights, and admin-dsr-management.
Action required (optional)
The simplest way to use an org template — drop the id and the wizard opens already populated:
// Before — every consumer answered the same 4-step wizard from scratch
<NDPRPrivacyPolicy adapter={policyAdapter} onComplete={save} />
// After — pre-fill the wizard for your sector
<NDPRPrivacyPolicy
template="ecommerce"
templateOverrides={{ orgName: 'Acme Stores', dpoEmail: 'dpo@acme.ng' }}
adapter={policyAdapter}
onComplete={save}
/>For a template-picker UI, the ORG_POLICY_TEMPLATE_REGISTRY constant exposes id, label, description, and example org types for each of the five templates.
3.8.0 — Lite manager variants
What changed
Phase G — three read-only Lite variants of the heavy Manager components: LawfulBasisTrackerLite, ROPAManagerLite, and CrossBorderTransferManagerLite. Each renders the same list-and-summary view as its Full counterpart, minus every write affordance (Add, Edit, Archive, Delete, CSV export). They're purpose-built for dashboards, audit pages, embedded compliance widgets, and customer-facing transparency surfaces.
| Module | Full bundle | Lite bundle | Saved |
|---|---|---|---|
/lawful-basis → /lawful-basis/lite | 36.7 KB | 12.7 KB | 65% |
/cross-border → /cross-border/lite | 53.3 KB | 5.6 KB | 89% |
/ropa → /ropa/lite | 36.9 KB | 13.2 KB | 64% |
The cross-border Lite saves the most because it skips the 124 KB country-adequacy dataset — Lite displays adequacyStatus directly from each transfer record instead of recomputing it.
Action required (optional)
If a surface only needs to display records (not edit them), switch the import:
// Before — heavy manager with all write affordances on a read-only dashboard
import { CrossBorderTransferManager } from '@tantainnovative/ndpr-toolkit/cross-border';
// After — same data, no edit/add/delete, saves 47.7 KB of JS
import { CrossBorderTransferManagerLite } from '@tantainnovative/ndpr-toolkit/cross-border/lite';
<CrossBorderTransferManagerLite
transfers={transfers}
onTransferClick={(t) => router.push(`/admin/transfers/${t.id}`)}
/>The full per-prop comparison and the “when to use which” decision tree are in the Lite vs Full managers guide.
3.8.1 — DSR submission polish
What changed
Phase H polish on the 3.6.0 submitTo mode. Three additions:
<NDPRSubjectRights onSubmitSuccess={...} />— typed counterpart to the existingonSubmitError. Called with the parsed JSON body returned by yoursubmitToendpoint.- DSR submission payload contract is now documented on the Data Subject Rights component page — exact field names, optional vs required, and the matching
DSRFormSubmissiontype. - Accessibility notes on each component doc page document the WCAG criteria already met by 3.5.4 and what consumers are responsible for (form labels, error announcements, focus management).
Action required (optional)
If you're already using submitTo, wire the success callback to show a confirmation or update local state without re-fetching:
<NDPRSubjectRights
submitTo="/api/dsr"
onSubmitError={(err) => toast.error(err.message)}
onSubmitSuccess={(body) => {
toast.success(`Request ${body.requestId} received`);
router.push(`/dsr/status/${body.requestId}`);
}}
/>Net migration diff — the common path
Most 3.5.x consumers running a public DSR form fall into this shape: an adapter-backed <NDPRSubjectRights> that writes to a homegrown API client. In 3.8.x, the recommended path is the built-in submitTo mode paired with validateDsrSubmission on the server. Here is the entire before/after, including the route handler:
Before (3.5.x)
// app/dsr/page.tsx
'use client';
import { NDPRSubjectRights } from '@tantainnovative/ndpr-toolkit/presets';
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
const adapter = apiAdapter({ url: '/api/dsr' });
export default function DSRPage() {
return <NDPRSubjectRights adapter={adapter} />;
}After (3.8.x)
// app/dsr/page.tsx
'use client';
import { NDPRSubjectRights } from '@tantainnovative/ndpr-toolkit/presets/dsr';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
export default function DSRPage() {
const router = useRouter();
return (
<NDPRSubjectRights
submitTo="/api/dsr"
submitOptions={{
credentials: 'same-origin',
headers: () => ({ 'x-csrf-token': getCsrfToken() }),
}}
onSubmitSuccess={(body) => {
toast.success(`Request ${body.requestId} received — we'll respond within 30 days.`);
router.push(`/dsr/status/${body.requestId}`);
}}
onSubmitError={(err) => toast.error(err.message)}
/>
);
}New backend handler
// app/api/dsr/route.ts
import { validateDsrSubmission } from '@tantainnovative/ndpr-toolkit/server';
import { dsrStore } from '@/lib/dsr-store';
export async function POST(req: Request) {
const result = validateDsrSubmission(await req.json());
if (!result.valid) {
return Response.json({ errors: result.errors }, { status: 422 });
}
const record = await dsrStore.create(result.data);
return Response.json(
{ requestId: record.id, status: record.status, dueDate: record.dueDate },
{ status: 201 },
);
}What you traded: an ad-hoc apiAdapter with no telemetry, no retry, no validation, and no typed success hook. What you got: a typed contract on both ends, CSRF + retry in the SDK, server-side schema validation, and a smaller bundle on the page (/presets/dsr ships only NDPRSubjectRights).
Bundle-size opportunity
If your app embeds CrossBorderTransferManager, ROPAManager, or LawfulBasisTracker anywhere it doesn't need to write, the 3.8 Lite variants are pure upside — same data, same look, 64–89% less JavaScript. The Lite vs Full managers guide has the decision tree, per-prop comparison, and bundle measurements.
Verify your upgrade
A short checklist after bumping. Each one catches a different regression class:
- Typecheck. Run
tsc --noEmiton your app — the 3.5.5 type-contract corrections should compile cleanly, but if you were relying on the brokensubmittedAt/estimatedCompletionDateOmit onuseDSR.submitRequest, you'll see a clear type error here. - Run your tests. Component snapshots should be unchanged. Hook tests that depended on the old DPIA
Record<string, any>may need to be tightened toDPIAAnswerMap. - Smoke-test the consent banner.Open and close it with a keyboard — focus should return to whatever opened it (the 3.5.4 fix), and macOS users with “Reduce Motion” on should see no slide-in animation.
- Smoke-test a DSR submission. If you've adopted
submitTo, verify the server returns 201 + JSON and thatonSubmitSuccessfires with the parsed body. - Re-run your bundle analyzer. Switching to
/presets/dsror any Lite variant should produce a visibly smaller chunk for the affected route.
That's the entire upgrade. No required code changes, optional adoption of better defaults, and a clear path to a smaller bundle if you want it.
Next steps
- Lite vs Full managers — when to switch and what you save.
- Data Subject Rights component reference — the documented
submitTopayload contract and accessibility notes. - Server Rendering & RSC — using
validateDsrSubmissionand the rest of/server. - Admin DSR Management — the DPO-side workflow that pairs with the new public form.