@redacto.io/consent-sdk-react 3.4.0 → 3.6.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.
Files changed (155) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +176 -0
  3. package/dist/index.d.mts +51 -4
  4. package/dist/index.d.ts +51 -4
  5. package/dist/index.js +1912 -345
  6. package/dist/index.mjs +1911 -345
  7. package/dist/privacy-center.d.mts +566 -0
  8. package/dist/privacy-center.d.ts +566 -0
  9. package/dist/privacy-center.js +17174 -0
  10. package/dist/privacy-center.mjs +17136 -0
  11. package/package.json +47 -3
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +201 -118
  13. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1194 -93
  14. package/src/RedactoNoticeConsent/api/index.ts +337 -246
  15. package/src/RedactoNoticeConsent/api/types.ts +140 -41
  16. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +602 -0
  17. package/src/RedactoNoticeConsent/components/index.ts +7 -0
  18. package/src/RedactoNoticeConsent/injectStyles.ts +11 -0
  19. package/src/RedactoNoticeConsent/language-codes.ts +61 -0
  20. package/src/RedactoNoticeConsent/services/storage.ts +122 -0
  21. package/src/RedactoNoticeConsent/types.ts +2 -0
  22. package/src/RedactoNoticeConsentInline/api/index.ts +26 -64
  23. package/src/RedactoNoticeConsentInline/api/types.ts +0 -25
  24. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
  25. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
  26. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
  27. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +27 -0
  28. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +142 -0
  29. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +51 -0
  30. package/src/RedactoPrivacyCenter/api/actions.ts +444 -0
  31. package/src/RedactoPrivacyCenter/api/client.ts +413 -0
  32. package/src/RedactoPrivacyCenter/api/config.ts +33 -0
  33. package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
  34. package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
  35. package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
  36. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
  37. package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
  38. package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
  39. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
  40. package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
  41. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
  42. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
  43. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +468 -0
  44. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +433 -0
  45. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +726 -0
  46. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +239 -0
  47. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
  48. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
  49. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
  50. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
  51. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
  52. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +305 -0
  53. package/src/RedactoPrivacyCenter/components/Form.tsx +1455 -0
  54. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +176 -0
  55. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +330 -0
  56. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +329 -0
  57. package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
  58. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +302 -0
  59. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +218 -0
  60. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
  61. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +412 -0
  62. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
  63. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
  64. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
  65. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
  66. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
  67. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
  68. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +179 -0
  69. package/src/RedactoPrivacyCenter/components/constant.ts +20 -0
  70. package/src/RedactoPrivacyCenter/components/types.ts +68 -0
  71. package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
  72. package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
  73. package/src/RedactoPrivacyCenter/config/table.ts +25 -0
  74. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +123 -0
  75. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +44 -0
  76. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
  77. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +64 -0
  78. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
  79. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
  80. package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
  81. package/src/RedactoPrivacyCenter/lib/api.ts +468 -0
  82. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
  83. package/src/RedactoPrivacyCenter/lib/constants.ts +116 -0
  84. package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
  85. package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
  86. package/src/RedactoPrivacyCenter/lib/queries.ts +30 -0
  87. package/src/RedactoPrivacyCenter/lib/types.ts +346 -0
  88. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
  89. package/src/RedactoPrivacyCenter/lib/utils.ts +625 -0
  90. package/src/RedactoPrivacyCenter/locales/as/translation.json +188 -0
  91. package/src/RedactoPrivacyCenter/locales/bho/translation.json +188 -0
  92. package/src/RedactoPrivacyCenter/locales/bn/translation.json +188 -0
  93. package/src/RedactoPrivacyCenter/locales/brx/translation.json +188 -0
  94. package/src/RedactoPrivacyCenter/locales/doi/translation.json +188 -0
  95. package/src/RedactoPrivacyCenter/locales/en/translation.json +188 -0
  96. package/src/RedactoPrivacyCenter/locales/gom/translation.json +188 -0
  97. package/src/RedactoPrivacyCenter/locales/gu/translation.json +188 -0
  98. package/src/RedactoPrivacyCenter/locales/hi/translation.json +188 -0
  99. package/src/RedactoPrivacyCenter/locales/kn/translation.json +188 -0
  100. package/src/RedactoPrivacyCenter/locales/ks/translation.json +188 -0
  101. package/src/RedactoPrivacyCenter/locales/mai/translation.json +188 -0
  102. package/src/RedactoPrivacyCenter/locales/ml/translation.json +188 -0
  103. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +188 -0
  104. package/src/RedactoPrivacyCenter/locales/mr/translation.json +188 -0
  105. package/src/RedactoPrivacyCenter/locales/ne/translation.json +188 -0
  106. package/src/RedactoPrivacyCenter/locales/or/translation.json +188 -0
  107. package/src/RedactoPrivacyCenter/locales/pa/translation.json +188 -0
  108. package/src/RedactoPrivacyCenter/locales/sa/translation.json +188 -0
  109. package/src/RedactoPrivacyCenter/locales/sat/translation.json +188 -0
  110. package/src/RedactoPrivacyCenter/locales/sd/translation.json +188 -0
  111. package/src/RedactoPrivacyCenter/locales/ta/translation.json +188 -0
  112. package/src/RedactoPrivacyCenter/locales/te/translation.json +188 -0
  113. package/src/RedactoPrivacyCenter/locales/ur/translation.json +188 -0
  114. package/src/RedactoPrivacyCenter/styles/base.css +117 -0
  115. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1000 -0
  116. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +819 -0
  117. package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
  118. package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
  119. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +365 -0
  120. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +479 -0
  121. package/src/RedactoPrivacyCenter/table/index.ts +2 -0
  122. package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
  123. package/src/RedactoPrivacyCenter/ui/Footer.tsx +73 -0
  124. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
  125. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +87 -0
  126. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
  127. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
  128. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
  129. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
  130. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
  131. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +97 -0
  132. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
  133. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +128 -0
  134. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +30 -0
  135. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +104 -0
  136. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +175 -0
  137. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +381 -0
  138. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +137 -0
  139. package/src/RedactoPrivacyCenter/ui/PCText.tsx +39 -0
  140. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
  141. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
  142. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
  143. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
  144. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +55 -0
  145. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +70 -0
  146. package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
  147. package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
  148. package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
  149. package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
  150. package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
  151. package/src/index.ts +10 -0
  152. package/src/privacy-center.ts +107 -0
  153. package/tsconfig.json +2 -0
  154. package/tsup.config.ts +16 -1
  155. package/src/utils/isLegacyApi.ts +0 -12
