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