@redacto.io/consent-sdk-react 4.0.0 → 5.1.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 (161) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/LICENSE +176 -0
  3. package/README.md +98 -1
  4. package/dist/index.js +231 -96
  5. package/dist/index.mjs +231 -96
  6. package/dist/privacy-center.d.mts +649 -0
  7. package/dist/privacy-center.d.ts +649 -0
  8. package/dist/privacy-center.js +20824 -0
  9. package/dist/privacy-center.mjs +20789 -0
  10. package/package.json +47 -3
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +6 -4
  12. package/src/RedactoNoticeConsent/api/index.ts +2 -0
  13. package/src/RedactoNoticeConsent/api/types.ts +2 -0
  14. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +128 -62
  15. package/src/RedactoNoticeConsent/language-codes.ts +97 -0
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
  17. package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
  18. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
  19. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
  20. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
  21. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
  22. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +30 -0
  23. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +173 -0
  24. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +55 -0
  25. package/src/RedactoPrivacyCenter/api/actions.ts +476 -0
  26. package/src/RedactoPrivacyCenter/api/client.ts +524 -0
  27. package/src/RedactoPrivacyCenter/api/config.ts +33 -0
  28. package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
  29. package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
  30. package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
  31. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
  32. package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
  33. package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
  34. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
  35. package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
  36. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
  37. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
  38. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
  39. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
  40. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
  41. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
  42. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
  43. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
  44. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
  45. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
  46. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
  47. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
  48. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
  49. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +469 -0
  50. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +426 -0
  51. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +732 -0
  52. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +238 -0
  53. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
  54. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
  55. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
  56. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
  57. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
  58. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +304 -0
  59. package/src/RedactoPrivacyCenter/components/Form.tsx +1596 -0
  60. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +174 -0
  61. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +337 -0
  62. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +330 -0
  63. package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
  64. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +304 -0
  65. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +232 -0
  66. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
  67. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +408 -0
  68. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
  69. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
  70. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
  71. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
  72. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
  73. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
  74. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +159 -0
  75. package/src/RedactoPrivacyCenter/components/constant.ts +16 -0
  76. package/src/RedactoPrivacyCenter/components/types.ts +76 -0
  77. package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
  78. package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
  79. package/src/RedactoPrivacyCenter/config/table.ts +25 -0
  80. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +137 -0
  81. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +41 -0
  82. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
  83. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +96 -0
  84. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
  85. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
  86. package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
  87. package/src/RedactoPrivacyCenter/context/types.ts +14 -0
  88. package/src/RedactoPrivacyCenter/lib/api.ts +566 -0
  89. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
  90. package/src/RedactoPrivacyCenter/lib/constants.ts +161 -0
  91. package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
  92. package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
  93. package/src/RedactoPrivacyCenter/lib/queries.ts +134 -0
  94. package/src/RedactoPrivacyCenter/lib/types.ts +531 -0
  95. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
  96. package/src/RedactoPrivacyCenter/lib/utils.ts +626 -0
  97. package/src/RedactoPrivacyCenter/locales/as/translation.json +280 -0
  98. package/src/RedactoPrivacyCenter/locales/bho/translation.json +280 -0
  99. package/src/RedactoPrivacyCenter/locales/bn/translation.json +280 -0
  100. package/src/RedactoPrivacyCenter/locales/brx/translation.json +280 -0
  101. package/src/RedactoPrivacyCenter/locales/doi/translation.json +280 -0
  102. package/src/RedactoPrivacyCenter/locales/en/translation.json +255 -0
  103. package/src/RedactoPrivacyCenter/locales/gom/translation.json +280 -0
  104. package/src/RedactoPrivacyCenter/locales/gu/translation.json +280 -0
  105. package/src/RedactoPrivacyCenter/locales/hi/translation.json +280 -0
  106. package/src/RedactoPrivacyCenter/locales/kn/translation.json +280 -0
  107. package/src/RedactoPrivacyCenter/locales/ks/translation.json +280 -0
  108. package/src/RedactoPrivacyCenter/locales/mai/translation.json +280 -0
  109. package/src/RedactoPrivacyCenter/locales/ml/translation.json +280 -0
  110. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +280 -0
  111. package/src/RedactoPrivacyCenter/locales/mr/translation.json +280 -0
  112. package/src/RedactoPrivacyCenter/locales/ne/translation.json +280 -0
  113. package/src/RedactoPrivacyCenter/locales/or/translation.json +280 -0
  114. package/src/RedactoPrivacyCenter/locales/pa/translation.json +280 -0
  115. package/src/RedactoPrivacyCenter/locales/sa/translation.json +280 -0
  116. package/src/RedactoPrivacyCenter/locales/sat/translation.json +280 -0
  117. package/src/RedactoPrivacyCenter/locales/sd/translation.json +280 -0
  118. package/src/RedactoPrivacyCenter/locales/ta/translation.json +280 -0
  119. package/src/RedactoPrivacyCenter/locales/te/translation.json +280 -0
  120. package/src/RedactoPrivacyCenter/locales/ur/translation.json +280 -0
  121. package/src/RedactoPrivacyCenter/styles/base.css +117 -0
  122. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1725 -0
  123. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +813 -0
  124. package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
  125. package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
  126. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +367 -0
  127. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +476 -0
  128. package/src/RedactoPrivacyCenter/table/index.ts +2 -0
  129. package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
  130. package/src/RedactoPrivacyCenter/ui/Footer.tsx +75 -0
  131. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
  132. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +93 -0
  133. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
  134. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
  135. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
  136. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
  137. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
  138. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +107 -0
  139. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
  140. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +132 -0
  141. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +31 -0
  142. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +106 -0
  143. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +173 -0
  144. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +387 -0
  145. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +149 -0
  146. package/src/RedactoPrivacyCenter/ui/PCText.tsx +41 -0
  147. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
  148. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
  149. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
  150. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
  151. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +53 -0
  152. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +72 -0
  153. package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
  154. package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
  155. package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
  156. package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
  157. package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
  158. package/src/privacy-center.ts +108 -0
  159. package/tsconfig.json +2 -0
  160. package/tsup.config.ts +16 -1
  161. package/.turbo/turbo-build.log +0 -21
