@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
@@ -0,0 +1,413 @@
1
+ import { apiFetch } from "./fetcher";
2
+ import type { ApiFetchOptions, ApiResponse } from "./fetcher";
3
+
4
+ /**
5
+ * Base request parameters for consent service calls
6
+ */
7
+ export type BaseRequest = {
8
+ organisationUuid: string;
9
+ workspaceUuid: string;
10
+ authToken: string;
11
+ };
12
+
13
+ /**
14
+ * Consent service client factory.
15
+ * Creates a client instance with a specific base URL.
16
+ * @param baseUrl - Consent server base URL
17
+ */
18
+ export function createConsentClient(baseUrl: string) {
19
+ /**
20
+ * Privacy Center API fetch wrapper that uses Authorization: Bearer header.
21
+ * Privacy center endpoints require JWT tokens in Authorization header, not X-Auth-Token.
22
+ */
23
+ async function privacyCenterApiFetch<T>(
24
+ url: string,
25
+ options: ApiFetchOptions & { authToken?: string }
26
+ ): Promise<T> {
27
+ const { authToken, getAuthToken, headers = {}, ...fetchOptions } = options;
28
+
29
+ // Build headers using Headers object
30
+ const requestHeaders = new Headers();
31
+
32
+ // Set default headers (skip Content-Type for FormData)
33
+ const isFormData = fetchOptions.body instanceof FormData;
34
+ if (!isFormData) {
35
+ requestHeaders.set("Content-Type", "application/json");
36
+ }
37
+ requestHeaders.set("Accept", "application/json");
38
+
39
+ // Add custom headers from options
40
+ if (headers && typeof headers === "object") {
41
+ if (headers instanceof Headers) {
42
+ headers.forEach((value, key) => {
43
+ requestHeaders.set(key, value);
44
+ });
45
+ } else {
46
+ Object.entries(headers as Record<string, string>).forEach(
47
+ ([key, value]) => {
48
+ if (value) {
49
+ requestHeaders.set(key, value);
50
+ }
51
+ }
52
+ );
53
+ }
54
+ }
55
+
56
+ // Add Authorization Bearer token for privacy center APIs
57
+ const token = authToken || (getAuthToken ? await getAuthToken() : null);
58
+ if (token) {
59
+ const trimmedToken = token.trim();
60
+ // Format token with Bearer prefix if not already present
61
+ const bearerToken = trimmedToken.startsWith('Bearer ')
62
+ ? trimmedToken
63
+ : `Bearer ${trimmedToken}`;
64
+ requestHeaders.set("Authorization", bearerToken);
65
+ }
66
+
67
+ // Build fetch options
68
+ const fetchOptionsFinal: RequestInit = {
69
+ ...fetchOptions,
70
+ headers: requestHeaders,
71
+ };
72
+
73
+ try {
74
+ const response = await fetch(url, fetchOptionsFinal);
75
+
76
+ // Handle non-OK responses
77
+ if (!response.ok) {
78
+ let errorMessage = `Request failed with status ${response.status}`;
79
+
80
+ try {
81
+ const errorData = (await response.json()) as {
82
+ detail?: unknown;
83
+ message?: string;
84
+ } | null;
85
+ if (errorData) {
86
+ if ("detail" in errorData && errorData.detail) {
87
+ errorMessage =
88
+ typeof errorData.detail === "string"
89
+ ? errorData.detail
90
+ : JSON.stringify(errorData.detail, null, 2);
91
+ } else if ("message" in errorData && errorData.message) {
92
+ errorMessage = errorData.message;
93
+ }
94
+ }
95
+ } catch {
96
+ // If response is not JSON, try to get text
97
+ try {
98
+ const errorText = await response.text();
99
+ if (errorText) {
100
+ errorMessage = errorText;
101
+ }
102
+ } catch {
103
+ // Ignore parsing errors
104
+ }
105
+ }
106
+
107
+ throw new Error(errorMessage);
108
+ }
109
+
110
+ // Parse JSON response
111
+ const contentType = response.headers.get("Content-Type") || "";
112
+ if (!contentType.includes("application/json")) {
113
+ const textResponse = await response.text();
114
+ throw new Error(
115
+ `Expected JSON response, got ${contentType}: ${textResponse}`
116
+ );
117
+ }
118
+
119
+ const data = (await response.json()) as ApiResponse<T> | T;
120
+
121
+ // Handle wrapped API responses (code, status, detail format)
122
+ if (
123
+ typeof data === "object" &&
124
+ data !== null &&
125
+ "code" in data &&
126
+ "status" in data &&
127
+ "detail" in data
128
+ ) {
129
+ const apiResponse = data as ApiResponse<T>;
130
+ return apiResponse.detail as T;
131
+ }
132
+
133
+ return data as T;
134
+ } catch (error) {
135
+ if (error instanceof Error) {
136
+ throw error;
137
+ }
138
+ throw new Error(`Unknown error occurred: ${String(error)}`);
139
+ }
140
+ }
141
+
142
+ return {
143
+ /**
144
+ * Get form data (privacy center)
145
+ */
146
+ async getFormData(
147
+ baseRequest: BaseRequest,
148
+ contact: string,
149
+ authToken?: string,
150
+ options?: ApiFetchOptions & { useSessionAuth?: boolean },
151
+ language?: string
152
+ ): Promise<unknown> {
153
+ const queryParams = new URLSearchParams();
154
+ if (language) queryParams.append("language", language);
155
+ const queryString = queryParams.toString();
156
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/form/data${queryString ? `?${queryString}` : ""}`;
157
+ const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
158
+
159
+ // Case management uses session-based auth (X-Auth-Token)
160
+ // Privacy center uses JWT Bearer tokens (Authorization: Bearer)
161
+ if (useSessionAuth) {
162
+ return apiFetch(url, {
163
+ ...fetchOptions,
164
+ method: "POST",
165
+ body: JSON.stringify({ contact }),
166
+ getAuthToken,
167
+ });
168
+ }
169
+
170
+ // Privacy center: use JWT Bearer token
171
+ return privacyCenterApiFetch(url, {
172
+ ...fetchOptions,
173
+ method: "POST",
174
+ body: JSON.stringify({ contact }),
175
+ authToken: authToken || baseRequest.authToken,
176
+ });
177
+ },
178
+
179
+ /**
180
+ * Create case request (privacy center or case management)
181
+ */
182
+ async createCaseRequest(
183
+ baseRequest: BaseRequest,
184
+ payload: unknown,
185
+ authToken?: string,
186
+ options?: ApiFetchOptions & { useSessionAuth?: boolean }
187
+ ): Promise<unknown> {
188
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/case`;
189
+ const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
190
+
191
+ // Case management uses session-based auth (X-Auth-Token)
192
+ // Privacy center uses JWT Bearer tokens (Authorization: Bearer)
193
+ if (useSessionAuth) {
194
+ return apiFetch(url, {
195
+ ...fetchOptions,
196
+ method: "POST",
197
+ body: JSON.stringify(payload),
198
+ getAuthToken,
199
+ });
200
+ }
201
+
202
+ // Privacy center: use JWT Bearer token
203
+ return privacyCenterApiFetch(url, {
204
+ ...fetchOptions,
205
+ method: "POST",
206
+ body: JSON.stringify(payload),
207
+ authToken: authToken || baseRequest.authToken,
208
+ });
209
+ },
210
+
211
+ /**
212
+ * Create OTP (public)
213
+ */
214
+ async createOtp(
215
+ organisationId: string,
216
+ workspaceId: string,
217
+ email: string,
218
+ options?: Omit<ApiFetchOptions, "getAuthToken">
219
+ ): Promise<unknown> {
220
+ const url = `${baseUrl}/public/organisations/${organisationId}/workspaces/${workspaceId}/otp/create`;
221
+ return apiFetch(url, {
222
+ ...options,
223
+ method: "POST",
224
+ body: JSON.stringify({ to: email, channel: "email" }),
225
+ });
226
+ },
227
+
228
+ /**
229
+ * Verify OTP (public)
230
+ */
231
+ async verifyOtp(
232
+ organisationId: string,
233
+ workspaceId: string,
234
+ otpId: string,
235
+ otp: string,
236
+ options?: Omit<ApiFetchOptions, "getAuthToken">
237
+ ): Promise<unknown> {
238
+ const url = `${baseUrl}/public/organisations/${organisationId}/workspaces/${workspaceId}/otp/verify`;
239
+ return apiFetch(url, {
240
+ ...options,
241
+ method: "POST",
242
+ body: JSON.stringify({ uuid: otpId, otp }),
243
+ });
244
+ },
245
+
246
+ /**
247
+ * Exchange one-time token for JWT tokens (public)
248
+ */
249
+ async exchangeToken(
250
+ token: string,
251
+ ): Promise<unknown> {
252
+ const url = `${baseUrl}/public/tokens/token-exchange?token=${encodeURIComponent(token)}`;
253
+ const response = await fetch(url, {
254
+ method: "GET",
255
+ headers: { "Accept": "application/json" },
256
+ });
257
+
258
+ if (!response.ok) {
259
+ const errorData = await response.json().catch(() => null) as { detail?: string; message?: string } | null;
260
+ throw new Error(
261
+ errorData?.detail || errorData?.message || `Token exchange failed with status ${response.status}`
262
+ );
263
+ }
264
+
265
+ const data = await response.json() as { code?: number; status?: string; detail?: unknown };
266
+ if (data && typeof data === "object" && "code" in data && "status" in data && "detail" in data) {
267
+ return data.detail;
268
+ }
269
+ return data;
270
+ },
271
+
272
+ /**
273
+ * Refresh OTP token (public)
274
+ */
275
+ async refreshOtpToken(
276
+ organisationId: string,
277
+ workspaceId: string,
278
+ refreshToken: string,
279
+ options?: Omit<ApiFetchOptions, "getAuthToken">
280
+ ): Promise<unknown> {
281
+ const url = `${baseUrl}/public/organisations/${organisationId}/workspaces/${workspaceId}/otp/refresh`;
282
+ return apiFetch(url, {
283
+ ...options,
284
+ method: "POST",
285
+ body: JSON.stringify({ refresh_token: refreshToken }),
286
+ });
287
+ },
288
+
289
+ /**
290
+ * Get products
291
+ */
292
+ async getProducts(
293
+ baseRequest: BaseRequest,
294
+ skip: number = 0,
295
+ limit: number = 10,
296
+ options?: Omit<ApiFetchOptions, "getAuthToken">
297
+ ): Promise<unknown> {
298
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/notice-management/products/?skip=${skip}&limit=${limit}`;
299
+ return apiFetch<unknown>(url, {
300
+ ...options,
301
+ getAuthToken: async () => baseRequest.authToken,
302
+ });
303
+ },
304
+
305
+ /**
306
+ * Get user consents (privacy center)
307
+ */
308
+ async getUserConsents(
309
+ baseRequest: BaseRequest,
310
+ contact: string,
311
+ offset?: number,
312
+ limit?: number,
313
+ search?: string,
314
+ status?: string,
315
+ product_uuid?: string,
316
+ language?: string,
317
+ authToken?: string,
318
+ options?: Omit<ApiFetchOptions, "getAuthToken">
319
+ ): Promise<unknown> {
320
+ const queryParams = new URLSearchParams();
321
+ if (offset !== undefined) queryParams.append("offset", offset.toString());
322
+ if (limit !== undefined) queryParams.append("limit", limit.toString());
323
+ if (search) queryParams.append("search", search);
324
+ if (status) queryParams.append("status", status);
325
+ if (product_uuid) queryParams.append("product_uuid", product_uuid);
326
+ if (language) queryParams.append("language", language);
327
+ const queryString = queryParams.toString();
328
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/user-consents${queryString ? `?${queryString}` : ""}`;
329
+ return privacyCenterApiFetch(url, {
330
+ ...options,
331
+ method: "POST",
332
+ body: JSON.stringify({ contact }),
333
+ authToken: authToken || baseRequest.authToken,
334
+ });
335
+ },
336
+
337
+ /**
338
+ * Manage consent (privacy center) - revoke, regrant, or renew
339
+ */
340
+ async manageConsent(
341
+ baseRequest: BaseRequest,
342
+ purposeId: string,
343
+ contact: string,
344
+ status: 'revoke' | 'regrant' | 'renew',
345
+ authToken?: string,
346
+ options?: Omit<ApiFetchOptions, "getAuthToken">,
347
+ nominatorContact?: string,
348
+ dataElementUuids?: string[]
349
+ ): Promise<unknown> {
350
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/manage-consent?status=${status}`;
351
+ const body: Record<string, unknown> = { purpose_uuid: purposeId, contact };
352
+ if (nominatorContact) {
353
+ body.nominator_contact = nominatorContact;
354
+ }
355
+ if (Array.isArray(dataElementUuids) && dataElementUuids.length > 0) {
356
+ body.data_element_uuids = dataElementUuids;
357
+ }
358
+ return privacyCenterApiFetch(url, {
359
+ ...options,
360
+ method: "POST",
361
+ body: JSON.stringify(body),
362
+ authToken: authToken || baseRequest.authToken,
363
+ });
364
+ },
365
+
366
+ /**
367
+ * Get activities (privacy center)
368
+ */
369
+ async getActivities(
370
+ baseRequest: BaseRequest,
371
+ offset?: number,
372
+ limit?: number,
373
+ language?: string,
374
+ authToken?: string,
375
+ options?: Omit<ApiFetchOptions, "getAuthToken">
376
+ ): Promise<unknown> {
377
+ const queryParams = new URLSearchParams();
378
+ if (offset !== undefined) queryParams.append("offset", offset.toString());
379
+ if (limit !== undefined) queryParams.append("limit", limit.toString());
380
+ if (language) queryParams.append("language", language);
381
+ const queryString = queryParams.toString();
382
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/activities${queryString ? `?${queryString}` : ""}`;
383
+ return privacyCenterApiFetch<unknown>(url, {
384
+ ...options,
385
+ authToken: authToken || baseRequest.authToken,
386
+ });
387
+ },
388
+
389
+ /**
390
+ * Get case history (privacy center)
391
+ */
392
+ async getCaseHistory(
393
+ baseRequest: BaseRequest,
394
+ contact: string,
395
+ offset?: number,
396
+ limit?: number,
397
+ language?: string,
398
+ authToken?: string,
399
+ options?: Omit<ApiFetchOptions, "getAuthToken">
400
+ ): Promise<unknown> {
401
+ const queryParams = new URLSearchParams();
402
+ if (offset !== undefined) queryParams.append("offset", offset.toString());
403
+ if (limit !== undefined) queryParams.append("limit", limit.toString());
404
+ if (language) queryParams.append("language", language);
405
+ const queryString = queryParams.toString();
406
+ const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/cases?${queryString}`;
407
+ return privacyCenterApiFetch<unknown>(url, {
408
+ ...options,
409
+ authToken: authToken || baseRequest.authToken,
410
+ });
411
+ },
412
+ };
413
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Client-side configuration for backend service URLs.
3
+ * consent.baseUrl is set via BaseUrlProvider context.
4
+ */
5
+ let baseUrl: string | undefined = undefined;
6
+
7
+ export const config = {
8
+ consent: {
9
+ get baseUrl() {
10
+ return baseUrl;
11
+ },
12
+ },
13
+ } as const;
14
+
15
+ /**
16
+ * Sets the base URL from the context provider.
17
+ * This should be called by BaseUrlProvider when it mounts.
18
+ */
19
+ export function setUserBaseUrl(url: string): void {
20
+ baseUrl = url;
21
+ }
22
+
23
+ /**
24
+ * Validates that required base URL is set.
25
+ * Throws an error if base URL is missing.
26
+ */
27
+ export function validateConfig(): void {
28
+ if (!config.consent.baseUrl) {
29
+ throw new Error(
30
+ "Base URL is not defined. Ensure BaseUrlProvider is set up correctly."
31
+ );
32
+ }
33
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Options for apiFetch function
3
+ */
4
+ export type ApiFetchOptions = RequestInit & {
5
+ /**
6
+ * Auth token getter function
7
+ */
8
+ getAuthToken?: () => Promise<string | null>;
9
+ };
10
+
11
+ /**
12
+ * Standard API response wrapper
13
+ */
14
+ export type ApiResponse<T> = {
15
+ code: number;
16
+ status: string;
17
+ detail: T;
18
+ };
19
+
20
+ /**
21
+ * Unified fetch wrapper for client-side API calls.
22
+ * Provides typed responses and automatic error handling.
23
+ *
24
+ * @param url - The URL to fetch
25
+ * @param options - Fetch options including auth token getter
26
+ * @returns Typed response data
27
+ * @throws Error if the request fails
28
+ */
29
+ export async function apiFetch<T>(
30
+ url: string,
31
+ options: ApiFetchOptions = {}
32
+ ): Promise<T> {
33
+ const { getAuthToken, headers = {}, ...fetchOptions } = options;
34
+
35
+ // Build headers using Headers object for better compatibility
36
+ const requestHeaders = new Headers();
37
+
38
+ // Set default headers (skip Content-Type for FormData - browser will set it with boundary)
39
+ const isFormData = fetchOptions.body instanceof FormData;
40
+ if (!isFormData) {
41
+ requestHeaders.set("Content-Type", "application/json");
42
+ }
43
+ requestHeaders.set("Accept", "application/json");
44
+
45
+ // Add custom headers from options
46
+ if (headers && typeof headers === "object") {
47
+ if (headers instanceof Headers) {
48
+ headers.forEach((value, key) => {
49
+ requestHeaders.set(key, value);
50
+ });
51
+ } else {
52
+ Object.entries(headers as Record<string, string>).forEach(
53
+ ([key, value]) => {
54
+ if (value) {
55
+ requestHeaders.set(key, value);
56
+ }
57
+ }
58
+ );
59
+ }
60
+ }
61
+
62
+ // Add auth token AFTER custom headers to ensure it's never overridden
63
+ if (getAuthToken) {
64
+ const token = await getAuthToken();
65
+ if (token) {
66
+ // Use the exact header format that works in other parts of the codebase
67
+ // Other working API calls use 'X-Auth-Token' (capital A and T, rest lowercase)
68
+ const trimmedToken = token.trim();
69
+ requestHeaders.set("X-Auth-Token", trimmedToken);
70
+ // Also set lowercase for compatibility (some servers are case-sensitive)
71
+ requestHeaders.set("x-auth-token", trimmedToken);
72
+ }
73
+ }
74
+
75
+ // Build fetch options
76
+ const fetchOptionsFinal: RequestInit = {
77
+ ...fetchOptions,
78
+ headers: requestHeaders,
79
+ };
80
+
81
+ try {
82
+ const response = await fetch(url, fetchOptionsFinal);
83
+
84
+ // Handle non-OK responses
85
+ if (!response.ok) {
86
+ let errorMessage = `Request failed with status ${response.status}`;
87
+
88
+ try {
89
+ const errorData = (await response.json()) as {
90
+ detail?: unknown;
91
+ message?: string;
92
+ } | null;
93
+ if (errorData) {
94
+ if ("detail" in errorData && errorData.detail) {
95
+ errorMessage =
96
+ typeof errorData.detail === "string"
97
+ ? errorData.detail
98
+ : JSON.stringify(errorData.detail, null, 2);
99
+ } else if ("message" in errorData && errorData.message) {
100
+ errorMessage = errorData.message;
101
+ }
102
+ }
103
+ } catch {
104
+ // If response is not JSON, try to get text
105
+ try {
106
+ const errorText = await response.text();
107
+ if (errorText) {
108
+ errorMessage = errorText;
109
+ }
110
+ } catch {
111
+ // Ignore parsing errors
112
+ }
113
+ }
114
+
115
+ throw new Error(errorMessage);
116
+ }
117
+
118
+ // Handle empty responses (common for DELETE requests with 204 No Content)
119
+ const contentLength = response.headers.get("Content-Length");
120
+ const contentType = response.headers.get("Content-Type") || "";
121
+
122
+ // If content length is 0, return undefined for void types
123
+ if (contentLength === "0") {
124
+ return undefined as T;
125
+ }
126
+
127
+ // Read response text once (we can't read it twice)
128
+ let text: string;
129
+ try {
130
+ text = await response.text();
131
+ } catch (error) {
132
+ // If reading fails, assume empty response is acceptable for void return types
133
+ return undefined as T;
134
+ }
135
+
136
+ // If response is empty, return undefined for void types
137
+ if (!text || text.trim() === "") {
138
+ return undefined as T;
139
+ }
140
+
141
+ // Parse JSON response
142
+ if (contentType && !contentType.includes("application/json")) {
143
+ throw new Error(
144
+ `Expected JSON response, got ${contentType}: ${text.substring(0, 100)}`
145
+ );
146
+ }
147
+
148
+ let data: ApiResponse<T> | T;
149
+ try {
150
+ data = JSON.parse(text) as ApiResponse<T> | T;
151
+ } catch (parseError) {
152
+ // If JSON parsing fails, check if it's just whitespace or "null"
153
+ if (text.trim() === "" || text.trim() === "null") {
154
+ return undefined as T;
155
+ }
156
+ // Otherwise, throw the parsing error
157
+ throw new Error(
158
+ `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}. Response: ${text.substring(0, 200)}`
159
+ );
160
+ }
161
+
162
+ // Handle wrapped API responses (code, status, detail format)
163
+ if (
164
+ typeof data === "object" &&
165
+ data !== null &&
166
+ "code" in data &&
167
+ "status" in data &&
168
+ "detail" in data
169
+ ) {
170
+ const apiResponse = data as ApiResponse<T>;
171
+ return apiResponse.detail as T;
172
+ }
173
+
174
+ return data as T;
175
+ } catch (error) {
176
+ if (error instanceof Error) {
177
+ throw error;
178
+ }
179
+ throw new Error(`Unknown error occurred: ${String(error)}`);
180
+ }
181
+ }