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

BucketLanded inAction required
Accessibility3.5.4None — automatic
Type fixes3.5.5None — corrects existing contracts
SEO / landing pages3.5.6None — site-only
Developer-experience features3.6.0Optional — adopt to simplify integration
Tooling / CLI3.6.1, 3.6.2None — companion packages
Content (templates + recipes)3.7.0Optional — opt in via template prop
Bundle size (Lite variants)3.8.0Optional — switch read-only views to /lite
DSR submission contract3.8.1Optional — 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 — its Omit<...> argument previously referenced submittedAt and estimatedCompletionDate, which don't exist on DSRRequest. Corrected to Omit<DSRRequest, 'id' | 'status' | 'createdAt' | 'updatedAt' | 'dueDate'>.
  • useDSR.getRequestsByStatus now accepts DSRStatus | RequestStatus — modern callers using literals like 'awaitingVerification' no longer get a type error.
  • DPIA type leakDPIAProvider and <NDPRDPIA> props now use the newly exported DPIAAnswerMap and DPIAAnswerValue instead of Record<string, any>, restoring callsite type-safety on onComplete, initialAnswers, and adapter.

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:

  • apiAdapter is production-ready. Adds credentials (defaults 'same-origin', set 'include' for cross-origin), dynamic headers (function form for runtime CSRF token lookup), loadMethod/saveMethod overrides, unwrap (for { data: ... } envelopes), configurable retry with exponential backoff and a shouldRetry predicate, and onError / onSuccess hooks for telemetry.
  • <NDPRConsent copy={...} /> — override title / description / acceptAll / rejectAll / customize / save strings 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 with submitOptions (credentials, headers) and onSubmitError. The existing state-managed adapter mode is unchanged.
  • Per-preset subpath entries /presets/consent, /presets/dsr, and /presets/policy ship only one preset each (~4 KB vs ~8 KB for the full /presets barrel).

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.

ModuleFull bundleLite bundleSaved
/lawful-basis/lawful-basis/lite36.7 KB12.7 KB65%
/cross-border/cross-border/lite53.3 KB5.6 KB89%
/ropa/ropa/lite36.9 KB13.2 KB64%

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 existing onSubmitError. Called with the parsed JSON body returned by your submitTo endpoint.
  • DSR submission payload contract is now documented on the Data Subject Rights component page — exact field names, optional vs required, and the matching DSRFormSubmission type.
  • 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:

  1. Typecheck. Run tsc --noEmit on your app — the 3.5.5 type-contract corrections should compile cleanly, but if you were relying on the broken submittedAt/estimatedCompletionDate Omit onuseDSR.submitRequest, you'll see a clear type error here.
  2. Run your tests. Component snapshots should be unchanged. Hook tests that depended on the old DPIA Record<string, any> may need to be tightened to DPIAAnswerMap.
  3. 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.
  4. Smoke-test a DSR submission. If you've adopted submitTo, verify the server returns 201 + JSON and that onSubmitSuccess fires with the parsed body.
  5. Re-run your bundle analyzer. Switching to /presets/dsr or 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