@@ -0,0 +1,626 @@
1
+ import { refreshOtpTokenServer } from './api';
2
+ import type { BaseUrlContextValue } from '../context/BaseUrlContext';
3
+
4
+ // Token refresh lock to prevent concurrent refresh attempts
5
+ let isRefreshing = false;
6
+ let refreshPromise: Promise<string | null> | null = null;
7
+
8
+ // Buffer time in seconds before token expiration to proactively refresh
9
+ // Using 30 seconds to ensure tokens are refreshed before they expire
10
+ const TOKEN_EXPIRY_BUFFER_SECONDS = 30;
11
+
12
+ export const getCookieValue = (name: string) => {
13
+ const cookies = document.cookie.split('; ');
14
+ for (let cookie of cookies) {
15
+ const [key, value] = cookie.split('=');
16
+ if (key === name) {
17
+ return value;
18
+ }
19
+ }
20
+ return null;
21
+ };
22
+
23
+ /**
24
+ * Set refresh token cookie with proper attributes
25
+ * @param refreshToken - The refresh token value to set
26
+ */
27
+ export const setRefreshTokenCookie = (refreshToken: string): void => {
28
+ if (typeof window === 'undefined') {
29
+ return;
30
+ }
31
+
32
+ const cookieAttributes = [
33
+ `refresh_token=${refreshToken}`,
34
+ 'path=/privacy-center',
35
+ `max-age=${60 * 60 * 24 * 7}`, // 7 days
36
+ `samesite=lax`,
37
+ ]
38
+ .filter(Boolean)
39
+ .join('; ');
40
+
41
+ document.cookie = cookieAttributes;
42
+ };
43
+
44
+ /**
45
+ * Decode JWT token and extract payload
46
+ * @param token - JWT token string
47
+ * @returns Decoded JWT payload or null if decoding fails
48
+ */
49
+ export const decodeJwtToken = (token: string): { organisation_uuid?: string; organisation_name?: string; workspace_uuid?: string; [key: string]: any } | null => {
50
+ if (!token || typeof token !== 'string') {
51
+ return null;
52
+ }
53
+
54
+ try {
55
+ // JWT format: header.payload.signature
56
+ const parts = token.split('.');
57
+ if (parts.length !== 3) {
58
+ // Malformed JWT
59
+ return null;
60
+ }
61
+
62
+ // Decode payload (second part)
63
+ const payload = parts[1];
64
+
65
+ // Base64URL decode
66
+ // Replace URL-safe characters and add padding if needed
67
+ let base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
68
+ const padding = base64.length % 4;
69
+ if (padding) {
70
+ base64 += '='.repeat(4 - padding);
71
+ }
72
+
73
+ // Decode base64
74
+ const decodedPayload = atob(base64);
75
+ const payloadObj = JSON.parse(decodedPayload) as { organisation_uuid?: string; organisation_name?: string; workspace_uuid?: string; [key: string]: any };
76
+
77
+ return payloadObj;
78
+ } catch (error) {
79
+ // If decoding fails, return null
80
+ console.error('Error decoding JWT:', error);
81
+ return null;
82
+ }
83
+ };
84
+
85
+ /**
86
+ * Decode JWT payload and check if token is expired or will expire soon
87
+ * @param token - JWT token string
88
+ * @param bufferSeconds - Buffer time in seconds before expiration to consider token expired (default: 5 minutes)
89
+ * @returns true if token is expired, invalid, or will expire within buffer time; false if valid
90
+ */
91
+ export const isJwtExpired = (token: string, bufferSeconds: number = 5 * 60): boolean => {
92
+ if (!token || typeof token !== 'string') {
93
+ return true;
94
+ }
95
+
96
+ try {
97
+ // JWT format: header.payload.signature
98
+ const parts = token.split('.');
99
+ if (parts.length !== 3) {
100
+ // Malformed JWT
101
+ return true;
102
+ }
103
+
104
+ // Decode payload (second part)
105
+ const payload = parts[1];
106
+
107
+ // Base64URL decode
108
+ // Replace URL-safe characters and add padding if needed
109
+ let base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
110
+ const padding = base64.length % 4;
111
+ if (padding) {
112
+ base64 += '='.repeat(4 - padding);
113
+ }
114
+
115
+ // Decode base64
116
+ const decodedPayload = atob(base64);
117
+ const payloadObj: { exp?: number } = JSON.parse(decodedPayload) as { exp?: number };
118
+
119
+ // Check if exp claim exists
120
+ if (typeof payloadObj.exp !== 'number') {
121
+ // Missing exp claim - treat as invalid
122
+ return true;
123
+ }
124
+
125
+ // Get current time in seconds
126
+ const now = Math.floor(Date.now() / 1000);
127
+
128
+ // Check if token is expired or will expire within buffer time
129
+ const isExpired = payloadObj.exp <= (now + bufferSeconds);
130
+
131
+ // exp is in seconds since epoch
132
+ return isExpired;
133
+ } catch (error) {
134
+ // If decoding fails, treat as expired/invalid
135
+ console.error('Error decoding JWT:', error);
136
+ return true;
137
+ }
138
+ };
139
+
140
+ /**
141
+ * Check if user is authenticated in privacy center
142
+ * User is considered authenticated if both access_token and email exist in sessionStorage
143
+ */
144
+ export const isUserAuthenticated = (expectedSlug: string): boolean => {
145
+ if (typeof window === 'undefined') return false;
146
+ const accessToken = sessionStorage.getItem('access_token');
147
+ const email = sessionStorage.getItem('email');
148
+ const orgSlug = sessionStorage.getItem('pc_org_slug');
149
+
150
+ // Require that tokens exist and the stored organization slug matches
151
+ if (!accessToken || !email || !orgSlug) {
152
+ return false;
153
+ }
154
+
155
+ return orgSlug === expectedSlug;
156
+ };
157
+
158
+ /**
159
+ * Get initial state for sign-in form visibility
160
+ * Returns true if window is undefined (SSR) or user is not authenticated
161
+ * @param slug - The organization slug to check authentication against
162
+ * @returns true if sign-in form should be shown, false otherwise
163
+ */
164
+ export const getInitialState = (slug: string): boolean => {
165
+ if (typeof window === 'undefined') return true;
166
+ return !isUserAuthenticated(slug);
167
+ };
168
+
169
+ /**
170
+ * Check authentication status and attempt token refresh if needed
171
+ * This function checks if the user is authenticated, validates JWT expiration,
172
+ * and attempts to refresh using the refresh token cookie if the token is expired or missing.
173
+ * @param expectedSlug - The organization slug to verify against
174
+ * @param organizationId - Optional organization ID (will be read from sessionStorage if not provided)
175
+ * @param workspaceId - Optional workspace ID (will be read from sessionStorage if not provided)
176
+ * @returns Promise<boolean> - true if authenticated with valid non-expired token, false otherwise
177
+ */
178
+ export const checkAndRefreshAuth = async (
179
+ expectedSlug: string,
180
+ organizationId?: string,
181
+ workspaceId?: string,
182
+ consentClient?: BaseUrlContextValue['consentClient']
183
+ ): Promise<boolean> => {
184
+ if (typeof window === 'undefined') return false;
185
+
186
+ const accessToken = sessionStorage.getItem('access_token');
187
+ const email = sessionStorage.getItem('email');
188
+ const orgSlug = sessionStorage.getItem('pc_org_slug');
189
+
190
+ // If slug doesn't match, user is not authenticated for this organization
191
+ if (orgSlug && orgSlug !== expectedSlug) {
192
+ return false;
193
+ }
194
+
195
+ // Check if we need to refresh (token missing or expired)
196
+ const needsRefresh = !accessToken || (accessToken && isJwtExpired(accessToken, TOKEN_EXPIRY_BUFFER_SECONDS));
197
+
198
+ // If we have a valid token and all required data, user is authenticated
199
+ if (!needsRefresh && accessToken && email && orgSlug && orgSlug === expectedSlug) {
200
+ return true;
201
+ }
202
+
203
+ // If token is missing or expired, and we have email and orgSlug, try to refresh
204
+ if (needsRefresh && email && orgSlug === expectedSlug) {
205
+ // Get organizationId and workspaceId from sessionStorage if not provided
206
+ const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
207
+ const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
208
+
209
+ if (!orgId || !wsId) {
210
+ return false;
211
+ }
212
+
213
+ try {
214
+ const refreshToken = getCookieValue('refresh_token');
215
+ if (!refreshToken) {
216
+ // No refresh token available - user needs to sign in again
217
+ signOutPrivacyCenter();
218
+ return false;
219
+ }
220
+
221
+ const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
222
+ if (result && result.success !== false) {
223
+ const newAccessToken = (result as { access_token?: string })?.access_token;
224
+ const newRefreshToken = (result as { refresh_token?: string })?.refresh_token;
225
+
226
+ if (newAccessToken) {
227
+ // Validate the new token is not expired before storing
228
+ if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
229
+ console.error('Refreshed access token is already expired - clearing auth state');
230
+ signOutPrivacyCenter();
231
+ return false;
232
+ }
233
+ sessionStorage.setItem('access_token', newAccessToken);
234
+ } else {
235
+ // No access token in response - refresh failed
236
+ console.error('Refresh response missing access token - clearing auth state');
237
+ signOutPrivacyCenter();
238
+ return false;
239
+ }
240
+
241
+ if (newRefreshToken) {
242
+ setRefreshTokenCookie(newRefreshToken);
243
+ }
244
+
245
+ // Verify we now have a valid, non-expired access token
246
+ const isValid = !!newAccessToken && !isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS);
247
+ if (!isValid) {
248
+ console.error('Refreshed token validation failed - clearing auth state');
249
+ signOutPrivacyCenter();
250
+ }
251
+ return isValid;
252
+ } else {
253
+ // Refresh token expired/revoked or server returned failure
254
+ // This is an expected scenario - silently handle it
255
+ signOutPrivacyCenter();
256
+ return false;
257
+ }
258
+ } catch (error: any) {
259
+ // Check if error indicates refresh token is expired/revoked (expected scenario)
260
+ const errorMessage = error?.message || '';
261
+ const isRefreshTokenExpired =
262
+ errorMessage.includes('revoked') ||
263
+ errorMessage.includes('expired') ||
264
+ errorMessage.includes('Refresh token') ||
265
+ errorMessage.includes('Invalid refresh token') ||
266
+ errorMessage.includes('401') ||
267
+ errorMessage.includes('Unauthorized');
268
+
269
+ if (isRefreshTokenExpired) {
270
+ // Refresh token is expired/revoked - silently handle (expected scenario)
271
+ signOutPrivacyCenter();
272
+ return false;
273
+ }
274
+
275
+ // For other errors (network, etc.), log and don't clear auth state - might be temporary
276
+ console.error('[Token Refresh] Unexpected error refreshing token:', error);
277
+ return false;
278
+ }
279
+ }
280
+
281
+ return false;
282
+ };
283
+
284
+ /**
285
+ * Compare two email addresses (case-insensitive, handles URL encoding)
286
+ * @param email1 - First email (can be URL-encoded)
287
+ * @param email2 - Second email
288
+ * @returns true if emails match (case-insensitive), false otherwise
289
+ */
290
+ export const compareEmails = (email1: string | null, email2: string | null): boolean => {
291
+ if (!email1 || !email2) {
292
+ return false;
293
+ }
294
+
295
+ // Decode URL-encoded email (handles %40 for @, %2B for +, etc.)
296
+ const decodedEmail1 = decodeURIComponent(email1.trim());
297
+ const normalizedEmail1 = decodedEmail1.toLowerCase();
298
+ const normalizedEmail2 = email2.trim().toLowerCase();
299
+
300
+ return normalizedEmail1 === normalizedEmail2;
301
+ };
302
+
303
+ /**
304
+ * Sign out from privacy center only
305
+ * Clears privacy-center specific storage and cookies without touching Supabase auth
306
+ */
307
+ export const signOutPrivacyCenter = (): void => {
308
+ if (typeof window === 'undefined') return;
309
+
310
+ const privacyCenterKeys = [
311
+ 'access_token',
312
+ 'email',
313
+ 'i18nextLng',
314
+ 'lang',
315
+ 'pc_org_slug',
316
+ 'pc_org_id',
317
+ 'pc_workspace_id',
318
+ 'pc_current_page',
319
+ 'pc_grievance_view',
320
+ 'pc_selected_case_uuid',
321
+ ];
322
+ privacyCenterKeys.forEach((key) => {
323
+ sessionStorage.removeItem(key);
324
+ });
325
+
326
+ // Clear refresh token cookie set for privacy center
327
+ document.cookie =
328
+ 'refresh_token=; path=/privacy-center; expires=Thu, 01 Jan 1970 00:00:00 GMT';
329
+ // Fallback in case the cookie path differs
330
+ document.cookie =
331
+ 'refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
332
+ };
333
+
334
+ /**
335
+ * Ensure a valid access token is available before making API calls.
336
+ * This function checks if the current token is valid (not expired).
337
+ * If expired or missing, it attempts to refresh using the refresh token.
338
+ * If refresh fails (refresh token expired), it signs out and redirects to sign-in.
339
+ *
340
+ * @param organizationId - The organization UUID
341
+ * @param workspaceId - The workspace UUID
342
+ * @param slug - The organization slug for redirect
343
+ * @param authContext - Optional auth context values (accessToken, refreshToken, onError, updateTokens)
344
+ * @returns Promise<string | null> - Valid access token or null if auth failed
345
+ */
346
+ export const ensureValidToken = async (
347
+ organizationId?: string,
348
+ workspaceId?: string,
349
+ slug?: string,
350
+ authContext?: {
351
+ accessToken?: string;
352
+ refreshToken?: string;
353
+ onError?: (error: Error) => boolean;
354
+ updateTokens?: (accessToken: string, refreshToken?: string) => void;
355
+ },
356
+ consentClient?: BaseUrlContextValue['consentClient']
357
+ ): Promise<string | null> => {
358
+ if (typeof window === 'undefined') return null;
359
+
360
+ // Use context accessToken if provided, otherwise fall back to sessionStorage
361
+ const accessToken = authContext?.accessToken || sessionStorage.getItem('access_token');
362
+
363
+ // Check if current token is valid (exists, is a string, and not expired)
364
+ // Use a buffer to proactively refresh before actual expiry
365
+ if (
366
+ accessToken &&
367
+ typeof accessToken === 'string' &&
368
+ accessToken.trim() !== '' &&
369
+ !isJwtExpired(accessToken, TOKEN_EXPIRY_BUFFER_SECONDS)
370
+ ) {
371
+ return accessToken;
372
+ }
373
+
374
+ // Token is missing or expired, need to refresh
375
+ // Use lock to prevent concurrent refresh attempts
376
+ if (isRefreshing && refreshPromise) {
377
+ // Wait for ongoing refresh to complete
378
+ const newToken = await refreshPromise;
379
+ // Return the new token directly (it's already stored in sessionStorage/context)
380
+ return newToken;
381
+ }
382
+
383
+ // Start refresh process
384
+ isRefreshing = true;
385
+ refreshPromise = performTokenRefresh(organizationId, workspaceId, slug, authContext, consentClient);
386
+
387
+ try {
388
+ const newToken = await refreshPromise;
389
+ // Return the new token directly (it's already stored in sessionStorage/context)
390
+ return newToken;
391
+ } finally {
392
+ isRefreshing = false;
393
+ refreshPromise = null;
394
+ }
395
+ };
396
+
397
+ /**
398
+ * Internal function to perform token refresh
399
+ * @returns Promise<string | null> - The new access token on success, null on failure
400
+ */
401
+ const performTokenRefresh = async (
402
+ organizationId?: string,
403
+ workspaceId?: string,
404
+ slug?: string,
405
+ authContext?: {
406
+ accessToken?: string;
407
+ refreshToken?: string;
408
+ onError?: (error: Error) => boolean;
409
+ updateTokens?: (accessToken: string, refreshToken?: string) => void;
410
+ },
411
+ consentClient?: BaseUrlContextValue['consentClient']
412
+ ): Promise<string | null> => {
413
+ // Get organizationId and workspaceId from sessionStorage if not provided
414
+ const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
415
+ const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
416
+ const orgSlug = slug || sessionStorage.getItem('pc_org_slug') || '';
417
+
418
+ if (!orgId || !wsId) {
419
+ console.error('[Token Refresh] Missing organizationId or workspaceId');
420
+ const error = new Error('Missing organizationId or workspaceId');
421
+ // Call onError if provided, check return value before proceeding
422
+ if (authContext?.onError) {
423
+ const shouldProceed = authContext.onError(error);
424
+ if (!shouldProceed) {
425
+ return null;
426
+ }
427
+ }
428
+ signOutPrivacyCenter();
429
+ redirectToSignIn(orgSlug, authContext?.onError);
430
+ return null;
431
+ }
432
+
433
+ // Use context refreshToken if provided, otherwise fall back to cookie
434
+ // If authContext is provided, prioritize its refreshToken (props mode)
435
+ // Otherwise, fall back to cookie (legacy mode)
436
+ let refreshTokenValue: string | null = null;
437
+
438
+ if (authContext) {
439
+ // Props mode: use refreshToken from context
440
+ refreshTokenValue = authContext.refreshToken || null;
441
+ } else {
442
+ // Legacy mode: use refreshToken from cookie
443
+ refreshTokenValue = getCookieValue('refresh_token');
444
+ }
445
+
446
+ // Validate refreshToken is a non-empty string
447
+ if (!refreshTokenValue || typeof refreshTokenValue !== 'string' || refreshTokenValue.trim() === '') {
448
+ // No refresh token available - user needs to sign in again
449
+ console.warn('[Token Refresh] No valid refresh token available', {
450
+ hasAuthContext: !!authContext,
451
+ refreshTokenFromContext: authContext?.refreshToken,
452
+ refreshTokenFromCookie: getCookieValue('refresh_token'),
453
+ });
454
+ const error = new Error('No refresh token available');
455
+ // Call onError if provided, check return value before proceeding
456
+ if (authContext?.onError) {
457
+ const shouldProceed = authContext.onError(error);
458
+ if (!shouldProceed) {
459
+ return null;
460
+ }
461
+ }
462
+ signOutPrivacyCenter();
463
+ redirectToSignIn(orgSlug, authContext?.onError);
464
+ return null;
465
+ }
466
+
467
+ // At this point, refreshTokenValue is guaranteed to be a non-empty string
468
+ const refreshToken: string = refreshTokenValue;
469
+
470
+ try {
471
+ const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
472
+
473
+ if (result && result.success !== false) {
474
+ const newAccessToken = (result as { access_token?: string })?.access_token;
475
+ const newRefreshToken = (result as { refresh_token?: string })?.refresh_token;
476
+
477
+ if (newAccessToken) {
478
+ // Validate the new token is not expired before storing
479
+ if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
480
+ console.error('[Token Refresh] New access token is already expired');
481
+ const error = new Error('New access token is already expired');
482
+ // Call onError if provided, check return value before proceeding
483
+ if (authContext?.onError) {
484
+ const shouldProceed = authContext.onError(error);
485
+ if (!shouldProceed) {
486
+ return null;
487
+ }
488
+ }
489
+ signOutPrivacyCenter();
490
+ redirectToSignIn(orgSlug, authContext?.onError);
491
+ return null;
492
+ }
493
+
494
+ // Update tokens via context if available, otherwise use storage
495
+ if (authContext?.updateTokens) {
496
+ authContext.updateTokens(newAccessToken, newRefreshToken);
497
+ } else {
498
+ sessionStorage.setItem('access_token', newAccessToken);
499
+ if (newRefreshToken) {
500
+ setRefreshTokenCookie(newRefreshToken);
501
+ }
502
+ }
503
+
504
+ // Return the new access token directly (already stored in sessionStorage/context)
505
+ return newAccessToken;
506
+ } else {
507
+ // No access token in response - refresh failed
508
+ console.error('[Token Refresh] Response missing access token');
509
+ const error = new Error('Response missing access token');
510
+ // Call onError if provided, check return value before proceeding
511
+ if (authContext?.onError) {
512
+ const shouldProceed = authContext.onError(error);
513
+ if (!shouldProceed) {
514
+ return null;
515
+ }
516
+ }
517
+ signOutPrivacyCenter();
518
+ redirectToSignIn(orgSlug, authContext?.onError);
519
+ return null;
520
+ }
521
+ } else {
522
+ // Refresh token expired/revoked or server returned failure
523
+ const error = new Error('Refresh token expired or revoked');
524
+ // Call onError if provided, check return value before proceeding
525
+ if (authContext?.onError) {
526
+ const shouldProceed = authContext.onError(error);
527
+ if (!shouldProceed) {
528
+ return null;
529
+ }
530
+ }
531
+ signOutPrivacyCenter();
532
+ redirectToSignIn(orgSlug, authContext?.onError);
533
+ return null;
534
+ }
535
+ } catch (error: any) {
536
+ const errorMessage = error?.message || '';
537
+ const isRefreshTokenExpired =
538
+ errorMessage.includes('revoked') ||
539
+ errorMessage.includes('expired') ||
540
+ errorMessage.includes('Refresh token') ||
541
+ errorMessage.includes('Invalid refresh token') ||
542
+ errorMessage.includes('401') ||
543
+ errorMessage.includes('Unauthorized');
544
+
545
+ if (isRefreshTokenExpired) {
546
+ // Refresh token is expired/revoked - expected scenario
547
+ const err = new Error('Refresh token expired or revoked');
548
+ // Call onError if provided, check return value before proceeding
549
+ if (authContext?.onError) {
550
+ const shouldProceed = authContext.onError(err);
551
+ if (!shouldProceed) {
552
+ return null;
553
+ }
554
+ }
555
+ signOutPrivacyCenter();
556
+ redirectToSignIn(orgSlug, authContext?.onError);
557
+ return null;
558
+ }
559
+
560
+ // For other errors (network, etc.), log but don't clear auth state
561
+ console.error('[Token Refresh] Unexpected error:', error);
562
+ // Still call onError for unexpected errors
563
+ if (authContext?.onError) {
564
+ const err = error instanceof Error ? error : new Error(String(error));
565
+ authContext.onError(err);
566
+ }
567
+ return null;
568
+ }
569
+ };
570
+
571
+ /**
572
+ * Helper to redirect to sign-in page
573
+ */
574
+ const redirectToSignIn = (slug?: string, onError?: (error: Error) => boolean): void => {
575
+ if (typeof window === 'undefined') return;
576
+
577
+ // If onError is provided, call it and check return value
578
+ if (onError) {
579
+ const error = new Error('Authentication failed - redirecting to sign-in');
580
+ const shouldProceed = onError(error);
581
+ if (!shouldProceed) {
582
+ // User wants to handle navigation themselves
583
+ return;
584
+ }
585
+ }
586
+
587
+ const orgSlug = slug || sessionStorage.getItem('pc_org_slug') || '';
588
+ if (orgSlug) {
589
+ window.location.href = `/privacy-center/${orgSlug}/sign-in`;
590
+ } else {
591
+ window.location.href = '/privacy-center';
592
+ }
593
+ };
594
+
595
+ // Activity helper functions
596
+
597
+ export type ActivityBadgeStatus =
598
+ | 'success'
599
+ | 'warning'
600
+ | 'error'
601
+ | 'info'
602
+ | 'pending';
603
+
604
+ /**
605
+ * Get badge status based on activity type
606
+ */
607
+ export const getActivityTypeStatus = (
608
+ activityType: string
609
+ ): ActivityBadgeStatus => {
610
+ const type = activityType.toLowerCase();
611
+ if (type.includes('consent') || type.includes('grant')) return 'success';
612
+ if (type.includes('revoke') || type.includes('withdraw')) return 'error';
613
+ if (type.includes('update') || type.includes('modify')) return 'warning';
614
+ if (type.includes('view') || type.includes('access')) return 'info';
615
+ return 'pending';
616
+ };
617
+
618
+ /**
619
+ * Format activity type for display
620
+ */
621
+ export const formatActivityType = (activityType: string): string => {
622
+ return activityType
623
+ .split('_')
624
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
625
+ .join(' ');
626
+ };