@@ -4,18 +4,17 @@ import type {
4
4
  ConsentEventPayload,
5
5
  FetchConsentContentParams,
6
6
  FetchTTSAudioUrlsParams,
7
- GuardianInfoPayload,
8
- LegacyConsentEventPayload,
7
+ InitiateGuardianVerificationParams,
8
+ InitiateGuardianVerificationResponse,
9
9
  RedactoJwtPayload,
10
10
  SubmitConsentEventParams,
11
- SubmitGuardianInfoParams,
12
11
  TTSAudioUrlsResponse,
12
+ VerifyGuardianStatusParams,
13
+ VerifyGuardianStatusResponse,
13
14
  } from "./types";
14
- import { isLegacyApi } from "../../utils/isLegacyApi";
15
15
 
16
16
  const BASE_URL = "https://api.redacto.io/consent";
17
17
 
18
-
19
18
  // Simple in-memory cache for API responses to reduce calls
20
19
  const apiCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
21
20
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
@@ -60,90 +59,82 @@ export const fetchConsentContent = async ({
60
59
  specific_uuid,
61
60
  validate_against = "all",
62
61
  include_fully_consented_data = false,
63
- signal, // AbortSignal for request cancellation
62
+ signal,
64
63
  }: FetchConsentContentParams & { signal?: AbortSignal }): Promise<ConsentContent> => {
65
- try {
66
- // Validate required parameters
67
- if (!noticeId || !accessToken || !validate_against) {
68
- throw new Error("noticeId and accessToken are required");
69
- }
70
-
71
- const decodedToken = decodeTokenSafely(accessToken);
72
- if (!decodedToken) {
73
- throw new Error("Invalid access token");
74
- }
75
-
76
- const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
77
- if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
78
- throw new Error("Invalid token: missing organization or workspace UUID");
79
- }
64
+ if (!noticeId || !accessToken || !validate_against) {
65
+ throw new Error("noticeId and accessToken are required");
66
+ }
80
67
 
81
- const apiBaseUrl = baseUrl || BASE_URL;
68
+ const decodedToken = decodeTokenSafely(accessToken);
69
+ if (!decodedToken) {
70
+ throw new Error("Invalid access token");
71
+ }
82
72
 
83
- // Create cache key for this request - include accessToken to ensure different users get different cache
84
- const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}-${include_fully_consented_data}`;
73
+ const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
74
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
75
+ throw new Error("Invalid token: missing organization or workspace UUID");
76
+ }
85
77
 
86
- // Check cache first (skip for reconsent requests as they need fresh data)
87
- if (validate_against === "all" && !specific_uuid) {
88
- const cachedData = getCachedData(cacheKey);
89
- if (cachedData) {
90
- return cachedData;
91
- }
92
- }
78
+ const apiBaseUrl = baseUrl || BASE_URL;
93
79
 
94
- const url = new URL(
95
- `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeId}`
96
- );
80
+ // Create cache key for this request - include accessToken to ensure different users get different cache
81
+ const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}-${include_fully_consented_data}`;
97
82
 
