consentera_consent 2.0.0
consentera_consent: ^2.0.0 copied to clipboard
Consentera Consent SDK for Flutter — DPDP Act 2023 compliant consent for mobile apps.
consentera_consent #
DPDP (India, Digital Personal Data Protection Act 2023) consent for Flutter.
The app never holds a secret key. Your own backend holds tiq_live_* and
proxies to Consentera; this SDK talks to your backend, opens Consentera's hosted
notice in an in-app browser, and reads the decision back.
- Version 2.0.0 · Dart ≥3.5, Flutter ≥3.24
Install #
dependencies:
consentera_consent: ^2.0.0
Register your callback scheme in AndroidManifest.xml
(<data android:scheme="yourapp" android:host="consent" />) and in Info.plist
(CFBundleURLTypes). It must ALSO be registered server-side in
ALLOWED_CALLBACK_APP_SCHEMES, or session create is refused 400.
Quickstart #
import 'package:consentera_consent/consentera_consent.dart';
final session = ConsenteraSession(const SessionConfig(
// YOUR backend's route to Consentera. Required — there is no default server,
// and a blank or scheme-less value throws ArgumentError('backendBaseUrl').
backendBaseUrl: 'https://api.yourbank.in/consentera',
callbackScheme: 'yourapp',
));
// The ONE shape the lifecycle roads accept: an OPEN map keyed by YOUR
// organisation's locked integration key (F015), the same shape and the same
// vocabulary `dataPrincipal` uses on create. A bare string
// (`data_principal_ref`) is refused outright by the API.
const who = PrincipalRef.identifiedBy({'email': 'riya@example.in'});
// 1 — create the session through YOUR backend
final s = await session.createSession(
noticeInternalName: 'your_notice_code',
// Keyed by YOUR organisation's locked integration key — the SAME vocabulary
// the lifecycle roads take. The mobile atom is `mobile` on both.
dataPrincipal: {'email': 'riya@example.in'},
dateOfBirth: '1998-04-12', // omit and the person's age is UNKNOWN
);
if (s.warnings.isNotEmpty) debugPrint(s.warnings.join('; ')); // arrive with a 200
if (s.guardianVerification != null) {
// §9(1): a child, invitation sent to the guardian, NOT yet consented.
}
// 2 — present the hosted notice in the in-app browser
await session.presentConsentSession(s);
// 3 — on resume, check any deep link and then VALIDATE
// (validate is the only source of consent truth, and it is also what covers
// the person who simply closed the tab: no callback ever arrives)
final cb = session.parseCallback(incomingUri, expectedState: s.callbackState);
// The platform returns
// yourapp://consent/callback?artifact_id=…&pending=1&session_id=…&state=…&status=granted|partial|denied
// cb.callbackStatus is granted / partial / denied, else unknown (never a
// grant); cb.pending means the record is still being written, so your
// backend's read-back may answer 202 + Retry-After. Both are HINTS.
final d = await session.validate(who, 'product_analytics');
if (d.allowed) { /* process */ }
// 4 — withdrawal must be as easy as giving (DPDP §6(4))
await session.withdraw(who, ['product_analytics']);
// 5 — the full rights portal (access, erasure, nomination, grievance), one tap.
// The same identifier map as everywhere else; the key is the kind.
await session.openPortal({'email': 'riya@example.in'});
// Release the HTTP client the session owns when you are done with it.
session.close();
Re-validate on AppLifecycleState.resumed: Chrome blocks gesture-less
custom-scheme redirects, so a person who closes the tab produces no callback
at all.
One identifier vocabulary (F015) #
dataPrincipal on create and PrincipalRef.identifiedBy on the lifecycle roads
are the same open map, keyed by the fields your organisation locked. This SDK
does not allow-list them: the admissible set is a per-tenant fact the
platform reads at request time, so send what your key defines and let the server
answer.
the portal (openPortal) |
the same map, and the key is the kind: {'mobile': '+91…'} is sent as a mobile reference, never as an email. The portal takes ONE identifier: pass one, or set identifierScheme so the SDK can choose. phone is refused before the wire. |
| the mobile atom | mobile on both roads. phone is refused by name — UNKNOWN_IDENTIFIER_FIELD, whose message says "…use mobile". |
pan |
not a scheme field at all. It is evidence-class and can never be one. |
aadhaar |
the Aadhaar-linked token, never the number. A raw 12-digit value is refused — but the code differs by road: INVALID_IDENTIFIER_FORMAT on the lifecycle roads (F015 folded the old token into it there), and still AADHAAR_RAW_REFUSED on session create. Switch on both, or on the 400 alone. |
The four refusals you can switch on: UNKNOWN_IDENTIFIER_FIELD (names the field
you sent and the ones that are allowed), IDENTIFIER_REQUIRED (you named
nobody), INVALID_IDENTIFIER_FORMAT (right field, wrong value — this is also where a raw
Aadhaar number lands on these roads), and SCHEME_NOT_CONFIGURED (this tenant has no locked key yet — an onboarding
problem, not a request problem).
Only the wire KEY differs between the two roads: data_principal on create may
mint a person, data_principal_identifiers on the lifecycle roads resolves only.
ConsentGate — the mobile equivalent of cookie consent #
final gate = ConsentGate(session, who);
gate.register(Tracker(
purposeCode: 'product_analytics',
name: 'Analytics SDK',
init: analytics.start,
revoke: analytics.stopAndClear,
));
await gate.refresh(); // on start and on every resume
init runs at most once per allowed-transition; revoke runs when a purpose
stops validating. Validation failures are fail-closed.
What this SDK will not do #
It refuses rather than degrading:
| Missing | It does | It will not |
|---|---|---|
| in-app browser | throws ConsenteraBrowserUnavailable |
fall back to LaunchMode.externalApplication — the external browser leaves the callback to any app that claims the scheme |
| a Custom Tabs browser, on Android | throws ConsenteraBrowserUnavailable (code: 'NO_BROWSER') |
let url_launcher fall back to its own WebView, which has no address bar, so the person cannot see whose page is asking |
callback whose scheme, host, path or state does not match |
throws ConsenteraCallbackRejected |
accept it |
| a 2xx that is not JSON | throws ConsenteraException |
let a raw FormatException escape |
It has no advertising function: no advertising identifier, no IABTCF keys. That is a separate concern and would be a separate package.
Errors, retries, TLS #
ConsenteraException carries statusCode, code (the platform's canonical
code), platformMessage and requestId.
platformMessage is the platform's own sentence for a refusal, verbatim. Show
it to the person, because it says why. It is kept out of message and
toString(), as is the rest of the response body, because a body can carry the
person's own identifiers and an exception message ends up wherever you log.
try {
await session.validate(who, 'product_analytics');
} on ConsenteraException catch (e) {
show(e.platformMessage ?? 'Something went wrong');
report(e.code, e.statusCode, e.requestId); // for support
}
Android: declare the Custom Tabs query #
On Android the SDK presents the hosted pages only in a Custom Tab. It checks
for one first, and refuses with NO_BROWSER rather than let url_launcher fall
back to its address-bar-less WebView. On Android 11+ that check can see a
Custom Tabs browser only if your app's manifest asks to:
<!-- android/app/src/main/AndroidManifest.xml, a child of <manifest> -->
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
Without it, every Android 11+ device refuses. This package is pure Dart, so it cannot merge that entry into your manifest for you.
Mutations carry an Idempotency-Key minted once per operation and reused across
retries; 429 and 5xx are retried with full jitter, honouring Retry-After; a
4xx is never retried.
Certificate pinning is the constructor's client: parameter — pass an
IOClient over an HttpClient with your own badCertificateCallback, or any
http.Client. A client you pass in is yours: close() will not close it.
| Option | Default | |
|---|---|---|
requestTimeout |
30s | per ATTEMPT |
totalTimeout |
2× the above | ceiling on the whole call including retries |
maxAttempts |
3 | including the first; 1 disables retrying |
retryBaseDelay |
250ms | full-jitter base |
onDiagnostic |
— | opt-in sink; the SDK is otherwise SILENT |
callbackHost / callbackPath |
consent / /callback |
both are CHECKED on the callback |
A complete example app ships in example/ (BNB Pay).
Support #
Security issues: see SECURITY.md at the repository root.
Full integration guide: https://docs.consentera.in/docs/developer/df-integration/apps/mobile-04-flutter