@redacto.io/consent-sdk-react 10.0.0-beta.0 → 10.0.0-beta.11
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/.turbo/turbo-build.log +22 -25
- package/CHANGELOG.md +123 -0
- package/README.md +78 -0
- package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
- package/dist/index.d.mts +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +345 -237
- package/dist/index.mjs +344 -238
- package/dist/privacy-center.d.mts +113 -37
- package/dist/privacy-center.d.ts +113 -37
- package/dist/privacy-center.js +2317 -1910
- package/dist/privacy-center.mjs +2245 -1835
- package/package.json +1 -1
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +87 -0
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +27 -0
- package/src/RedactoNoticeAssisted/api/index.ts +7 -1
- package/src/RedactoNoticeAssisted/api/types.ts +2 -0
- package/src/RedactoNoticeAssisted/i18n.ts +0 -55
- package/src/RedactoNoticeAssisted/types.ts +2 -0
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +42 -8
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +75 -33
- package/src/RedactoNoticeConsent/api/index.ts +90 -177
- package/src/RedactoNoticeConsent/api/sandbox.ts +150 -0
- package/src/RedactoNoticeConsent/api/types.ts +60 -44
- package/src/RedactoNoticeConsent/language-codes.ts +0 -2
- package/src/RedactoNoticeConsent/types.ts +29 -3
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +86 -5
- package/src/RedactoNoticeConsentInline/api/index.ts +14 -5
- package/src/RedactoNoticeConsentInline/api/notice-read-routing.test.ts +72 -0
- package/src/RedactoNoticeConsentInline/api/types.ts +4 -1
- package/src/RedactoNoticeConsentInline/types.ts +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +8 -5
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +38 -3
- package/src/RedactoPrivacyCenter/api/actions.ts +13 -5
- package/src/RedactoPrivacyCenter/api/client.ts +216 -39
- package/src/RedactoPrivacyCenter/api/fetcher.ts +24 -9
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +28 -0
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +107 -76
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +2 -1
- package/src/RedactoPrivacyCenter/components/Form.tsx +53 -24
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/SelectUser.tsx +95 -0
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +195 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +11 -0
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +42 -6
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +131 -39
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +1 -1
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +15 -0
- package/src/RedactoPrivacyCenter/context/types.ts +39 -1
- package/src/RedactoPrivacyCenter/lib/api.ts +3 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +12 -6
- package/src/RedactoPrivacyCenter/lib/locales.ts +0 -3
- package/src/RedactoPrivacyCenter/lib/sandbox.ts +104 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +78 -2
- package/src/RedactoPrivacyCenter/lib/utils.ts +15 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +8 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +7 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +7 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +112 -2
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +17 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +83 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +33 -9
- package/src/shared/notice-consent-status.ts +190 -0
- package/src/shared/sandbox.ts +6 -0
- package/tests/Form.test.tsx +190 -7
- package/tests/RevokeWarningMessage.test.tsx +151 -0
- package/tests/consentClient.test.ts +217 -0
- package/tests/manageConsentServer.test.ts +38 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +0 -334
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./api";
|
|
19
19
|
import type { ConsentContent, TTSAudioUrlsResponse } from "./api/types";
|
|
20
20
|
import { ApiError } from "../shared/api-errors";
|
|
21
|
+
import { fetchNoticeConsentStatusForScope } from "../shared/notice-consent-status";
|
|
21
22
|
import { makeStaticT } from "./i18n";
|
|
22
23
|
|
|
23
24
|
vi.mock("./api", () => ({
|
|
@@ -28,12 +29,26 @@ vi.mock("./api", () => ({
|
|
|
28
29
|
submitAssistedConsent: vi.fn(),
|
|
29
30
|
}));
|
|
30
31
|
|
|
32
|
+
// Mock only the ledger check-consent network read; keep the real
|
|
33
|
+
// isNoticeFullyConsented predicate so the already-consented decision is exercised.
|
|
34
|
+
vi.mock("../shared/notice-consent-status", async (importActual) => {
|
|
35
|
+
const actual =
|
|
36
|
+
await importActual<typeof import("../shared/notice-consent-status")>();
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
fetchNoticeConsentStatusForScope: vi.fn(),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const LEDGER_BASE_URL = "https://ledger.example.test/consent-ledger";
|
|
44
|
+
|
|
31
45
|
const otpLength = ASSISTED_CONFIG.otpLength;
|
|
32
46
|
const mockedFetch = vi.mocked(fetchAssistedNotice);
|
|
33
47
|
const mockedFetchTTS = vi.mocked(fetchAssistedTTSAudioUrls);
|
|
34
48
|
const mockedCreateOtp = vi.mocked(createOtp);
|
|
35
49
|
const mockedVerifyOtp = vi.mocked(verifyOtp);
|
|
36
50
|
const mockedSubmitConsent = vi.mocked(submitAssistedConsent);
|
|
51
|
+
const mockedFetchScope = vi.mocked(fetchNoticeConsentStatusForScope);
|
|
37
52
|
|
|
38
53
|
const OTP_DETAIL = {
|
|
39
54
|
uuid: "otp-1",
|
|
@@ -226,6 +241,9 @@ describe("RedactoNoticeAssisted", () => {
|
|
|
226
241
|
mockedCreateOtp.mockResolvedValue(OTP_DETAIL);
|
|
227
242
|
mockedVerifyOtp.mockResolvedValue(VERIFY_DETAIL);
|
|
228
243
|
mockedSubmitConsent.mockResolvedValue(null);
|
|
244
|
+
// Default: ledger reports no prior full consent, so flows proceed to submit.
|
|
245
|
+
mockedFetchScope.mockReset();
|
|
246
|
+
mockedFetchScope.mockResolvedValue(null);
|
|
229
247
|
});
|
|
230
248
|
|
|
231
249
|
it("fetches with the public-notice props (no auth)", async () => {
|
|
@@ -604,6 +622,75 @@ describe("RedactoNoticeAssisted", () => {
|
|
|
604
622
|
).not.toBeInTheDocument();
|
|
605
623
|
});
|
|
606
624
|
|
|
625
|
+
// ── Go ledger (ledgerBaseUrl) ─────────────────────────────────────────────
|
|
626
|
+
// Drives the full notice → OTP → Confirm flow with a notice that carries a
|
|
627
|
+
// notice_uuid (so the check-consent read runs), and returns the onComplete spy.
|
|
628
|
+
const runConfirmFlow = async (extraProps: Record<string, unknown>) => {
|
|
629
|
+
mockedFetch.mockResolvedValue(NOTICE_WITH_AUDIO);
|
|
630
|
+
const onComplete = vi.fn();
|
|
631
|
+
render(
|
|
632
|
+
<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} {...extraProps} />,
|
|
633
|
+
);
|
|
634
|
+
await screen.findByRole("heading", { name: "EA Sports" });
|
|
635
|
+
selectRequired();
|
|
636
|
+
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
637
|
+
fireEvent.change(screen.getByLabelText("Mobile number"), {
|
|
638
|
+
target: { value: "9876543210" },
|
|
639
|
+
});
|
|
640
|
+
fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
|
|
641
|
+
await screen.findByText(/OTP sent to/);
|
|
642
|
+
typeOtp("1".repeat(otpLength));
|
|
643
|
+
fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
|
|
644
|
+
return onComplete;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
it("routes the write through ledgerBaseUrl and submits when not already consented", async () => {
|
|
648
|
+
const onComplete = await runConfirmFlow({ ledgerBaseUrl: LEDGER_BASE_URL });
|
|
649
|
+
|
|
650
|
+
await waitFor(() => expect(mockedSubmitConsent).toHaveBeenCalledTimes(1));
|
|
651
|
+
// The check-consent read runs with props org/workspace + the OTP token.
|
|
652
|
+
expect(mockedFetchScope).toHaveBeenCalledWith(
|
|
653
|
+
expect.objectContaining({
|
|
654
|
+
accessToken: "access-token-1",
|
|
655
|
+
ledgerBaseUrl: LEDGER_BASE_URL,
|
|
656
|
+
organisationUuid: "org-1",
|
|
657
|
+
workspaceUuid: "ws-1",
|
|
658
|
+
noticeUuid: "ntc-1",
|
|
659
|
+
}),
|
|
660
|
+
);
|
|
661
|
+
// The submit carries ledgerBaseUrl so the api layer routes it to the ledger.
|
|
662
|
+
expect(mockedSubmitConsent).toHaveBeenCalledWith(
|
|
663
|
+
expect.objectContaining({ ledgerBaseUrl: LEDGER_BASE_URL }),
|
|
664
|
+
);
|
|
665
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("short-circuits an already fully-consented principal via the ledger (no submit)", async () => {
|
|
669
|
+
mockedFetchScope.mockResolvedValue({
|
|
670
|
+
consented: true,
|
|
671
|
+
all_mandatory_active: true,
|
|
672
|
+
purposes: [],
|
|
673
|
+
});
|
|
674
|
+
const onComplete = await runConfirmFlow({ ledgerBaseUrl: LEDGER_BASE_URL });
|
|
675
|
+
|
|
676
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
677
|
+
expect(mockedSubmitConsent).not.toHaveBeenCalled();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("skips the ledger check-consent and stays on baseUrl when ledgerBaseUrl is absent", async () => {
|
|
681
|
+
const onComplete = await runConfirmFlow({});
|
|
682
|
+
|
|
683
|
+
await waitFor(() => expect(mockedSubmitConsent).toHaveBeenCalledTimes(1));
|
|
684
|
+
expect(mockedFetchScope).not.toHaveBeenCalled();
|
|
685
|
+
expect(mockedSubmitConsent).toHaveBeenCalledWith(
|
|
686
|
+
expect.objectContaining({
|
|
687
|
+
ledgerBaseUrl: undefined,
|
|
688
|
+
baseUrl: "https://api.example.test/consent",
|
|
689
|
+
}),
|
|
690
|
+
);
|
|
691
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
692
|
+
});
|
|
693
|
+
|
|
607
694
|
// The verify endpoint returns English copy with no error_code, so the SDK
|
|
608
695
|
// localizes by HTTP status + the `should_request_new` flag instead of echoing
|
|
609
696
|
// the server message.
|
|
@@ -23,6 +23,10 @@ import type {
|
|
|
23
23
|
TTSAudioUrlsResponse,
|
|
24
24
|
} from "./api/types";
|
|
25
25
|
import { ApiError } from "../shared/api-errors";
|
|
26
|
+
import {
|
|
27
|
+
fetchNoticeConsentStatusForScope,
|
|
28
|
+
isNoticeFullyConsented,
|
|
29
|
+
} from "../shared/notice-consent-status";
|
|
26
30
|
import {
|
|
27
31
|
toNativeLabel,
|
|
28
32
|
toBcp47Code,
|
|
@@ -251,6 +255,7 @@ export const RedactoNoticeAssisted = ({
|
|
|
251
255
|
workspaceUuid,
|
|
252
256
|
noticeUuid,
|
|
253
257
|
baseUrl,
|
|
258
|
+
ledgerBaseUrl,
|
|
254
259
|
onDecline,
|
|
255
260
|
onComplete,
|
|
256
261
|
}: RedactoNoticeAssistedProps) => {
|
|
@@ -1207,6 +1212,26 @@ export const RedactoNoticeAssisted = ({
|
|
|
1207
1212
|
}
|
|
1208
1213
|
const token = verifyDetail.access_token;
|
|
1209
1214
|
|
|
1215
|
+
// Post-cutover, consent records live in the Go ledger, so Python's 409
|
|
1216
|
+
// CONSENT_ALREADY_PROVIDED can no longer be relied on. When a ledger is
|
|
1217
|
+
// configured, ask it directly (org/workspace from props, OTP token as
|
|
1218
|
+
// bearer) whether this principal has already fully consented and, if so,
|
|
1219
|
+
// hand off without writing a duplicate consent event. Any failure returns
|
|
1220
|
+
// null and falls through to the normal submit (graceful degradation).
|
|
1221
|
+
if (ledgerBaseUrl) {
|
|
1222
|
+
const ledgerStatus = await fetchNoticeConsentStatusForScope({
|
|
1223
|
+
accessToken: token,
|
|
1224
|
+
ledgerBaseUrl,
|
|
1225
|
+
organisationUuid,
|
|
1226
|
+
workspaceUuid,
|
|
1227
|
+
noticeUuid: content.notice_uuid,
|
|
1228
|
+
});
|
|
1229
|
+
if (ledgerStatus && isNoticeFullyConsented(ledgerStatus)) {
|
|
1230
|
+
handleSuccess();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1210
1235
|
// Map current selection onto the submit payload shape (required → selected).
|
|
1211
1236
|
const submitPurposes = content.purposes.map((purpose) => ({
|
|
1212
1237
|
...purpose,
|
|
@@ -1224,6 +1249,7 @@ export const RedactoNoticeAssisted = ({
|
|
|
1224
1249
|
organisationUuid,
|
|
1225
1250
|
workspaceUuid,
|
|
1226
1251
|
baseUrl,
|
|
1252
|
+
ledgerBaseUrl,
|
|
1227
1253
|
noticeUuid: content.notice_uuid,
|
|
1228
1254
|
purposes: submitPurposes,
|
|
1229
1255
|
language: toBcp47Code(selectedLanguage),
|
|
@@ -1255,6 +1281,7 @@ export const RedactoNoticeAssisted = ({
|
|
|
1255
1281
|
otpUuid,
|
|
1256
1282
|
otpValue,
|
|
1257
1283
|
baseUrl,
|
|
1284
|
+
ledgerBaseUrl,
|
|
1258
1285
|
selectedPurposes,
|
|
1259
1286
|
selectedDataElements,
|
|
1260
1287
|
selectedLanguage,
|
|
@@ -160,6 +160,7 @@ export const submitAssistedConsent = async ({
|
|
|
160
160
|
organisationUuid,
|
|
161
161
|
workspaceUuid,
|
|
162
162
|
baseUrl,
|
|
163
|
+
ledgerBaseUrl,
|
|
163
164
|
noticeUuid,
|
|
164
165
|
purposes,
|
|
165
166
|
language,
|
|
@@ -196,7 +197,12 @@ export const submitAssistedConsent = async ({
|
|
|
196
197
|
language: language || undefined,
|
|
197
198
|
};
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
// Route the write to the Go ledger when configured, else the legacy consent
|
|
201
|
+
// server. publicScope already applies `|| BASE_URL`, so this yields the
|
|
202
|
+
// standard `ledgerBaseUrl || baseUrl || BASE_URL` fallback used by the other
|
|
203
|
+
// notice components. Only submit-consent routes to the ledger; notice/OTP/TTS
|
|
204
|
+
// stay on baseUrl.
|
|
205
|
+
const url = `${publicScope(ledgerBaseUrl || baseUrl, organisationUuid, workspaceUuid)}/submit-consent`;
|
|
200
206
|
|
|
201
207
|
const response = await fetch(url, {
|
|
202
208
|
method: "POST",
|
|
@@ -94,6 +94,8 @@ export type SubmitAssistedConsentParams = {
|
|
|
94
94
|
organisationUuid: string;
|
|
95
95
|
workspaceUuid: string;
|
|
96
96
|
baseUrl?: string;
|
|
97
|
+
/** Go ledger base URL — routes the write to the ledger; omit to use the legacy consent server. */
|
|
98
|
+
ledgerBaseUrl?: string;
|
|
97
99
|
/** `notice_uuid` from the fetched active_config. */
|
|
98
100
|
noticeUuid: string;
|
|
99
101
|
/** Purposes carrying their current `selected` state + data elements. */
|
|
@@ -1248,60 +1248,6 @@ const as: StaticStrings = {
|
|
|
1248
1248
|
close: "বন্ধ কৰক",
|
|
1249
1249
|
};
|
|
1250
1250
|
|
|
1251
|
-
const bho: StaticStrings = {
|
|
1252
|
-
useEmailInstead: "ईमेल इस्तेमाल करीं",
|
|
1253
|
-
useMobileInstead: "मोबाइल इस्तेमाल करीं",
|
|
1254
|
-
emailAddress: "ईमेल पता",
|
|
1255
|
-
otpHelperEmail: "हमनी एह ईमेल पर एक बेर इस्तेमाल होखे वाला पासवर्ड भेजब।",
|
|
1256
|
-
changeEmail: "ईमेल बदलीं",
|
|
1257
|
-
expandDetails: "{name} के विवरण फैलाईं",
|
|
1258
|
-
collapseDetails: "{name} के विवरण समेटीं",
|
|
1259
|
-
selectAllFor: "{name} खातिर सभ डेटा तत्व चुनीं",
|
|
1260
|
-
selectElement: "{name} चुनीं",
|
|
1261
|
-
selectElementRequired: "{name} चुनीं (जरूरी)",
|
|
1262
|
-
required: "जरूरी",
|
|
1263
|
-
logoAlt: "लोगो",
|
|
1264
|
-
selectLanguage: "भाषा चुनीं",
|
|
1265
|
-
selectLanguageCurrent: "भाषा चुनीं. मौजूदा भाषा: {lang}",
|
|
1266
|
-
opensInNewTab: "नया टैब में खुलेला",
|
|
1267
|
-
audioStart: "ऑडियो प्लेबैक शुरू करीं",
|
|
1268
|
-
audioPause: "ऑडियो प्लेबैक रोकीं",
|
|
1269
|
-
talkbackStart: "टॉकबैक शुरू करीं",
|
|
1270
|
-
talkbackPause: "टॉकबैक रोकीं",
|
|
1271
|
-
mobileNumber: "मोबाइल नंबर",
|
|
1272
|
-
otpHelper: "हमनी एह नंबर पर एक बेर इस्तेमाल होखे वाला पासवर्ड भेजब।",
|
|
1273
|
-
otpSentTo: "ओटीपी (OTP) भेजल गइल",
|
|
1274
|
-
changeMobile: "मोबाइल नंबर बदलीं",
|
|
1275
|
-
enterNDigitOtp: "{count}-अंक के ओटीपी (OTP) डालीं",
|
|
1276
|
-
otpDigit: "ओटीपी (OTP) अंक {index}",
|
|
1277
|
-
resendIn: "{seconds} सेकंड में ओटीपी (OTP) फेर से भेजीं",
|
|
1278
|
-
sending: "भेजल जा रहल बा…",
|
|
1279
|
-
resendOtp: "ओटीपी (OTP) फेर से भेजीं",
|
|
1280
|
-
getOtp: "ओटीपी (OTP) पाईं",
|
|
1281
|
-
verifying: "सत्यापित कइल जा रहल बा…",
|
|
1282
|
-
verifyOtp: "ओटीपी (OTP) सत्यापित करीं",
|
|
1283
|
-
sendOtp: "ओटीपी (OTP) भेजीं",
|
|
1284
|
-
confirmOtp: "ओटीपी (OTP) के पुष्टि करीं",
|
|
1285
|
-
verifyWithOtp: "ओटीपी (OTP) से सत्यापित करीं",
|
|
1286
|
-
editSelections: "चुनाव में बदलाव करीं",
|
|
1287
|
-
otpIncorrect:
|
|
1288
|
-
"रउरा डालल सत्यापन कोड गलत बा। कृपया जाँच करीं आ फेर से कोशिश करीं।",
|
|
1289
|
-
otpExpired: "रउरा सत्यापन कोड के मियाद खतम हो गइल बा। कृपया नया कोड मांगीं।",
|
|
1290
|
-
otpNotFound: "सत्यापन कोड ना मिलल। कृपया नया कोड मांगीं।",
|
|
1291
|
-
otpVerifyFailed: "ओटीपी (OTP) के सत्यापन ना हो सकल। कृपया फेर से कोशिश करीं।",
|
|
1292
|
-
otpSendFailed: "ओटीपी (OTP) ना भेजल जा सकल। कृपया फेर से कोशिश करीं।",
|
|
1293
|
-
consentConfirmed: "सहमति के पुष्टि भइल",
|
|
1294
|
-
recordedUnder: "ओटीपी (OTP) सत्यापित · {regulation} के तहत दर्ज भइल",
|
|
1295
|
-
recordedUnderAt:
|
|
1296
|
-
"ओटीपी (OTP) सत्यापित · {regulation} के तहत {time} बजे दर्ज भइल",
|
|
1297
|
-
consentReferenceId: "सहमति संदर्भ आईडी",
|
|
1298
|
-
proceed: "आगे बढ़ीं",
|
|
1299
|
-
loadingNotice: "नोटिस लोड हो रहल बा",
|
|
1300
|
-
loadFailedTitle: "सहमति नोटिस लोड ना हो सकल",
|
|
1301
|
-
tryAgain: "कृपया फेर से कोसिस करीं।",
|
|
1302
|
-
close: "बंद करीं",
|
|
1303
|
-
};
|
|
1304
|
-
|
|
1305
1251
|
const brx: StaticStrings = {
|
|
1306
1252
|
useEmailInstead: "इमेल बाहाय",
|
|
1307
1253
|
useMobileInstead: "मबाइल बाहाय",
|
|
@@ -1435,7 +1381,6 @@ export const STATIC_STRINGS: Record<string, StaticStrings> = {
|
|
|
1435
1381
|
te,
|
|
1436
1382
|
ur,
|
|
1437
1383
|
as,
|
|
1438
|
-
bho,
|
|
1439
1384
|
brx,
|
|
1440
1385
|
doi,
|
|
1441
1386
|
};
|
|
@@ -17,6 +17,8 @@ export type RedactoNoticeAssistedProps = {
|
|
|
17
17
|
noticeUuid: string;
|
|
18
18
|
/** API base URL (defaults to the production consent API). */
|
|
19
19
|
baseUrl?: string;
|
|
20
|
+
/** Go ledger base URL — routes the consent write to the ledger; omit to use the legacy consent server. */
|
|
21
|
+
ledgerBaseUrl?: string;
|
|
20
22
|
/** Called when the agent/customer declines (host should unmount the modal). */
|
|
21
23
|
onDecline?: () => void;
|
|
22
24
|
/** Called when the agent clicks "Proceed" on the confirmed screen. */
|
|
@@ -26,11 +26,6 @@ vi.mock("./api", () => ({
|
|
|
26
26
|
fetchTTSAudioUrls: vi.fn(),
|
|
27
27
|
initiateGuardianVerification: vi.fn(),
|
|
28
28
|
verifyGuardianStatus: vi.fn(),
|
|
29
|
-
// Defaults to resolving undefined, so the ledger consent-state overlay is
|
|
30
|
-
// skipped and the banner falls back to the Python-provided values the
|
|
31
|
-
// existing tests assert on.
|
|
32
|
-
fetchNoticeConsentStatus: vi.fn(),
|
|
33
|
-
buildLedgerConsentOverlay: vi.fn(),
|
|
34
29
|
}));
|
|
35
30
|
|
|
36
31
|
// Mock jwt-decode
|
|
@@ -529,7 +524,7 @@ describe("RedactoNoticeConsent", () => {
|
|
|
529
524
|
noticeId: "notice-123",
|
|
530
525
|
accessToken: "mock-token",
|
|
531
526
|
refreshToken: "mock-refresh-token",
|
|
532
|
-
baseUrl:
|
|
527
|
+
baseUrl: defaultProps.ledgerBaseUrl,
|
|
533
528
|
language: "en",
|
|
534
529
|
specific_uuid: undefined,
|
|
535
530
|
validate_against: "required",
|
|
@@ -551,7 +546,7 @@ describe("RedactoNoticeConsent", () => {
|
|
|
551
546
|
noticeId: "notice-123",
|
|
552
547
|
accessToken: "mock-token",
|
|
553
548
|
refreshToken: "mock-refresh-token",
|
|
554
|
-
baseUrl:
|
|
549
|
+
baseUrl: defaultProps.ledgerBaseUrl,
|
|
555
550
|
language: "en",
|
|
556
551
|
specific_uuid: undefined,
|
|
557
552
|
validate_against: "all",
|
|
@@ -564,6 +559,45 @@ describe("RedactoNoticeConsent", () => {
|
|
|
564
559
|
});
|
|
565
560
|
});
|
|
566
561
|
|
|
562
|
+
describe("ledger routing", () => {
|
|
563
|
+
it("routes the standard notice fetch to the ledger base url", async () => {
|
|
564
|
+
await act(async () => {
|
|
565
|
+
render(<RedactoNoticeConsent {...defaultProps} />);
|
|
566
|
+
});
|
|
567
|
+
await waitFor(
|
|
568
|
+
() => {
|
|
569
|
+
expect(api.fetchConsentContent).toHaveBeenCalledWith(
|
|
570
|
+
expect.objectContaining({ baseUrl: defaultProps.ledgerBaseUrl })
|
|
571
|
+
);
|
|
572
|
+
},
|
|
573
|
+
{ timeout: 3000 }
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("keeps the magic-link flow (applicationId) on the consent server", async () => {
|
|
578
|
+
await act(async () => {
|
|
579
|
+
render(
|
|
580
|
+
<RedactoNoticeConsent {...defaultProps} applicationId="app-xyz" />
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
await waitFor(
|
|
584
|
+
() => {
|
|
585
|
+
expect(api.fetchConsentContent).toHaveBeenCalledWith(
|
|
586
|
+
expect.objectContaining({
|
|
587
|
+
// defaultProps has no baseUrl, so the magic-link flow routes the
|
|
588
|
+
// fetch to baseUrl=undefined (the consent server / SDK default),
|
|
589
|
+
// i.e. NOT ledgerBaseUrl — same as the submitConsentEvent
|
|
590
|
+
// assertion above. The point is the ledger routing is bypassed.
|
|
591
|
+
baseUrl: undefined,
|
|
592
|
+
specific_uuid: "app-xyz",
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
},
|
|
596
|
+
{ timeout: 3000 }
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
567
601
|
describe("includeFullyConsentedData (magic link flow)", () => {
|
|
568
602
|
beforeEach(() => {
|
|
569
603
|
mockedApi.fetchTTSAudioUrls.mockResolvedValue({
|
|
@@ -604,7 +638,7 @@ describe("RedactoNoticeConsent", () => {
|
|
|
604
638
|
noticeId: "notice-123",
|
|
605
639
|
accessToken: "mock-token",
|
|
606
640
|
refreshToken: "mock-refresh-token",
|
|
607
|
-
baseUrl:
|
|
641
|
+
baseUrl: defaultProps.ledgerBaseUrl,
|
|
608
642
|
language: "en",
|
|
609
643
|
specific_uuid: undefined,
|
|
610
644
|
validate_against: "all",
|
|
@@ -11,10 +11,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
|
11
11
|
|
|
12
12
|
// API imports
|
|
13
13
|
import {
|
|
14
|
-
buildLedgerConsentOverlay,
|
|
15
14
|
clearApiCache,
|
|
16
15
|
fetchConsentContent,
|
|
17
|
-
fetchNoticeConsentStatus,
|
|
18
16
|
fetchTTSAudioUrls,
|
|
19
17
|
initiateGuardianVerification,
|
|
20
18
|
submitConsentEvent,
|
|
@@ -1297,6 +1295,12 @@ export const RedactoNoticeConsent = ({
|
|
|
1297
1295
|
noticeId,
|
|
1298
1296
|
accessToken,
|
|
1299
1297
|
refreshToken,
|
|
1298
|
+
token,
|
|
1299
|
+
email,
|
|
1300
|
+
mobile,
|
|
1301
|
+
ucic,
|
|
1302
|
+
organisationUuid,
|
|
1303
|
+
workspaceUuid,
|
|
1300
1304
|
baseUrl,
|
|
1301
1305
|
ledgerBaseUrl,
|
|
1302
1306
|
language = "en",
|
|
@@ -1312,6 +1316,25 @@ export const RedactoNoticeConsent = ({
|
|
|
1312
1316
|
includeFullyConsentedData = false,
|
|
1313
1317
|
reviewModeButtonText = "Continue",
|
|
1314
1318
|
}: Props) => {
|
|
1319
|
+
// =============================================================================
|
|
1320
|
+
// SANDBOX MODE
|
|
1321
|
+
// =============================================================================
|
|
1322
|
+
|
|
1323
|
+
// Sandbox PoC: when `token` is set, every API call bypasses the JWT and sends
|
|
1324
|
+
// the single X-Consent-Token header, carrying the acting identity (subject) in
|
|
1325
|
+
// each request's own payload. Org/workspace/subject are taken straight from
|
|
1326
|
+
// props. Passed through to the API layer alongside `accessToken`.
|
|
1327
|
+
const sandboxParams = useMemo(
|
|
1328
|
+
() => ({
|
|
1329
|
+
token,
|
|
1330
|
+
sandboxUcic: ucic,
|
|
1331
|
+
sandboxSubject: email || mobile,
|
|
1332
|
+
organisationUuid,
|
|
1333
|
+
workspaceUuid,
|
|
1334
|
+
}),
|
|
1335
|
+
[token, ucic, email, mobile, organisationUuid, workspaceUuid]
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1315
1338
|
// =============================================================================
|
|
1316
1339
|
// PROP VALIDATION
|
|
1317
1340
|
// =============================================================================
|
|
@@ -1321,11 +1344,31 @@ export const RedactoNoticeConsent = ({
|
|
|
1321
1344
|
if (!noticeId?.trim()) {
|
|
1322
1345
|
return "RedactoNoticeConsent: 'noticeId' prop is required and cannot be empty";
|
|
1323
1346
|
}
|
|
1347
|
+
// Sandbox mode: require a static token, org/workspace, and a subject instead
|
|
1348
|
+
// of a JWT access token.
|
|
1349
|
+
if (token?.trim()) {
|
|
1350
|
+
if (!organisationUuid?.trim() || !workspaceUuid?.trim()) {
|
|
1351
|
+
return "RedactoNoticeConsent: sandbox mode requires 'organisationUuid' and 'workspaceUuid'";
|
|
1352
|
+
}
|
|
1353
|
+
if (!email?.trim() && !mobile?.trim() && !ucic?.trim()) {
|
|
1354
|
+
return "RedactoNoticeConsent: sandbox mode requires 'email', 'mobile', or 'ucic'";
|
|
1355
|
+
}
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1324
1358
|
if (!accessToken?.trim()) {
|
|
1325
1359
|
return "RedactoNoticeConsent: 'accessToken' prop is required and cannot be empty";
|
|
1326
1360
|
}
|
|
1327
1361
|
return null;
|
|
1328
|
-
}, [
|
|
1362
|
+
}, [
|
|
1363
|
+
noticeId,
|
|
1364
|
+
accessToken,
|
|
1365
|
+
token,
|
|
1366
|
+
organisationUuid,
|
|
1367
|
+
workspaceUuid,
|
|
1368
|
+
email,
|
|
1369
|
+
mobile,
|
|
1370
|
+
ucic,
|
|
1371
|
+
]);
|
|
1329
1372
|
|
|
1330
1373
|
// Show error UI if props are invalid
|
|
1331
1374
|
if (propValidationError) {
|
|
@@ -1812,16 +1855,23 @@ export const RedactoNoticeConsent = ({
|
|
|
1812
1855
|
setShowAgeVerification(false);
|
|
1813
1856
|
setShowGuardianForm(false);
|
|
1814
1857
|
try {
|
|
1858
|
+
// Standard flow reads the notice from the Go ledger. The magic-link flow
|
|
1859
|
+
// (applicationId/specific_uuid) and sandbox sessions stay on the consent
|
|
1860
|
+
// server: the ledger notice read supports neither (no specific_uuid, no
|
|
1861
|
+
// X-Consent-Token auth).
|
|
1862
|
+
const noticeBaseUrl =
|
|
1863
|
+
ledgerBaseUrl && !applicationId && !token ? ledgerBaseUrl : baseUrl;
|
|
1815
1864
|
const consentContentData = await fetchConsentContent({
|
|
1816
1865
|
noticeId,
|
|
1817
1866
|
accessToken,
|
|
1818
1867
|
refreshToken,
|
|
1819
|
-
baseUrl,
|
|
1868
|
+
baseUrl: noticeBaseUrl,
|
|
1820
1869
|
language,
|
|
1821
1870
|
specific_uuid: applicationId,
|
|
1822
1871
|
validate_against: validateAgainst,
|
|
1823
1872
|
include_fully_consented_data: includeFullyConsentedData,
|
|
1824
1873
|
signal: abortControllerRef.current?.signal,
|
|
1874
|
+
...sandboxParams,
|
|
1825
1875
|
});
|
|
1826
1876
|
const activeConfig = consentContentData.detail.active_config;
|
|
1827
1877
|
setContent(activeConfig);
|
|
@@ -1842,30 +1892,15 @@ export const RedactoNoticeConsent = ({
|
|
|
1842
1892
|
setCollapsedPurposes(initialCollapsedState);
|
|
1843
1893
|
|
|
1844
1894
|
// Initialize selected states - handle reconsent pre-population.
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
1849
|
-
//
|
|
1850
|
-
//
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
const ledgerStatus = await fetchNoticeConsentStatus({
|
|
1855
|
-
accessToken,
|
|
1856
|
-
ledgerBaseUrl,
|
|
1857
|
-
noticeUuid: noticeId,
|
|
1858
|
-
signal: abortControllerRef.current?.signal,
|
|
1859
|
-
});
|
|
1860
|
-
if (ledgerStatus) {
|
|
1861
|
-
const overlay = buildLedgerConsentOverlay(
|
|
1862
|
-
ledgerStatus,
|
|
1863
|
-
activeConfig.purposes
|
|
1864
|
-
);
|
|
1865
|
-
isReconsentRequired = overlay?.reconsent_required ?? false;
|
|
1866
|
-
purposeSelections = overlay?.purpose_selections;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1895
|
+
// Both notice reads return the per-principal reconsent overlay inline, so
|
|
1896
|
+
// we just consume it — same as the Python flow always did. The ledger
|
|
1897
|
+
// computes it from the very same consent-status read the standalone
|
|
1898
|
+
// check-consent route uses, so a second round-trip only duplicated that
|
|
1899
|
+
// work and produced a *weaker* overlay: it had no notice-version
|
|
1900
|
+
// awareness (a republished notice never re-prompted) and ignored
|
|
1901
|
+
// validate_against, both of which the inline overlay handles server-side.
|
|
1902
|
+
const isReconsentRequired = consentContentData.detail.reconsent_required;
|
|
1903
|
+
const purposeSelections = consentContentData.detail.purpose_selections;
|
|
1869
1904
|
|
|
1870
1905
|
// Categorize purposes for reconsent UI
|
|
1871
1906
|
const newCategorizedPurposes = isReconsentRequired
|
|
@@ -2033,8 +2068,10 @@ export const RedactoNoticeConsent = ({
|
|
|
2033
2068
|
noticeId,
|
|
2034
2069
|
accessToken,
|
|
2035
2070
|
refreshToken,
|
|
2071
|
+
sandboxParams,
|
|
2036
2072
|
language,
|
|
2037
2073
|
applicationId,
|
|
2074
|
+
ledgerBaseUrl,
|
|
2038
2075
|
validateAgainst,
|
|
2039
2076
|
includeFullyConsentedData,
|
|
2040
2077
|
// Removed onError and isRefreshingToken from dependencies to prevent duplicate API calls
|
|
@@ -2145,6 +2182,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2145
2182
|
if (content) {
|
|
2146
2183
|
await submitConsentEvent({
|
|
2147
2184
|
accessToken,
|
|
2185
|
+
baseUrl,
|
|
2148
2186
|
ledgerBaseUrl,
|
|
2149
2187
|
noticeUuid: content.notice_uuid,
|
|
2150
2188
|
purposes: content.purposes.map((purpose) => {
|
|
@@ -2193,6 +2231,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2193
2231
|
guardianVerificationReference: verificationReference || undefined,
|
|
2194
2232
|
selfDeclaredAdult: selfDeclaredAdult || undefined,
|
|
2195
2233
|
signal: submitController.signal,
|
|
2234
|
+
...sandboxParams,
|
|
2196
2235
|
});
|
|
2197
2236
|
}
|
|
2198
2237
|
onAccept?.();
|
|
@@ -2436,7 +2475,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2436
2475
|
} finally {
|
|
2437
2476
|
setIsSubmittingGuardian(false);
|
|
2438
2477
|
}
|
|
2439
|
-
}, [accessToken, baseUrl, guardianFormData, digilockerMode, digilockerCallbackUrl, selectedPurposes, selectedDataElements, noticeId, onError]);
|
|
2478
|
+
}, [accessToken, sandboxParams, baseUrl, guardianFormData, digilockerMode, digilockerCallbackUrl, selectedPurposes, selectedDataElements, noticeId, onError]);
|
|
2440
2479
|
|
|
2441
2480
|
// Age verification handlers
|
|
2442
2481
|
const handleAgeVerificationYes = useCallback(() => {
|
|
@@ -2515,7 +2554,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2515
2554
|
setIsPollingStatus(false);
|
|
2516
2555
|
isPollingStatusRef.current = false;
|
|
2517
2556
|
setIsInitiatingVerification(false);
|
|
2518
|
-
}, [accessToken, baseUrl]);
|
|
2557
|
+
}, [accessToken, sandboxParams, baseUrl]);
|
|
2519
2558
|
|
|
2520
2559
|
const startPopupMonitoring = useCallback((popup: Window) => {
|
|
2521
2560
|
// Check every 500ms if popup is closed
|
|
@@ -2696,7 +2735,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2696
2735
|
// First poll after 3s
|
|
2697
2736
|
statusPollIntervalRef.current = window.setTimeout(poll, POLL_INTERVAL);
|
|
2698
2737
|
},
|
|
2699
|
-
[accessToken, baseUrl, getVerificationErrorMessage]
|
|
2738
|
+
[accessToken, sandboxParams, baseUrl, getVerificationErrorMessage]
|
|
2700
2739
|
);
|
|
2701
2740
|
|
|
2702
2741
|
|
|
@@ -3224,13 +3263,16 @@ export const RedactoNoticeConsent = ({
|
|
|
3224
3263
|
// Create abort controller for this request
|
|
3225
3264
|
const ttsAbortController = new AbortController();
|
|
3226
3265
|
|
|
3227
|
-
//
|
|
3266
|
+
// Prefer the Go ledger for audio; fetchTTSAudioUrls keeps sandbox sessions
|
|
3267
|
+
// on the consent server (the ledger has no X-Consent-Token support).
|
|
3228
3268
|
fetchTTSAudioUrls({
|
|
3229
3269
|
accessToken,
|
|
3230
3270
|
baseUrl,
|
|
3271
|
+
ledgerBaseUrl,
|
|
3231
3272
|
noticeUuid: content.notice_uuid,
|
|
3232
3273
|
language: languageCode,
|
|
3233
3274
|
signal: ttsAbortController.signal,
|
|
3275
|
+
...sandboxParams,
|
|
3234
3276
|
})
|
|
3235
3277
|
.then((response) => {
|
|
3236
3278
|
// Store the response and mark TTS as available
|
|
@@ -3249,7 +3291,7 @@ export const RedactoNoticeConsent = ({
|
|
|
3249
3291
|
return () => {
|
|
3250
3292
|
ttsAbortController.abort();
|
|
3251
3293
|
};
|
|
3252
|
-
}, [content, selectedLanguage, accessToken, baseUrl, getLanguageCode]);
|
|
3294
|
+
}, [content, selectedLanguage, accessToken, sandboxParams, baseUrl, ledgerBaseUrl, getLanguageCode]);
|
|
3253
3295
|
|
|
3254
3296
|
const modalStyle = {
|
|
3255
3297
|
borderRadius: settings?.borderRadius || "8px",
|