98
- if (specific_uuid) {
99
- url.searchParams.append("specific_uuid", specific_uuid);
100
- }
101
- url.searchParams.append("validate_against", validate_against);
102
- if (include_fully_consented_data) {
103
- url.searchParams.append("include_fully_consented_data", "true");
83
+ if (validate_against === "all" && !specific_uuid) {
84
+ const cachedData = getCachedData(cacheKey);
85
+ if (cachedData) {
86
+ return cachedData;
104
87
  }
88
+ }
105
89
 
106
- const response = await fetch(url.toString(), {
107
- method: "GET",
108
- headers: {
109
- Authorization: `Bearer ${accessToken}`,
110
- "Accept-Language": language,
111
- "Content-Type": "application/json",
112
- },
113
- signal, // Add abort signal for request cancellation
114
- });
115
-
116
- if (!response.ok) {
117
- let errorMessage = `Failed to fetch consent content: ${response.status} ${response.statusText}`;
90
+ const url = new URL(
91
+ `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeId}`
92
+ );
118
93
 
119
- if (response.status === 401) {
120
- errorMessage = "Unauthorized: Invalid or expired token";
121
- } else if (response.status === 403) {
122
- errorMessage = "Forbidden: Access denied";
123
- } else if (response.status === 404) {
124
- errorMessage = "Notice not found";
125
- } else if (response.status === 409) {
126
- errorMessage = "User has already provided consent";
127
- } else if (response.status >= 500) {
128
- errorMessage = "Server error: Please try again later";
129
- }
94
+ if (specific_uuid) {
95
+ url.searchParams.append("specific_uuid", specific_uuid);
96
+ }
97
+ url.searchParams.append("validate_against", validate_against);
98
+ if (include_fully_consented_data) {
99
+ url.searchParams.append("include_fully_consented_data", "true");
100
+ }
130
101
 
131
- const error = new Error(errorMessage) as Error & { status?: number };
132
- error.status = response.status;
133
- throw error;
102
+ const response = await fetch(url.toString(), {
103
+ method: "GET",
104
+ headers: {
105
+ Authorization: `Bearer ${accessToken}`,
106
+ "Accept-Language": language,
107
+ },
108
+ signal,
109
+ });
110
+
111
+ if (!response.ok) {
112
+ let errorMessage = `Failed to fetch consent content: ${response.status} ${response.statusText}`;
113
+
114
+ if (response.status === 401) {
115
+ errorMessage = "Unauthorized: Invalid or expired token";
116
+ } else if (response.status === 403) {
117
+ errorMessage = "Forbidden: Access denied";
118
+ } else if (response.status === 404) {
119
+ errorMessage = "Notice not found";
120
+ } else if (response.status === 409) {
121
+ errorMessage = "User has already provided consent";
122
+ } else if (response.status >= 500) {
123
+ errorMessage = "Server error: Please try again later";
134
124
  }
135
125
 
136
- const data: ConsentContent = await response.json();
126
+ const error = new Error(errorMessage) as Error & { status?: number };
127
+ error.status = response.status;
128
+ throw error;
129
+ }
137
130
 
138
- // Cache successful responses (skip for reconsent as data may change)
139
- if (validate_against === "all" && !specific_uuid) {
140
- setCachedData(cacheKey, data);
141
- }
131
+ const data: ConsentContent = await response.json();
142
132
 
143
- return data;
144
- } catch (error) {
145
- throw error;
133
+ if (validate_against === "all" && !specific_uuid) {
134
+ setCachedData(cacheKey, data);
146
135
  }
136
+
137
+ return data;
147
138
  };
148
139
 
149
140
  export const submitConsentEvent = async ({
@@ -152,157 +143,193 @@ export const submitConsentEvent = async ({
152
143
  noticeUuid,
153
144
  purposes,
154
145
  declined,
146
+ language,
155
147
  meta_data,
156
- signal, // AbortSignal for request cancellation
148
+ guardianVerificationReference,
149
+ selfDeclaredAdult,
150
+ signal,
157
151
  }: SubmitConsentEventParams & { signal?: AbortSignal }): Promise<void> => {
158
- try {
159
- // Validate required parameters
160
- if (!noticeUuid || !accessToken || !Array.isArray(purposes)) {
161
- throw new Error("noticeUuid, accessToken, and purposes array are required");
162
- }
152
+ if (!noticeUuid || !accessToken || !Array.isArray(purposes)) {
153
+ throw new Error("noticeUuid, accessToken, and purposes array are required");
154
+ }
163
155
 
164
- const decodedToken = decodeTokenSafely(accessToken);
165
- if (!decodedToken) {
166
- throw new Error("Invalid access token");
167
- }
156
+ const decodedToken = decodeTokenSafely(accessToken);
157
+ if (!decodedToken) {
158
+ throw new Error("Invalid access token");
159
+ }
168
160
 
169
- const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
170
- if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
171
- throw new Error("Invalid token: missing organization or workspace UUID");
172
- }
161
+ const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
162
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
163
+ throw new Error("Invalid token: missing organization or workspace UUID");
164
+ }
173
165
 
174
- const apiBaseUrl = baseUrl || BASE_URL;
166
+ const apiBaseUrl = baseUrl || BASE_URL;
175
167
 
176
- let payload: ConsentEventPayload | LegacyConsentEventPayload;
177
-
178
- if (isLegacyApi(apiBaseUrl)) {
179
- // @deprecated Legacy payload for production (.io) backend
180
- const primaryEmail = typeof window !== "undefined" ? localStorage.getItem("userEmail") : null;
181
- payload = {
182
- primary_email: primaryEmail || undefined,
183
- notice_uuid: noticeUuid,
184
- source: "WEB",
185
- declined: Boolean(declined),
186
- consents: {
187
- purposes: purposes.map((purpose) => {
188
- if (!purpose.uuid) {
189
- throw new Error(`Invalid purpose data: missing uuid`);
190
- }
191
- return {
192
- uuid: purpose.uuid,
193
- name: purpose.name,
194
- description: purpose.description || "",
195
- industries: purpose.industries || "",
196
- selected: Boolean(purpose.selected),
197
- data_elements: (purpose.data_elements || []).map((element) => ({
198
- uuid: element.uuid,
199
- name: element.name,
200
- enabled: Boolean(element.enabled),
201
- required: Boolean(element.required),
202
- selected: element.required ? true : Boolean(element.selected),
203
- })),
204
- };
205
- }),
206
- },
207
- meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
208
- };
209
- } else {
210
- // New payload for staging (.tech) / localhost
211
- const validatedPurposes = purposes.map((purpose) => {
212
- if (!purpose.uuid) {
213
- throw new Error(`Invalid purpose data: missing uuid`);
214
- }
215
-
216
- // Only include enabled data elements (disabled elements are not selectable)
217
- const dataElements = (purpose.data_elements || [])
218
- .filter((element) => element.enabled)
219
- .map((element) => ({
220
- uuid: element.uuid,
221
- selected: element.required ? true : Boolean(element.selected),
222
- }));
223
-
224
- return {
225
- purpose_uuid: purpose.uuid,
226
- selected: Boolean(purpose.selected),
227
- data_elements: dataElements.length > 0 ? dataElements : undefined,
228
- };
229
- });
230
-
231
- payload = {
232
- notice_uuid: noticeUuid,
233
- purposes: validatedPurposes,
234
- select_all_mandatory: false,
235
- source: "WEB",
236
- declined: Boolean(declined),
237
- meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
238
- };
168
+ const validatedPurposes = purposes.map((purpose) => {
169
+ if (!purpose.uuid) {
170
+ throw new Error(`Invalid purpose data: missing uuid`);
239
171
  }
240
172
 
241
- const response = await fetch(
242
- `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
243
- {
244
- method: "POST",
245
- headers: {
246
- Authorization: `Bearer ${accessToken}`,
247
- "Content-Type": "application/json",
248
- },
249
- body: JSON.stringify(payload),
250
- signal, // Add abort signal for request cancellation
251
- }
252
- );
173
+ // Only include enabled data elements (disabled elements are not selectable)
174
+ const dataElements = (purpose.data_elements || [])
175
+ .filter((element) => element.enabled)
176
+ .map((element) => ({
177
+ uuid: element.uuid,
178
+ selected: element.required ? true : Boolean(element.selected),
179
+ }));
180
+
181
+ return {
182
+ purpose_uuid: purpose.uuid,
183
+ selected: Boolean(purpose.selected),
184
+ data_elements: dataElements.length > 0 ? dataElements : undefined,
185
+ };
186
+ });
187
+
188
+ const payload: ConsentEventPayload = {
189
+ notice_uuid: noticeUuid,
190
+ purposes: validatedPurposes,
191
+ select_all_mandatory: false,
192
+ source: "WEB",
193
+ declined: Boolean(declined),
194
+ language: language || undefined,
195
+ meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid, minor_age: meta_data.minor_age } : undefined,
196
+ guardian_verification_reference: guardianVerificationReference,
197
+ self_declared_adult: selfDeclaredAdult,
198
+ };
199
+
200
+ const response = await fetch(
201
+ `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
202
+ {
203
+ method: "POST",
204
+ headers: {
205
+ Authorization: `Bearer ${accessToken}`,
206
+ "Content-Type": "application/json",
207
+ },
208
+ body: JSON.stringify(payload),
209
+ signal,
210
+ }
211
+ );
253
212
 
254
- if (!response.ok) {
255
- let errorMessage = `Failed to submit consent: ${response.status} ${response.statusText}`;
213
+ if (!response.ok) {
214
+ let errorMessage = `Failed to submit consent: ${response.status} ${response.statusText}`;
256
215
 
257
- if (response.status === 400) {
216
+ if (response.status === 400) {
217
+ try {
218
+ const errorData = await response.json();
219
+ errorMessage = errorData.message || "Invalid request data";
220
+ } catch {
258
221
  errorMessage = "Invalid request data";
259
- } else if (response.status === 401) {
260
- errorMessage = "Unauthorized: Invalid or expired token";
261
- } else if (response.status === 403) {
262
- errorMessage = "Forbidden: Access denied";
263
- } else if (response.status === 404) {
264
- errorMessage = "Notice not found";
265
- } else if (response.status >= 500) {
266
- errorMessage = "Server error: Please try again later";
267
222
  }
268
-
269
- const error = new Error(errorMessage) as Error & { status?: number };
270
- error.status = response.status;
271
- throw error;
223
+ } else if (response.status === 401) {
224
+ errorMessage = "Unauthorized: Invalid or expired token";
225
+ } else if (response.status === 403) {
226
+ errorMessage = "Forbidden: Access denied";
227
+ } else if (response.status === 404) {
228
+ errorMessage = "Notice not found";
229
+ } else if (response.status >= 500) {
230
+ errorMessage = "Server error: Please try again later";
272
231
  }
273
232
 
274
- // Clear any cached consent content since user preferences have changed
275
- apiCache.clear();
233
+ const error = new Error(errorMessage) as Error & { status?: number };
234
+ error.status = response.status;
235
+ throw error;
236
+ }
276
237
 
277
- } catch (error) {
238
+ // Clear any cached consent content since user preferences have changed
239
+ apiCache.clear();
240
+ };
241
+
242
+ export const fetchTTSAudioUrls = async ({
243
+ accessToken,
244
+ baseUrl,
245
+ noticeUuid,
246
+ language,
247
+ signal,
248
+ }: FetchTTSAudioUrlsParams & { signal?: AbortSignal }): Promise<TTSAudioUrlsResponse> => {
249
+ if (!accessToken || !noticeUuid || !language) {
250
+ throw new Error("accessToken, noticeUuid, and language are required");
251
+ }
252
+
253
+ const decodedToken = decodeTokenSafely(accessToken);
254
+ if (!decodedToken) {
255
+ throw new Error("Invalid access token");
256
+ }
257
+
258
+ const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
259
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
260
+ throw new Error("Invalid token: missing organization or workspace UUID");
261
+ }
262
+
263
+ const apiBaseUrl = baseUrl || BASE_URL;
264
+
265
+ const url = `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeUuid}/audio/${language}`;
266
+
267
+ const response = await fetch(url, {
268
+ method: "GET",
269
+ headers: {
270
+ Authorization: `Bearer ${accessToken}`,
271
+ Accept: "application/json",
272
+ },
273
+ signal,
274
+ });
275
+
276
+ if (!response.ok) {
277
+ let errorMessage = `Failed to fetch TTS audio URLs: ${response.status} ${response.statusText}`;
278
+
279
+ if (response.status === 401) {
280
+ errorMessage = "Unauthorized: Invalid or expired token";
281
+ } else if (response.status === 403) {
282
+ errorMessage = "Forbidden: Access denied";
283
+ } else if (response.status === 404) {
284
+ errorMessage = "Notice or audio not found";
285
+ } else if (response.status >= 500) {
286
+ errorMessage = "Server error: Please try again later";
287
+ }
288
+
289
+ const error = new Error(errorMessage) as Error & { status?: number };
290
+ error.status = response.status;
278
291
  throw error;
279
292
  }
293
+
294
+ const data: TTSAudioUrlsResponse = await response.json();
295
+ return data;
280
296
  };
281
297
 
282
- export const submitGuardianInfo = async ({
298
+ // =============================================================================
299
+ // Guardian Verification API Functions
300
+ // =============================================================================
301
+
302
+ /**
303
+ * Initiate guardian verification via DigiLocker
304
+ * POST /guardian/initiate-verification
305
+ *
306
+ * This combines guardian details submission and DigiLocker initiation in one call.
307
+ */
308
+ export const initiateGuardianVerification = async ({
283
309
  accessToken,
284
310
  baseUrl,
285
311
  guardianName,
286
312
  guardianContact,
287
313
  guardianRelationship,
288
- signal, // AbortSignal for request cancellation
289
- }: SubmitGuardianInfoParams & { signal?: AbortSignal }): Promise<void> => {
314
+ frontendCallbackUrl,
315
+ metadata,
316
+ signal,
317
+ }: InitiateGuardianVerificationParams): Promise<InitiateGuardianVerificationResponse> => {
290
318
  try {
291
- // Validate required parameters
292
319
  if (!accessToken) {
293
320
  throw new Error("accessToken is required");
294
321
  }
295
322
 
296
323
  if (!guardianName?.trim()) {
297
- throw new Error("guardianName is required and cannot be empty");
324
+ throw new Error("Guardian name is required");
298
325
  }
299
326
 
300
327
  if (!guardianContact?.trim()) {
301
- throw new Error("guardianContact is required and cannot be empty");
328
+ throw new Error("Guardian contact is required");
302
329
  }
303
330
 
304
331
  if (!guardianRelationship?.trim()) {
305
- throw new Error("guardianRelationship is required and cannot be empty");
332
+ throw new Error("Guardian relationship is required");
306
333
  }
307
334
 
308
335
  const decodedToken = decodeTokenSafely(accessToken);
@@ -315,16 +342,30 @@ export const submitGuardianInfo = async ({
315
342
  throw new Error("Invalid token: missing organization or workspace UUID");
316
343
  }
317
344
 
318
- const payload: GuardianInfoPayload = {
345
+ const apiBaseUrl = baseUrl || BASE_URL;
346
+
347
+ const payload: {
348
+ guardian_name: string;
349
+ guardian_contact: string;
350
+ guardian_relationship: string;
351
+ frontend_callback_url?: string;
352
+ metadata?: Record<string, unknown>;
353
+ } = {
319
354
  guardian_name: guardianName.trim(),
320
355
  guardian_contact: guardianContact.trim(),
321
356
  guardian_relationship: guardianRelationship.trim(),
322
357
  };
323
358
 
324
- const apiBaseUrl = baseUrl || BASE_URL;
359
+ if (frontendCallbackUrl) {
360
+ payload.frontend_callback_url = frontendCallbackUrl;
361
+ }
362
+
363
+ if (metadata) {
364
+ payload.metadata = metadata;
365
+ }
325
366
 
326
367
  const response = await fetch(
327
- `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/guardian-info`,
368
+ `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/guardian/initiate-verification`,
328
369
  {
329
370
  method: "POST",
330
371
  headers: {
@@ -332,27 +373,29 @@ export const submitGuardianInfo = async ({
332
373
  "Content-Type": "application/json",
333
374
  },
334
375
  body: JSON.stringify(payload),
335
- signal, // Add abort signal for request cancellation
376
+ signal,
336
377
  }
337
378
  );
338
379
 
339
380
  if (!response.ok) {
340
- let errorMessage = `Failed to submit guardian info: ${response.status} ${response.statusText}`;
381
+ let errorMessage = `Failed to initiate verification: ${response.status} ${response.statusText}`;
341
382
 
342
383
  if (response.status === 400) {
343
- errorMessage = "Invalid request data";
384
+ try {
385
+ const errorData = await response.json();
386
+ errorMessage = errorData.message || "Invalid request data";
387
+ } catch {
388
+ errorMessage = "Invalid request data";
389
+ }
344
390
  } else if (response.status === 401) {
345
391
  errorMessage = "Unauthorized: Invalid or expired token";
346
- } else if (response.status === 403) {
347
- errorMessage = "Forbidden: Access denied";
348
392
  } else if (response.status === 404) {
349
393
  errorMessage = "Organization or workspace not found";
350
394
  } else if (response.status === 422) {
351
- // Handle validation errors from the server
352
395
  try {
353
396
  const errorData = await response.json();
354
- errorMessage = errorData.message || "Validation error";
355
- } catch (parseError) {
397
+ errorMessage = errorData.message || "Validation error: Please check your input";
398
+ } catch {
356
399
  errorMessage = "Validation error: Please check your input";
357
400
  }
358
401
  } else if (response.status >= 500) {
@@ -364,72 +407,120 @@ export const submitGuardianInfo = async ({
364
407
  throw error;
365
408
  }
366
409
 
410
+ const responseData = await response.json();
411
+
412
+ // Handle response with detail wrapper (backend format: { code, status, detail: {...} })
413
+ const detail = responseData.detail || responseData;
414
+
415
+ // Already verified — skip DigiLocker entirely
416
+ if (detail.already_verified) {
417
+ return {
418
+ success: true,
419
+ guardian_info_uuid: detail.guardian_info_uuid,
420
+ already_verified: true,
421
+ verification_reference: detail.verification_reference,
422
+ };
423
+ }
424
+
425
+ // New verification flow — needs DigiLocker
426
+ if (!detail.session_token || !detail.digilocker_redirect_url) {
427
+ throw new Error("Invalid response: missing session_token or digilocker_redirect_url");
428
+ }
429
+
430
+ return {
431
+ success: detail.success ?? true,
432
+ guardian_info_uuid: detail.guardian_info_uuid,
433
+ already_verified: false,
434
+ session_token: detail.session_token,
435
+ digilocker_redirect_url: detail.digilocker_redirect_url,
436
+ expires_at: detail.expires_at,
437
+ };
367
438
  } catch (error) {
368
- // Log error for debugging but don't expose sensitive information
369
- console.error("Error submitting guardian info:", error instanceof Error ? error.message : error);
439
+ console.error("Error initiating guardian verification:", error instanceof Error ? error.message : error);
370
440
  throw error;
371
441
  }
372
442
  };
373
443
 
374
- export const fetchTTSAudioUrls = async ({
444
+ /**
445
+ * Check guardian verification status
446
+ * POST /guardian/verify-status
447
+ *
448
+ * Poll this endpoint to check if DigiLocker verification is complete.
449
+ * Returns verification_reference when status is "verified".
450
+ */
451
+ export const verifyGuardianStatus = async ({
375
452
  accessToken,
376
453
  baseUrl,
377
- noticeUuid,
378
- language,
379
- signal, // AbortSignal for request cancellation
380
- }: FetchTTSAudioUrlsParams & { signal?: AbortSignal }): Promise<TTSAudioUrlsResponse> => {
381
- try {
382
- // Validate required parameters
383
- if (!accessToken || !noticeUuid || !language) {
384
- throw new Error("accessToken, noticeUuid, and language are required");
385
- }
386
-
387
- const decodedToken = decodeTokenSafely(accessToken);
388
- if (!decodedToken) {
389
- throw new Error("Invalid access token");
390
- }
454
+ sessionToken,
455
+ signal,
456
+ }: VerifyGuardianStatusParams): Promise<VerifyGuardianStatusResponse> => {
457
+ if (!accessToken || !sessionToken) {
458
+ throw new Error("accessToken and sessionToken are required");
459
+ }
391
460
 
392
- const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
393
- if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
394
- throw new Error("Invalid token: missing organization or workspace UUID");
395
- }
461
+ const decodedToken = decodeTokenSafely(accessToken);
462
+ if (!decodedToken) {
463
+ throw new Error("Invalid access token");
464
+ }
396
465
 
397
- const apiBaseUrl = baseUrl || BASE_URL;
466
+ const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
467
+ if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
468
+ throw new Error("Invalid token: missing organization or workspace UUID");
469
+ }
398
470
 
399
- const url = `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeUuid}/audio/${language}`;
471
+ const apiBaseUrl = baseUrl || BASE_URL;
400
472
 
401
- const response = await fetch(url, {
402
- method: "GET",
473
+ const response = await fetch(
474
+ `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/guardian/verify-status`,
475
+ {
476
+ method: "POST",
403
477
  headers: {
404
478
  Authorization: `Bearer ${accessToken}`,
405
- Accept: "application/json",
406
479
  "Content-Type": "application/json",
407
480
  },
408
- signal, // Add abort signal for request cancellation
409
- });
481
+ body: JSON.stringify({
482
+ session_token: sessionToken,
483
+ }),
484
+ signal,
485
+ }
486
+ );
410
487
 
411
- if (!response.ok) {
412
- let errorMessage = `Failed to fetch TTS audio URLs: ${response.status} ${response.statusText}`;
488
+ if (!response.ok) {
489
+ let errorMessage = `Failed to check verification status: ${response.status} ${response.statusText}`;
413
490
 
414
- if (response.status === 401) {
415
- errorMessage = "Unauthorized: Invalid or expired token";
416
- } else if (response.status === 403) {
417
- errorMessage = "Forbidden: Access denied";
418
- } else if (response.status === 404) {
419
- errorMessage = "Notice or audio not found";
420
- } else if (response.status >= 500) {
421
- errorMessage = "Server error: Please try again later";
491
+ if (response.status === 400) {
492
+ try {
493
+ const errorData = await response.json();
494
+ errorMessage = errorData.message || errorData.detail?.message || "Invalid session token";
495
+ } catch {
496
+ errorMessage = "Invalid session token";
422
497
  }
423
-
424
- const error = new Error(errorMessage) as Error & { status?: number };
425
- error.status = response.status;
426
- throw error;
498
+ } else if (response.status === 401) {
499
+ errorMessage = "Unauthorized: Invalid or expired token";
500
+ } else if (response.status === 404) {
501
+ errorMessage = "Verification session not found or expired. Please restart the verification process.";
502
+ } else if (response.status === 410) {
503
+ errorMessage = "Verification session has expired. Please restart the verification process.";
504
+ } else if (response.status >= 500) {
505
+ errorMessage = "Server error: Please try again later";
427
506
  }
428
507
 
429
- const data: TTSAudioUrlsResponse = await response.json();
430
-
431
- return data;
432
- } catch (error) {
508
+ const error = new Error(errorMessage) as Error & { status?: number };
509
+ error.status = response.status;
433
510
  throw error;
434
511
  }
512
+
513
+ const responseData = await response.json();
514
+ const detail = responseData.detail || responseData;
515
+
516
+ return {
517
+ status: detail.status ?? "in_progress",
518
+ guardian_info_uuid: detail.guardian_info_uuid ?? null,
519
+ verification_reference: detail.verification_reference ?? null,
520
+ guardian_details: detail.guardian_details ?? null,
521
+ error: detail.error ?? null,
522
+ error_code: detail.error_code ?? null,
523
+ can_retry: detail.can_retry ?? true,
524
+ };
435
525
  };
526
+