@redacto.io/consent-sdk-react-native 5.0.1-beta.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,6 +85,7 @@ Pass the sandbox `token`, org/workspace, and a test identity (`email`, `mobile`,
85
85
  import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react-native";
86
86
 
87
87
  <RedactoNoticeConsent
88
+ baseUrl={BASE_URL} // the API host, e.g. https://api.redacto.io
88
89
  noticeId={NOTICE_UUID}
89
90
  token={SANDBOX_TOKEN}
90
91
  organisationUuid={ORG_UUID}
@@ -104,7 +105,6 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react-native/priva
104
105
 
105
106
  <RedactoPrivacyCenter
106
107
  baseUrl={BASE_URL}
107
- slug="acme-co"
108
108
  token={SANDBOX_TOKEN}
109
109
  organisationUuid={ORG_UUID}
110
110
  workspaceUuid={WORKSPACE_UUID}
@@ -131,7 +131,7 @@ Both `RedactoNoticeConsent` and `RedactoPrivacyCenter` accept the same sandbox p
131
131
 
132
132
  - The SDK sends the sandbox token as an `X-Consent-Token` header and the test identity inside each request (query params on reads, body on writes) — no JWT decode, no token refresh (a `401` fails fast).
133
133
  - The server records everything under `environment=test`, namespaced so sandbox data never mixes with — or reads — your live records.
134
- - Sandbox always talks to the consent server; it never routes to the Go ledger.
134
+ - With the API host as `baseUrl`, sandbox requests go to the same services as live ones (see [`baseUrl` and service routing](#baseurl-and-service-routing)). With the deprecated consent server URL and no `ledgerBaseUrl`, sandbox talks to the consent server only.
135
135
 
136
136
  ### Going live
137
137
 
@@ -143,10 +143,10 @@ Sandbox and production use the same components. To ship, remove the sandbox prop
143
143
 
144
144
  The SDK never talks to Redacto with your CMS API key directly — your backend acts as a token broker. Obtain your **CMS-API-Key** from the Redacto Dashboard, store it in your backend's environment, and expose two endpoints to your mobile app:
145
145
 
146
- 1. **Generate access token** — `POST /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/generate`
147
- 2. **Refresh access token** — `POST /consent/public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/refresh`
146
+ 1. **Generate access token** — `POST {consent server}/public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/generate`
147
+ 2. **Refresh access token** — `POST {consent server}/public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/refresh`
148
148
 
149
- Both endpoints expect the `X-CMS-API-Key` header. Example (Express):
149
+ The consent server is `{API host}/consent`, e.g. `https://api.redacto.io/consent`: the backend calls it directly, so it takes the consent server URL even though the SDK takes the API host. Both endpoints expect the `X-CMS-API-Key` header. Example (Express), where `base_url` is the consent server URL:
150
150
 
151
151
  ```ts
152
152
  import express from "express";
@@ -174,7 +174,7 @@ app.post("/api/consent/refresh-token", async (req, res) => {
174
174
  const { refresh_token, organisation_uuid, workspace_uuid, base_url } = req.body;
175
175
 
176
176
  const response = await axios.post(
177
- `${base_url}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/refresh`,
177
+ `${base_url}/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/refresh`,
178
178
  { refresh_token },
179
179
  { headers: { "X-CMS-API-Key": CMS_API_KEY } }
180
180
  );
@@ -195,6 +195,7 @@ Modal consent component. Supports initial consent, reconsent, age verification,
195
195
  import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react-native";
196
196
 
197
197
  <RedactoNoticeConsent
198
+ baseUrl="https://api.redacto.io"
198
199
  noticeId="your-notice-uuid"
199
200
  accessToken={accessToken}
200
201
  refreshToken={refreshToken}
@@ -210,19 +211,49 @@ import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react-native";
210
211
  | `noticeId` | `string` | ✓ | UUID of the consent notice |
211
212
  | `accessToken` | `string` | ✓ | JWT access token |
212
213
  | `refreshToken` | `string` | ✓ | JWT refresh token |
213
- | `baseUrl` | `string` | | Override API base URL |
214
- | `ledgerBaseUrl` | `string` | | Go ledger base URL. When set, consent writes and prior-consent checks route through the ledger (falls back: ledger → baseUrl → default). Omit to keep the legacy consent-server path. |
215
- | `settings` | `Partial<Settings>` | | UI customisation (colours, border radius, fonts) |
214
+ | `baseUrl` | `string` | | API host, e.g. `https://api.redacto.io` (recommended). The consent server URL (`…/consent`) still works but is deprecated. Defaults to `https://api.redacto.io/consent`. See [`baseUrl` and service routing](#baseurl-and-service-routing) |
215
+ | `ledgerBaseUrl` | `string` | | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases: it then routes consent writes and already-consented checks through the ledger. Ignored when `baseUrl` is the API host |
216
+ | `settings` | `Partial<Settings>` | | Overrides for the appearance set in the Redacto console, key by key (see [Appearance](#appearance)) |
216
217
  | `language` | `string` | | Initial language code (e.g. `"en"`) |
217
218
  | `blockUI` | `boolean` | | Block the modal from being dismissed |
218
219
  | `applicationId` | `string` | | Application-specific UUID |
219
220
  | `validateAgainst` | `"all" \| "required"` | | Validation mode |
220
221
  | `includeFullyConsentedData` | `boolean` | | Include already-consented items (review mode) |
221
- | `reviewModeButtonText` | `string` | | CTA text in review mode |
222
+ | `reviewModeButtonText` | `string` | | With `includeFullyConsentedData`, the button shown when everything is already consented; it calls `onAccept`. Default `"Continue"` |
222
223
  | `onAccept` | `() => void` | ✓ | Called on successful consent |
223
224
  | `onDecline` | `() => void` | ✓ | Called when user declines |
224
225
  | `onError` | `(error: Error) => void` | | Called on API errors |
225
226
 
227
+ #### Appearance
228
+
229
+ The notice renders the appearance configured in the Redacto console (`active_config.appearance`): style (`classic`, `glass`, `minimal`, `soft`), layout (centred modal or bottom sheet on phones; centre, corner or bottom bar on wider screens), palette and dark palette, corner radius, logo size and position, selection control (checkbox, radio or dropdown, on purposes, product headings and data elements; checkboxes can be drawn as switches), text scale, motion, collapsible sections and an optional "are you sure" step before submitting. The frame around the admin's copy (yes/no, the confirm step, the verification panel, hints and labels) is shown in the notice's language. With nothing configured it looks as it did before.
230
+
231
+ Every `settings` key overrides the console key by key:
232
+
233
+ | Key | Type | Description |
234
+ | --- | --- | --- |
235
+ | `theme` | `"classic" \| "glass" \| "minimal" \| "soft"` | Visual style |
236
+ | `selectionControl` | `"checkbox" \| "radio" \| "dropdown"` | Control on purposes, product headings and data elements |
237
+ | `controlStyle` | `"checkbox" \| "switch"` | How checkbox rows are drawn, in any style |
238
+ | `confirmBeforeSubmit` | `boolean` | Ask "are you sure" before Accept All, Accept Selected or Decline submits |
239
+ | `mobileLayout` | `"modal" \| "sheet"` | Phones: centred or docked to the bottom edge |
240
+ | `desktopPosition` | `"center" \| "bottom-right" \| "bottom-left" \| "bottom-bar"` | Wider screens |
241
+ | `backdrop` | `"dim" \| "none"` | Dim the screen behind the notice |
242
+ | `collapsibleSections` | `boolean` | Collapse the notice text and DPO block into disclosures |
243
+ | `phoneFooter` | `"stacked" \| "paired"` | Phone button layout |
244
+ | `autoDark`, `darkPalette` | `boolean`, palette | Follow the device's dark mode (`useColorScheme`) with this palette |
245
+ | `backgroundColor`, `surfaceColor`, `headingColor`, `textColor`, `mutedTextColor`, `borderColor`, `overlayColor`, `link` | `string` | Palette |
246
+ | `button.accept`, `button.acceptAll`, `button.acceptSelected`, `button.decline`, `button.language` | colours | Button colours |
247
+ | `toggle`, `ttsHighlight` | colours | Switch colours; the element being read aloud |
248
+ | `borderRadius`, `maxWidth`, `logoSize` | `string \| number` | Corner radius, wide-screen width, logo height, in px |
249
+ | `textScale` | `"sm" \| "md" \| "lg"` | Scales every text size |
250
+ | `motion` | `boolean` | `false` turns entrances and transitions off (they are also off while the device asks for reduced motion) |
251
+ | `font` | `string` | Font family, winning over the console's |
252
+
253
+ React Native has no blur, so `glass` draws a denser tinted surface instead of a frosted one. `customCss` is accepted for parity with the web SDK and ignored. A phone sheet, a corner card and a bottom bar size to their content, up to 92% (sheet) or 70% of the screen.
254
+
255
+ The language selector names each language in its own script ("हिन्दी (Hindi)"). If the notice fails to load, it stays on screen with the reason and a **Refresh** button (closing it counts as a decline), and `onError` is still called. With `includeFullyConsentedData`, a visitor who has already consented to everything sees a single **Continue** button (`reviewModeButtonText`) that calls `onAccept`.
256
+
226
257
  ### `RedactoNoticeConsentInline`
227
258
 
228
259
  Inline consent component. Auto-submits when all required elements are checked and an access token is available.
@@ -231,6 +262,7 @@ Inline consent component. Auto-submits when all required elements are checked an
231
262
  import { RedactoNoticeConsentInline } from "@redacto.io/consent-sdk-react-native";
232
263
 
233
264
  <RedactoNoticeConsentInline
265
+ baseUrl="https://api.redacto.io"
234
266
  org_uuid="org-uuid"
235
267
  workspace_uuid="workspace-uuid"
236
268
  notice_uuid="notice-uuid"
@@ -248,16 +280,20 @@ import { RedactoNoticeConsentInline } from "@redacto.io/consent-sdk-react-native
248
280
  | `workspace_uuid` | `string` | ✓ | Workspace UUID |
249
281
  | `notice_uuid` | `string` | ✓ | Notice UUID |
250
282
  | `accessToken` | `string` | | JWT access token (triggers auto-submit when set) |
251
- | `baseUrl` | `string` | | Override API base URL |
252
- | `ledgerBaseUrl` | `string` | | Go ledger base URL. When set, consent writes and already-consented checks route through the ledger. Omit for legacy behavior. |
283
+ | `baseUrl` | `string` | | API host, e.g. `https://api.redacto.io` (recommended). The consent server URL (`…/consent`) still works but is deprecated. Defaults to `https://api.redacto.io/consent`. See [`baseUrl` and service routing](#baseurl-and-service-routing) |
284
+ | `ledgerBaseUrl` | `string` | | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases: it then routes consent writes and already-consented checks through the ledger. Ignored when `baseUrl` is the API host |
253
285
  | `language` | `string` | | Language code |
254
- | `settings` | `Partial<Settings>` | | UI customisation |
286
+ | `settings` | `Partial<Settings>` | | Overrides for the console appearance (palette, text scale, selection control, `confirmBeforeSubmit`, …), as on `RedactoNoticeConsent` |
255
287
  | `applicationId` | `string` | | Application-specific UUID |
256
288
  | `onAccept` | `() => void` | | Called on successful consent |
257
289
  | `onDecline` | `() => void` | | Called when user declines |
258
290
  | `onError` | `(error: Error) => void` | | Called on errors |
259
291
  | `onValidationChange` | `(isValid: boolean) => void` | | Fires when validation state changes |
260
292
 
293
+ The inline notice follows the console appearance too, and starts from the notice's pre-selection (`purpose_preselection`: nothing ticked, the required elements, or everything), so it can auto-submit on first paint when that satisfies the required elements and a token is present. With "confirm before submit" set, it asks before the auto-submit and records nothing if the visitor says no.
294
+
295
+ `RedactoNoticeAssisted` (operator-assisted capture) displays the console appearance as well and takes the same `settings` prop; what it submits is unchanged.
296
+
261
297
  ## TTS Audio
262
298
 
263
299
  If `expo-audio` is installed, the consent notice will dynamically load it and enable text-to-speech playback. The SDK calls `setAudioModeAsync` and creates an audio player internally — you don't need to set anything up beyond installing the package. See the [expo-audio docs](https://docs.expo.dev/versions/latest/sdk/audio/) if you need to customise audio session behaviour app-wide.
@@ -277,7 +313,6 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react-native/priva
277
313
 
278
314
  <RedactoPrivacyCenter
279
315
  baseUrl={BASE_URL}
280
- slug="acme-co"
281
316
  accessToken={accessToken}
282
317
  refreshToken={refreshToken}
283
318
  theme="light"
@@ -293,15 +328,67 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react-native/priva
293
328
 
294
329
  | Prop | Type | Required | Default | Description |
295
330
  | -------------- | ----------------------------------------------------------------------------- | -------- | -------------------- | ---------------------------------------- |
296
- | `baseUrl` | `string` | ✓ | - | Consent server base URL |
297
- | `ledgerBaseUrl` | `string` | | - | Go ledger base URL. When set, user consents, group purposes, receipts, and manage-consent route through the ledger. Omit to use the legacy `/dsar/privacy-center/*` routes. See the note below on the DSAR form picker. |
298
- | `slug` | `string` | ✓ | - | Workspace slug |
331
+ | `baseUrl` | `string` | ✓ | - | API host, e.g. `https://api.redacto.io` (recommended). The consent server URL (`…/consent`) still works but is deprecated. See [`baseUrl` and service routing](#baseurl-and-service-routing) |
332
+ | `ledgerBaseUrl` | `string` | | - | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`), as in earlier releases: it then routes user consents, group purposes, receipts and manage-consent through the ledger; without it, the legacy `/dsar/privacy-center/*` routes are used. Ignored when `baseUrl` is the API host. See the note below on the DSAR form picker. |
333
+ | `slug` | `string` | | - | **Deprecated** and ignored, to be removed in a future major release; passing it logs a one-time console warning. The organisation, and its branding, come from the access token, or from `organisationUuid` and `workspaceUuid` |
299
334
  | `accessToken` | `string` | ✓ | - | JWT access token |
300
335
  | `refreshToken` | `string` | ✓ | - | JWT refresh token |
301
336
  | `onError` | `(error: Error) => boolean` | ✓ | - | Error handler; return `true` if handled |
302
337
  | `theme` | `"light" \| "dark"` | | `"light"` | Theme variant |
303
338
  | `initialPage` | `"consent-manager" \| "form" \| "activity" \| "receipts" \| "case-details"` | | `"consent-manager"` | Page to render first |
304
339
  | `onBack` | `"back" \| "signout"` | | - | Navbar back / signout control behaviour |
340
+ | `organisationUuid` / `workspaceUuid` | `string` | | - | Without a token, address the built-in sign-in and the published settings |
341
+ | `settings` | `PrivacyCenterSettings` | | - | Overrides for the settings published in the console, merged field by field per group |
342
+ | `builtInSignIn` | `boolean` | | `false` | With no token, show the built-in OTP sign-in instead of the loader |
343
+ | `onReady` | `(event: { signedIn: boolean }) => void` | | - | Called once, when the first screen (sign-in or the Privacy Center) is on show |
344
+ | `onConsentChange` | `(change: PrivacyCenterConsentChange) => void` | | - | After a consent was withdrawn, re-granted or renewed |
345
+ | `onRequestSubmit` | `(request: PrivacyCenterRequestSubmission) => void` | | - | After a data request (case) was created |
346
+ | `onSignOut` | `(event: { reason: "user" \| "timeout" \| "session-expired" }) => void` | | - | When the visitor is signed out, with the reason |
347
+
348
+ #### Published settings
349
+
350
+ `RedactoPrivacyCenter` fetches the settings published in the Redacto console (Privacy Center › Settings) once it knows the organisation (from the props, the token's claims or an earlier session), and again in each language the visitor switches to. It never waits for them: it renders with the defaults, which reproduce the Privacy Center as it was, and keeps them if the request fails. Responses are cached per organisation and language for five minutes.
351
+
352
+ What the settings drive:
353
+
354
+ - **Branding**: display name, logo and logo height, primary and accent colours, font (resolved through the shared font catalogue, as the notice does) and corner radius. Each inherits workspace branding until the console says otherwise.
355
+ - **Menu**: section order, labels and on/off. A switched-off section leaves the home screen and menu and can't be navigated to; switching off notifications removes the bell.
356
+ - **Welcome message** on the home screen, with `{brand}`, `{legal}`, `{year}`, `{name}` and `{contact}` filled in.
357
+ - **Requests**: the request types offered (order, labels, on/off, narrowed to what the server allows), the grievance categories, and whether an erasure also revokes consent (asked of the data principal unless the organisation decided).
358
+ - **Withdraw warning**: the organisation's default; a purpose's own warning still wins.
359
+ - **Languages**: the languages the switcher offers, and the one a visitor lands on.
360
+ - **Inactivity sign-out**: with `portal.session_timeout_minutes`, the visitor is signed out after that long without a touch, scroll or keystroke (including on return from the background past the deadline) and lands on the session-expired screen.
361
+ - **Footer**: links, privacy policy, grievance officer and Data Protection Board lines, copyright, and "Powered by".
362
+ - **Sign-in screen** (see below).
363
+
364
+ Urdu, Sindhi and Kashmiri lay out right to left once settings are published or overridden, using `direction` and `writingDirection` on the Privacy Center's own views: the app's `I18nManager` is never touched.
365
+
366
+ The `settings` prop takes overrides merged over each published group, so one value can change while the rest of what the console published stays; `settings.theme` is kept as an alias for `branding.style`. React Native has one Privacy Center look, so the style is read but changes nothing; custom CSS, the SEO page title and embed origins are web-only and not applied.
367
+
368
+ ```tsx
369
+ <RedactoPrivacyCenter
370
+ baseUrl={BASE_URL}
371
+ accessToken={accessToken}
372
+ refreshToken={refreshToken}
373
+ onError={() => false}
374
+ settings={{ portal: { welcome_enabled: true, welcome_title: "Hello from {brand}" } }}
375
+ />
376
+ ```
377
+
378
+ #### Built-in sign-in
379
+
380
+ With `builtInSignIn` and no token, the Privacy Center shows its own sign-in: the visitor gives a mobile number or email, receives a one-time code (resend after 50 seconds) and is signed straight in with the tokens it issues. Layout (`card`, `split` or `minimal`), background, copy, channels, default country code, notice line and support link come from the published `sign_in` settings; copy left at its default is shown in the visitor's language. The organisation and workspace come from `organisationUuid` / `workspaceUuid` or an earlier sign-in's session, never from `slug`; without them the sign-in says the organisation could not be loaded. When the session expires and the host gave no `onBack`, the session-expired screen offers signing in again. Without `builtInSignIn` the Privacy Center keeps its loader until the host passes a token, as before.
381
+
382
+ #### Lifecycle callbacks
383
+
384
+ `onReady`, `onConsentChange`, `onRequestSubmit` and `onSignOut` always call the latest handler passed, and a handler that throws is logged, never rethrown.
385
+
386
+ | Callback | Payload |
387
+ | --- | --- |
388
+ | `onReady` | `{ signedIn }` — false when the sign-in screen is what shows |
389
+ | `onConsentChange` | `{ action: "revoke" \| "regrant" \| "renew", purposeUuid, productUuid, dataElementUuids, nominatorUuid }` |
390
+ | `onRequestSubmit` | `{ caseId, caseUuid, requestType, productUuid }` |
391
+ | `onSignOut` | `{ reason: "user" \| "timeout" \| "session-expired" }` |
305
392
 
306
393
  ### Composable Components
307
394
 
@@ -319,7 +406,7 @@ If you want to compose your own shell, import the screen-level building blocks i
319
406
  | `BottomTabBar` | Bottom tab navigation |
320
407
  | `Footer` | Footer block |
321
408
 
322
- These rely on the context providers (`AuthProvider`, `BaseUrlProvider`, `NavigationProvider`, `ThemeProvider`, and `QueryClientProvider` for React Query hooks) — wrap them yourself if you compose the pages directly.
409
+ These rely on the context providers (`AuthProvider`, `BaseUrlProvider`, `NavigationProvider`, `ThemeProvider`, and `QueryClientProvider` for React Query hooks) — wrap them yourself if you compose the pages directly. Pass `ThemeProvider` the `organisationUuid` to brand a composition: it ignores `slug`.
323
410
 
324
411
  ### Receipt downloads
325
412
 
@@ -339,11 +426,11 @@ For building custom screens that match Privacy Center styling, the package expor
339
426
 
340
427
  For consumers who want fully custom UI, the headless core exposes the API client, orchestration helpers, hooks, and auth utilities:
341
428
 
342
- - **API client**: `createConsentClient`
429
+ - **API client**: `createConsentClient(baseUrl, ledgerBaseUrl?)`, which takes `baseUrl` the same way as the components
343
430
  - **Orchestration helpers** (handle auth retry / token refresh internally): `getFormData`, `createCaseRequest`, `fetchProducts`, `fetchUserConsents`, `manageConsent`, `fetchActivities`, `fetchCaseHistory`, `getCaseMessages`, `sendCaseMessage`, `uploadCaseDocument`, `submitDocumentForRequest`, `createOtp`, `verifyOtp`, `exchangeToken`
344
431
  - **Auth utilities**: `decodeJwtToken`, `isJwtExpired`, `ensureValidToken`, `signOutPrivacyCenter`, `compareEmails`, `getActivityTypeStatus`, `formatActivityType`
345
432
  - **React Query hooks**: `useCaseHistory`, `useCaseMessages`
346
- - **Context providers**: `AuthProvider` + `useAuth`, `BaseUrlProvider` + `useBaseUrl`, `NavigationProvider` + `useInternalNavigation`, `ThemeProvider` + `useTheme`
433
+ - **Context providers**: `AuthProvider` + `useAuth`, `BaseUrlProvider` + `useBaseUrl`, `NavigationProvider` + `useInternalNavigation`, `ThemeProvider` + `useTheme`. Given the API host, `useBaseUrl()` returns the derived consent server URL as `baseUrl` and the derived ledger URL as `ledgerBaseUrl`
347
434
  - **Types**: `Workspace`, `OrganizationDetail`, `DataElement`, `Purpose`, `UserConsent`, `Activity`, `CaseHistoryDetail`, `CaseMessage`, `MessageDocument`, `RequestTypeEnum`, `GrievanceTypeEnum`, `ConsentStatusEnum`, and more (see `privacy-center.ts` for the full export list)
348
435
 
349
436
  ## Configuration and Env Best Practices
@@ -351,19 +438,52 @@ For consumers who want fully custom UI, the headless core exposes the API client
351
438
  - The SDK is runtime-configured via props and does not read from `.env` directly.
352
439
  - Keep API keys and server secrets on your backend only.
353
440
  - In Expo apps, values read from `EXPO_PUBLIC_*` are bundled into the app and should only contain non-sensitive config like UUIDs and URLs.
354
- - If you do not pass `baseUrl`, the SDK defaults to `https://api.redacto.io/consent` for both notice and Privacy Center surfaces.
355
- - `ledgerBaseUrl` is optional and independent of `baseUrl` — pass both when routing consent through the Go ledger. Omitting it keeps the legacy consent-server behavior.
441
+ - Pass the API host of your environment as `baseUrl`, e.g. `https://api.redacto.io`. If the notice components get no `baseUrl`, they fall back to the built-in production default, `https://api.redacto.io/consent`, and log a one-time warning.
442
+
443
+ ## `baseUrl` and service routing
444
+
445
+ Pass the Redacto **API host** as `baseUrl`, for example `https://api.redacto.io`. The SDK derives each service from it — the consent server at `{baseUrl}/consent`, the consent ledger at `{baseUrl}/consent-ledger` and the user service at `{baseUrl}/user/api/0` — and sends each request to the service that serves it:
446
+
447
+ - **Notice components** — consent writes (`submit-consent`) and the already-consented check (`check-consent`) go to the ledger. `RedactoNoticeConsent` reads the notice from the ledger in a JWT session, and from the consent server in the magic-link (`applicationId`) flow and in sandbox; its audio comes from the ledger, sandbox included. `RedactoNoticeConsentInline` reads the notice from the ledger when it has an `accessToken`, and from the consent server without one and in the magic-link flow. `RedactoNoticeAssisted`'s notice, OTP and audio, and guardian verification, go to the consent server.
448
+ - **Privacy Center** — user consents, group purposes, receipts (including the `/download` PDF path) and manage-consent (sent as a `partial: true` write) go to the ledger. DSAR form, cases, activities, OTP, token exchange, notifications and uploads go to the consent server; org branding comes from the user service.
449
+
450
+ A host behind a path prefix works the same way: `https://gateway.example.com/redacto` gives `https://gateway.example.com/redacto/consent` and so on. A trailing `/` is ignored, and a `baseUrl` ending in `/consent-ledger` resolves to its host.
356
451
 
357
- ## Ledger mode
452
+ If the already-consented check fails, the component carries on and submits as usual.
358
453
 
359
- Passing `ledgerBaseUrl` routes consent-bearing reads and writes to the Go ledger:
454
+ > **Known limitation.** Unlike the React SDK, the DSAR form's product picker is **not** overlaid from ledger-served consents. Against a workspace cut over to the ledger — where the legacy `form/data` response carries no product groups — the picker renders empty. If your DSAR form depends on the product picker, use the deprecated consent server URL as `baseUrl` without `ledgerBaseUrl` until this is addressed.
360
455
 
361
- - **Notice components** — `submit-consent` goes to the ledger, and the already-consented check uses the ledger's `check-consent` endpoint instead of the legacy HTTP 409 signal. Notice content, tokens, and TTS always stay on `baseUrl`.
362
- - **Privacy Center** — user consents, group purposes, receipts (including the `/download` PDF path), and manage-consent (sent as a `partial: true` write) route to the ledger. DSAR form, cases, activities, OTP, and notifications stay on `baseUrl`.
456
+ ### Deprecated: the consent server URL and `ledgerBaseUrl`
457
+
458
+ Earlier releases took the consent server URL as `baseUrl` (`https://api.redacto.io/consent`) and the Go ledger as a separate `ledgerBaseUrl`. Both keep working exactly as before:
459
+
460
+ - A `baseUrl` ending in `/consent` is the consent server URL, and `ledgerBaseUrl`, when passed, the ledger. Requests route as in earlier releases: with `ledgerBaseUrl`, as above, except that the inline notice read and sandbox audio stay on the consent server; without it, everything goes to the consent server, on its legacy routes (`/dsar/privacy-center/*`, `manage-consent?status=`, receipts `/pdf`, and the HTTP 409 already-consented signal).
461
+ - With the API host as `baseUrl`, `ledgerBaseUrl` is ignored: the ledger is always `{baseUrl}/consent-ledger`, and the SDK logs a one-time warning asking you to remove it.
462
+
463
+ The SDK logs a one-time console warning when `baseUrl` is left out, when it ends in `/consent`, and when `ledgerBaseUrl` is passed. These forms will be removed in a future major release: pass the API host as `baseUrl` and drop `ledgerBaseUrl`.
464
+
465
+ ### Your backend
466
+
467
+ Your token backend calls the consent server itself, so it still needs the consent server URL, `{API host}/consent` (see [Backend Server Setup](#backend-server-setup)). If your app passes one value to both, give the SDK the API host and send `${BASE_URL}/consent` to the backend.
468
+
469
+ ### Local development
470
+
471
+ Put the services behind one local gateway, the way production does, and pass its address as `baseUrl`. For example, with [Caddy](https://caddyserver.com), the consent server on port 8000 and the ledger on port 8090:
472
+
473
+ ```caddyfile
474
+ :8080 {
475
+ handle /consent-ledger/* {
476
+ reverse_proxy localhost:8090
477
+ }
478
+ handle /consent/* {
479
+ reverse_proxy localhost:8000
480
+ }
481
+ }
482
+ ```
363
483
 
364
- Every ledger read degrades to the legacy path on failure, so a ledger outage does not break the consent flow.
484
+ Then pass `baseUrl="http://localhost:8080"`. Both services serve their own path prefix (`/consent/…`, `/consent-ledger/…`), so the gateway forwards paths unchanged. Any reverse proxy works the same way (nginx, Traefik, your docker-compose gateway).
365
485
 
366
- > **Known limitation.** Unlike the React SDK, the DSAR form's product picker is **not** overlaid from ledger-served consents. Against a workspace cut over to the ledger — where the legacy `form/data` response carries no product groups — the picker renders empty. If your DSAR form depends on the product picker, stay on the legacy path until this is addressed.
486
+ On the Android emulator the host machine is `http://10.0.2.2:8080`; the iOS simulator reaches it at `http://localhost:8080`. A physical device needs the machine's LAN IP.
367
487
 
368
488
  ## License
369
489