@akku-work/consent-sdk-react
v0.1.0-beta.1
Published
Akku Consent - React bindings and consent surfaces for an authenticated host application.
Readme
@akku-work/consent-sdk-react
React bindings and consent surfaces for an authenticated host application.
This package renders. @akku-work/consent-sdk-auth decides — it resolves
each published purpose to a legal basis, a validity window and an outcome, and this package draws
that. Nothing here evaluates expiry, derives a basis, or decides whether a purpose needs asking; if
you find yourself adding logic of that kind here, it belongs on the other side.
Install
npm install @akku-work/consent-sdk-react @akku-work/consent-sdk-auth@akku-work/consent-sdk-auth and react are peer dependencies. The auth SDK is a peer rather than a
dependency on purpose: your app must end up with exactly one ConsentManager. Two copies each
hold their own onChange listeners and their own view of the subject's decisions, so a write through
one would never notify the other.
Wiring
import { ConsentManager } from "@akku-work/consent-sdk-auth";
import { ConsentProvider } from "@akku-work/consent-sdk-react";
const manager = new ConsentManager({
apiHost: "https://consent.nova.example",
siteKey: "sk_live_9f2c…",
applicationId: "nova-bank-web",
getSubjectToken: () => session.fetchConsentToken(),
});
<ConsentProvider manager={manager}>
<App />
</ConsentProvider>;No userId is ever passed. Identity travels only inside the host-signed subject token, and the
backend derives the subject by verifying it — a userId handed over from the browser is a claim, not a
credential.
Gating a feature
const { isGranted, loading } = useConsent();
if (isGranted("device.analytics")) trackDashboardView();isGranted is false while loading and false for a key the policy does not declare. This is a
gate, and a gate that opens because the answer has not arrived yet is not a gate. Read loading
alongside it when you need to tell "no" from "not yet".
The four surfaces
Which one to use is a property of the moment, not of the purpose.
| Surface | Use for |
| --- | --- |
| AskModal | One purpose, at the moment it matters. Blocks, so a user action must provoke it. |
| DisclosureModal | A collection point, fired when a form opens. Discloses, then captures. |
| AskSnackbar | A low-stakes ask that must not interrupt. |
| PreferenceSheet | The standing entry point. Every toggle is an immediate write. |
// Renders nothing at all when there is nothing to ask — already decided and still valid, a basis
// that is never prompted, or a key the policy does not declare. Mount it where the question belongs
// and let it decide whether to appear.
<AskModal purposeKey="email.marketing" source="profile" />source is required and is stamped on the ledger event. It records where consent was captured,
which is what makes a consent record auditable after the fact.
Rules these components will not let you break
These are not style choices. Each one is a way a consent UI can misrepresent what a person chose.
- The legal basis decides the control.
consentgets a toggle;necessaryandlegal_obligationrender as "Always on" with no control at all. You cannot pass a prop to override this — offering a switch for something that cannot be switched off is the commonest way a consent UI lies. - Nothing is pre-ticked.
DisclosureModalstarts every choice unanswered and will not save until something is answered. A pre-ticked box is not consent, and an empty save would log a consent event for a decision nobody made. - A dismissal is never an answer. Escape, the backdrop and "Later" record nothing. Silence is not consent, and it is not a refusal either.
- No auto-dismiss timer on the snackbar. A surface that fades into a decision turns inaction into one.
- Decline is never de-emphasised. Accept and decline take the same button treatment; equal prominence is a contract requirement.
- The preference sheet has no Save button. Every toggle writes immediately, because a preference centre with unsaved changes is one that loses them — and a withdrawal has to take effect when it is asked for.
- A failed read never renders as "you have no choices." The error is shown and the last known state is kept. An unreachable API must not blank the screen or break the page around it.
Styling
Import the stylesheet. The package ships its own look:
import "@akku-work/consent-sdk-react/styles.css";This is not optional, and the package deliberately does not leave it to you. An earlier version
shipped class names and no CSS, on the reasoning that a host app has its own design system. That was
wrong: decline must never be de-emphasised relative to accept, and a requirement enforced only by a
comment in someone else's stylesheet is not enforced. Here both buttons take their height, size and
weight from the same rule — only background differs — so there is no separate declaration to
override.
Retheme with custom properties rather than by rewriting rules:
:root {
--akku-iris-500: #0d9488; /* accent */
--akku-iris-600: #0f766e; /* accent, hover */
--akku-ink: #0f172a;
--akku-font-sans: "Your Face", system-ui, sans-serif;
}Every class also carries display with !important. That is not defensiveness for its own sake: a
page-level * { display: none } reset would otherwise hide consent controls, and a surface a visitor
cannot see is a consent request that never happened. The same defence exists in @akku-work/consent-sdk,
where the bug was made twice.
The primitives (PurposeRow, LegalBasisPill, ConsentToggle, SurfaceHeader, SurfaceFooter,
ReasonBanner, PurposeKeyMeta) are exported so you can compose a surface of your own without
re-deriving the rules above and getting one wrong.
Known limit
Expiry does not currently fire on the authenticated plane. Its consent record carries one
updatedAt for the whole record rather than a per-purpose timestamp, so a decision cannot be aged
individually — and deriving it from the record-level timestamp would lapse a purpose answered
yesterday because a different one was answered a year ago. validityDays is published, displayed and
carried through; it simply does not yet cause a re-ask here. Tracked on AK-8494.
Licence
Proprietary. See LICENSE — publication on npm grants no licence to use this software.
