@redacto.io/consent-sdk-react 9.3.0 → 9.4.0-beta.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 (48) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +93 -12
  4. package/dist/index.d.mts +39 -1
  5. package/dist/index.d.ts +39 -1
  6. package/dist/index.js +4454 -334
  7. package/dist/index.mjs +4454 -335
  8. package/dist/privacy-center.d.mts +4 -1
  9. package/dist/privacy-center.d.ts +4 -1
  10. package/dist/privacy-center.js +3905 -3660
  11. package/dist/privacy-center.mjs +4172 -3921
  12. package/package.json +1 -1
  13. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +847 -0
  14. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +2133 -0
  15. package/src/RedactoNoticeAssisted/api/index.ts +269 -0
  16. package/src/RedactoNoticeAssisted/api/types.ts +180 -0
  17. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  18. package/src/RedactoNoticeAssisted/constants.ts +51 -0
  19. package/src/RedactoNoticeAssisted/i18n.ts +1463 -0
  20. package/src/RedactoNoticeAssisted/index.ts +1 -0
  21. package/src/RedactoNoticeAssisted/injectStyles.ts +251 -0
  22. package/src/RedactoNoticeAssisted/styles.ts +730 -0
  23. package/src/RedactoNoticeAssisted/types.ts +24 -0
  24. package/src/RedactoNoticeAssisted/useMediaQuery.ts +36 -0
  25. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +8 -0
  26. package/src/RedactoPrivacyCenter/api/actions.ts +6 -11
  27. package/src/RedactoPrivacyCenter/api/client.ts +15 -3
  28. package/src/RedactoPrivacyCenter/api/fetcher.ts +80 -1
  29. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +193 -213
  30. package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +239 -0
  31. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +3 -1
  32. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +8 -1
  33. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +23 -17
  34. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +24 -10
  35. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +16 -3
  36. package/src/RedactoPrivacyCenter/components/constant.ts +25 -0
  37. package/src/RedactoPrivacyCenter/components/types.ts +32 -1
  38. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +25 -23
  39. package/src/RedactoPrivacyCenter/lib/constants.ts +23 -10
  40. package/src/RedactoPrivacyCenter/lib/queries.ts +9 -2
  41. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +127 -0
  42. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +85 -0
  43. package/src/RedactoPrivacyCenter/lib/utils.ts +213 -63
  44. package/src/RedactoPrivacyCenter/locales/en/translation.json +3 -0
  45. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +16 -0
  46. package/src/index.ts +2 -0
  47. package/src/shared/api-errors.ts +9 -0
  48. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +0 -141
@@ -1,10 +1,33 @@
1
1
  import { refreshOtpTokenServer } from './api';
2
+ import { isTerminalRefreshError } from '../api/fetcher';
3
+ import {
4
+ MIN_REFRESH_BACKOFF_MS,
5
+ MAX_REFRESH_BACKOFF_MS,
6
+ REFRESH_TOKEN_STORAGE_KEY,
7
+ LEGACY_REFRESH_TOKEN_COOKIE,
8
+ } from './constants';
2
9
  import type { BaseUrlContextValue } from '../context/BaseUrlContext';
3
10
 
4
11
  // Token refresh lock to prevent concurrent refresh attempts
5
12
  let isRefreshing = false;
6
13
  let refreshPromise: Promise<string | null> | null = null;
7
14
 
15
+ // ─── OTP refresh circuit breaker (incident 2026-06-22) ──────────────────────
16
+ // ensureValidToken is called from 3 independent 30s pollers, before every
17
+ // authenticated API call, and from render-driven effects. Without a memory of
18
+ // failure, a single dead refresh token gets re-submitted on every one of those
19
+ // triggers, hammering POST /otp/refresh until the tab closes. These guards make a
20
+ // terminally-failed token cost exactly one request, and apply backoff to transient
21
+ // failures.
22
+ //
23
+ // `deadRefreshToken` holds the *value* of a refresh token that terminally failed.
24
+ // It auto-clears the moment a different token value appears (i.e. the user
25
+ // re-authenticated), so the breaker never permanently bricks a session.
26
+ let deadRefreshToken: string | null = null;
27
+ // Transient-failure backoff window (epoch ms) and the current backoff duration.
28
+ let transientRefreshFailureUntil = 0;
29
+ let transientRefreshBackoffMs = 0;
30
+
8
31
  // Buffer time in seconds before token expiration to proactively refresh
9
32
  // Using 30 seconds to ensure tokens are refreshed before they expire
10
33
  const TOKEN_EXPIRY_BUFFER_SECONDS = 30;
@@ -20,25 +43,140 @@ export const getCookieValue = (name: string) => {
20
43
  return null;
21
44
  };
22
45
 
46
+ // ─── Refresh token persistence ──────────────────────────────────────────────
47
+ // The refresh token lives in localStorage (key REFRESH_TOKEN_STORAGE_KEY), not a
48
+ // cookie. A path-scoped cookie is not readable when the host mounts the privacy
49
+ // center under a basePath (e.g. consent-app at /consent → /consent/privacy-center/...),
50
+ // so a rotated token can't be read back and the stale token gets re-presented,
51
+ // causing a 401 "Refresh token not found or already used" storm. localStorage is
52
+ // origin-scoped (no path matching), so it is readable under any basePath.
53
+ //
54
+ // localStorage access throws when storage is disabled or over quota (private
55
+ // modes, embedded webviews). Guard every access so the SDK degrades to "refresh
56
+ // fails -> re-auth" instead of crashing the render tree (setRefreshTokenCookie
57
+ // runs during AuthProvider's render). Mirrors components/AppShell/hooks/useSidebarState.ts.
58
+ const readStorage = (key: string): string | null => {
59
+ try {
60
+ return window.localStorage.getItem(key);
61
+ } catch {
62
+ return null;
63
+ }
64
+ };
65
+
66
+ const writeStorage = (key: string, value: string): void => {
67
+ try {
68
+ window.localStorage.setItem(key, value);
69
+ } catch {
70
+ // localStorage unavailable / quota exceeded — ignore
71
+ }
72
+ };
73
+
74
+ const removeStorage = (key: string): void => {
75
+ try {
76
+ window.localStorage.removeItem(key);
77
+ } catch {
78
+ // localStorage unavailable — ignore
79
+ }
80
+ };
81
+
23
82
  /**
24
- * Set refresh token cookie with proper attributes
83
+ * Expire the SDK's own legacy refresh_token cookie (older versions wrote it at
84
+ * path=/privacy-center). Scoped to that path only on purpose: a refresh_token
85
+ * cookie at path=/ belongs to the host app, and clearing it would sign the host
86
+ * out.
87
+ */
88
+ const clearLegacyRefreshTokenCookie = (): void => {
89
+ if (typeof window === 'undefined') {
90
+ return;
91
+ }
92
+ document.cookie = `${LEGACY_REFRESH_TOKEN_COOKIE}=; path=/privacy-center; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
93
+ };
94
+
95
+ /**
96
+ * Persist the refresh token. Named `setRefreshTokenCookie` for backward
97
+ * compatibility (it is part of the package's exported surface); it now writes
98
+ * to localStorage rather than a cookie, and clears any cookie an older SDK
99
+ * version left at path=/privacy-center.
25
100
  * @param refreshToken - The refresh token value to set
26
101
  */
27
102
  export const setRefreshTokenCookie = (refreshToken: string): void => {
28
103
  if (typeof window === 'undefined') {
29
104
  return;
30
105
  }
106
+ writeStorage(REFRESH_TOKEN_STORAGE_KEY, refreshToken);
107
+ clearLegacyRefreshTokenCookie();
108
+ };
109
+
110
+ /**
111
+ * Read the persisted refresh token from localStorage.
112
+ *
113
+ * Intentionally does NOT fall back to reading a `refresh_token` cookie: a cookie
114
+ * read is name-only (the path isn't visible to JS), so it cannot tell the SDK's
115
+ * own legacy cookie from a host app's unrelated `refresh_token` cookie — reading
116
+ * the latter would leak a host credential to the OTP refresh endpoint. Existing
117
+ * sessions are preserved by AuthProvider re-seeding storage from the token prop.
118
+ */
119
+ export const getStoredRefreshToken = (): string | null => {
120
+ if (typeof window === 'undefined') {
121
+ return null;
122
+ }
123
+ return readStorage(REFRESH_TOKEN_STORAGE_KEY);
124
+ };
125
+
126
+ /**
127
+ * Resolve the refresh token currently in play: from auth context (props mode) or
128
+ * persisted storage (legacy mode). Centralised so the circuit breaker and the
129
+ * refresh routine always agree on exactly which token value is being used.
130
+ */
131
+ const getActiveRefreshToken = (authContext?: { refreshToken?: string }): string | null => {
132
+ if (authContext) {
133
+ return authContext.refreshToken && authContext.refreshToken.trim() !== ''
134
+ ? authContext.refreshToken
135
+ : null;
136
+ }
137
+ if (typeof window === 'undefined') return null;
138
+ return getStoredRefreshToken();
139
+ };
140
+
141
+ /**
142
+ * Clear all OTP-refresh circuit-breaker state. Called after a successful refresh
143
+ * and exposed for tests. Deliberately NOT called from signOutPrivacyCenter: in
144
+ * props mode the refresh token lives in a prop that sign-out cannot clear, so
145
+ * clearing the breaker there would immediately re-arm the retry storm. Recovery
146
+ * instead happens automatically when a *different* token value appears.
147
+ */
148
+ export const resetRefreshCircuit = (): void => {
149
+ deadRefreshToken = null;
150
+ transientRefreshFailureUntil = 0;
151
+ transientRefreshBackoffMs = 0;
152
+ };
153
+
154
+ /**
155
+ * True when attempting a refresh right now would be pointless — the active refresh
156
+ * token has already terminally failed, or we're inside the transient backoff
157
+ * window. Pollers consult this to avoid scheduling doomed work.
158
+ */
159
+ export const isRefreshBlocked = (authContext?: { refreshToken?: string }): boolean => {
160
+ const active = getActiveRefreshToken(authContext);
161
+ if (active !== null && active === deadRefreshToken) return true;
162
+ if (Date.now() < transientRefreshFailureUntil) return true;
163
+ return false;
164
+ };
31
165
 
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('; ');
166
+ /** Record a transient refresh failure (network / 5xx) and grow the backoff window. */
167
+ const noteTransientRefreshFailure = (): void => {
168
+ transientRefreshBackoffMs = Math.min(
169
+ Math.max(transientRefreshBackoffMs * 2, MIN_REFRESH_BACKOFF_MS),
170
+ MAX_REFRESH_BACKOFF_MS,
171
+ );
172
+ transientRefreshFailureUntil = Date.now() + transientRefreshBackoffMs;
173
+ };
40
174
 
41
- document.cookie = cookieAttributes;
175
+ /** Trip the circuit breaker for a refresh token value that terminally failed. */
176
+ const markRefreshTokenDead = (token: string | null): void => {
177
+ if (token) {
178
+ deadRefreshToken = token;
179
+ }
42
180
  };
43
181
 
44
182
  /**
@@ -210,14 +348,23 @@ export const checkAndRefreshAuth = async (
210
348
  return false;
211
349
  }
212
350
 
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
- }
351
+ const refreshToken = getStoredRefreshToken();
352
+ if (!refreshToken) {
353
+ // No refresh token available - user needs to sign in again
354
+ signOutPrivacyCenter();
355
+ return false;
356
+ }
357
+
358
+ // Circuit breaker (incident 2026-06-22): a new token re-arms; a known-dead
359
+ // token or an open backoff window short-circuits without hitting the network.
360
+ if (deadRefreshToken !== null && refreshToken !== deadRefreshToken) {
361
+ resetRefreshCircuit();
362
+ }
363
+ if (refreshToken === deadRefreshToken || Date.now() < transientRefreshFailureUntil) {
364
+ return false;
365
+ }
220
366
 
367
+ try {
221
368
  const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
222
369
  if (result && result.success !== false) {
223
370
  const newAccessToken = (result as { access_token?: string })?.access_token;
@@ -227,13 +374,17 @@ export const checkAndRefreshAuth = async (
227
374
  // Validate the new token is not expired before storing
228
375
  if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
229
376
  console.error('Refreshed access token is already expired - clearing auth state');
377
+ markRefreshTokenDead(refreshToken);
230
378
  signOutPrivacyCenter();
231
379
  return false;
232
380
  }
233
381
  sessionStorage.setItem('access_token', newAccessToken);
382
+ // Success — re-arm the circuit breaker.
383
+ resetRefreshCircuit();
234
384
  } else {
235
385
  // No access token in response - refresh failed
236
386
  console.error('Refresh response missing access token - clearing auth state');
387
+ markRefreshTokenDead(refreshToken);
237
388
  signOutPrivacyCenter();
238
389
  return false;
239
390
  }
@@ -252,27 +403,20 @@ export const checkAndRefreshAuth = async (
252
403
  } else {
253
404
  // Refresh token expired/revoked or server returned failure
254
405
  // This is an expected scenario - silently handle it
406
+ markRefreshTokenDead(refreshToken);
255
407
  signOutPrivacyCenter();
256
408
  return false;
257
409
  }
258
410
  } 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)
411
+ if (isTerminalRefreshError(error)) {
412
+ // Refresh token is expired / not found / already used - trip the breaker.
413
+ markRefreshTokenDead(refreshToken);
271
414
  signOutPrivacyCenter();
272
415
  return false;
273
416
  }
274
417
 
275
- // For other errors (network, etc.), log and don't clear auth state - might be temporary
418
+ // Transient (network / 5xx): keep auth state, back off, retry on a later call.
419
+ noteTransientRefreshFailure();
276
420
  console.error('[Token Refresh] Unexpected error refreshing token:', error);
277
421
  return false;
278
422
  }
@@ -323,12 +467,10 @@ export const signOutPrivacyCenter = (): void => {
323
467
  sessionStorage.removeItem(key);
324
468
  });
325
469
 
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';
470
+ // Clear the persisted refresh token (and the SDK's own legacy cookie from
471
+ // older versions). The host app's path=/ cookie is intentionally left alone.
472
+ removeStorage(REFRESH_TOKEN_STORAGE_KEY);
473
+ clearLegacyRefreshTokenCookie();
332
474
  };
333
475
 
334
476
  /**
@@ -371,6 +513,22 @@ export const ensureValidToken = async (
371
513
  return accessToken;
372
514
  }
373
515
 
516
+ // Circuit breaker: before any network work, consult the failure memory.
517
+ // (Incident 2026-06-22 — stops the /otp/refresh retry storm at its source.)
518
+ const activeRefreshToken = getActiveRefreshToken(authContext);
519
+ // A new refresh token value means the user re-authenticated — re-arm the breaker.
520
+ if (deadRefreshToken !== null && activeRefreshToken !== deadRefreshToken) {
521
+ resetRefreshCircuit();
522
+ }
523
+ // Known-dead token: do not touch the network until a different token appears.
524
+ if (activeRefreshToken !== null && activeRefreshToken === deadRefreshToken) {
525
+ return null;
526
+ }
527
+ // Transient-failure backoff window still open: skip the network.
528
+ if (Date.now() < transientRefreshFailureUntil) {
529
+ return null;
530
+ }
531
+
374
532
  // Token is missing or expired, need to refresh
375
533
  // Use lock to prevent concurrent refresh attempts
376
534
  if (isRefreshing && refreshPromise) {
@@ -430,26 +588,19 @@ const performTokenRefresh = async (
430
588
  return null;
431
589
  }
432
590
 
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
- }
591
+ // Resolve the refresh token (props mode → context, legacy mode → storage) via
592
+ // the shared helper so the value matches what the circuit breaker keyed on.
593
+ const refreshTokenValue: string | null = getActiveRefreshToken(authContext);
445
594
 
446
595
  // Validate refreshToken is a non-empty string
447
596
  if (!refreshTokenValue || typeof refreshTokenValue !== 'string' || refreshTokenValue.trim() === '') {
448
597
  // No refresh token available - user needs to sign in again
598
+ // Log presence only — never the raw token, which would leak a credential to
599
+ // browser log collectors.
449
600
  console.warn('[Token Refresh] No valid refresh token available', {
450
601
  hasAuthContext: !!authContext,
451
- refreshTokenFromContext: authContext?.refreshToken,
452
- refreshTokenFromCookie: getCookieValue('refresh_token'),
602
+ hasRefreshTokenInContext: !!authContext?.refreshToken?.trim(),
603
+ hasRefreshTokenInStorage: !!getStoredRefreshToken(),
453
604
  });
454
605
  const error = new Error('No refresh token available');
455
606
  // Call onError if provided, check return value before proceeding
@@ -478,6 +629,7 @@ const performTokenRefresh = async (
478
629
  // Validate the new token is not expired before storing
479
630
  if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
480
631
  console.error('[Token Refresh] New access token is already expired');
632
+ markRefreshTokenDead(refreshToken);
481
633
  const error = new Error('New access token is already expired');
482
634
  // Call onError if provided, check return value before proceeding
483
635
  if (authContext?.onError) {
@@ -501,11 +653,14 @@ const performTokenRefresh = async (
501
653
  }
502
654
  }
503
655
 
656
+ // Success — re-arm the circuit breaker so future failures are tracked afresh.
657
+ resetRefreshCircuit();
504
658
  // Return the new access token directly (already stored in sessionStorage/context)
505
659
  return newAccessToken;
506
660
  } else {
507
661
  // No access token in response - refresh failed
508
662
  console.error('[Token Refresh] Response missing access token');
663
+ markRefreshTokenDead(refreshToken);
509
664
  const error = new Error('Response missing access token');
510
665
  // Call onError if provided, check return value before proceeding
511
666
  if (authContext?.onError) {
@@ -520,6 +675,7 @@ const performTokenRefresh = async (
520
675
  }
521
676
  } else {
522
677
  // Refresh token expired/revoked or server returned failure
678
+ markRefreshTokenDead(refreshToken);
523
679
  const error = new Error('Refresh token expired or revoked');
524
680
  // Call onError if provided, check return value before proceeding
525
681
  if (authContext?.onError) {
@@ -533,19 +689,12 @@ const performTokenRefresh = async (
533
689
  return null;
534
690
  }
535
691
  } 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
692
+ if (isTerminalRefreshError(error)) {
693
+ // Terminal: refresh token expired / not found / already used / 4xx. Trip the
694
+ // breaker so this dead token is never re-submitted (onError controls
695
+ // navigation only — the breaker has already tripped).
696
+ markRefreshTokenDead(refreshToken);
547
697
  const err = new Error('Refresh token expired or revoked');
548
- // Call onError if provided, check return value before proceeding
549
698
  if (authContext?.onError) {
550
699
  const shouldProceed = authContext.onError(err);
551
700
  if (!shouldProceed) {
@@ -557,7 +706,8 @@ const performTokenRefresh = async (
557
706
  return null;
558
707
  }
559
708
 
560
- // For other errors (network, etc.), log but don't clear auth state
709
+ // Transient (network / 5xx): keep auth state, back off, and let a later call retry.
710
+ noteTransientRefreshFailure();
561
711
  console.error('[Token Refresh] Unexpected error:', error);
562
712
  // Still call onError for unexpected errors
563
713
  if (authContext?.onError) {
@@ -138,6 +138,9 @@
138
138
  "activities": "Activities",
139
139
  "activitiesSubtitle": "View your privacy center activity history",
140
140
  "activity": "Activity",
141
+ "allCaughtUp": "You're all caught up",
142
+ "noActivitiesYet": "No activity yet",
143
+ "noActivitiesDescription": "Your privacy center activity will appear here as it happens.",
141
144
  "myReceipt": "My Receipt",
142
145
  "myReceiptSubtitle": "View your consent receipts",
143
146
  "event": "Event",
@@ -386,6 +386,22 @@ export const injectPCStyles = () => {
386
386
  transition: max-height 0.3s ease, padding 0.3s ease !important;
387
387
  }
388
388
 
389
+ /* Activity Timeline */
390
+ .pc-timeline-content {
391
+ transition: background-color var(--pc-transition-fast) !important;
392
+ }
393
+ .pc-timeline-row:hover .pc-timeline-content {
394
+ background-color: var(--pc-bg-secondary) !important;
395
+ }
396
+ .pc-timeline-item {
397
+ animation: pc-fadeIn 0.2s ease both;
398
+ }
399
+ @media (prefers-reduced-motion: reduce) {
400
+ .pc-timeline-item {
401
+ animation: none !important;
402
+ }
403
+ }
404
+
389
405
  /* Scrollbar Styles (for better UX) */
390
406
  .pc-scrollbar::-webkit-scrollbar {
391
407
  width: 8px;
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { default as RedactoNoticeConsent } from "./RedactoNoticeConsent";
2
2
  export { default as RedactoNoticeConsentInline } from "./RedactoNoticeConsentInline";
3
+ export { default as RedactoNoticeAssisted } from "./RedactoNoticeAssisted";
4
+ export type { RedactoNoticeAssistedProps } from "./RedactoNoticeAssisted/RedactoNoticeAssisted";
3
5
 
4
6
  // Guardian Verification Components
5
7
  export { DigiLockerCallback } from "./RedactoNoticeConsent/components/DigiLockerCallback";
@@ -52,6 +52,10 @@ interface ApiErrorDetail {
52
52
  interface ApiErrorBody {
53
53
  message?: string;
54
54
  error_code?: string | null;
55
+ // Some endpoints (e.g. OTP verify) signal "this code is dead, request a fresh
56
+ // one" via a top-level boolean instead of an `error_code`. Captured so callers
57
+ // can branch on it (e.g. to distinguish an expired code from a wrong one).
58
+ should_request_new?: boolean;
55
59
  // `detail` may arrive as:
56
60
  // { message: "...", error_code: "..." } -- our MessageResponse via Ninja
57
61
  // { detail: "..." } -- Ninja auto-401
@@ -69,16 +73,20 @@ export type ApiErrorCodeLike = ApiErrorCode | (string & {});
69
73
  export class ApiError extends Error {
70
74
  readonly status: number;
71
75
  readonly code?: ApiErrorCodeLike;
76
+ // Mirrors the server's `should_request_new` flag, when present.
77
+ readonly shouldRequestNew?: boolean;
72
78
 
73
79
  constructor(opts: {
74
80
  status: number;
75
81
  code?: ApiErrorCodeLike;
76
82
  message: string;
83
+ shouldRequestNew?: boolean;
77
84
  }) {
78
85
  super(opts.message);
79
86
  this.name = "ApiError";
80
87
  this.status = opts.status;
81
88
  this.code = opts.code;
89
+ this.shouldRequestNew = opts.shouldRequestNew;
82
90
  }
83
91
  }
84
92
 
@@ -167,5 +175,6 @@ export async function parseApiError(
167
175
  status: response.status,
168
176
  code,
169
177
  message,
178
+ shouldRequestNew: body?.should_request_new,
170
179
  });
171
180
  }
@@ -1,141 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { createColumnHelper } from '@tanstack/react-table';
4
- import { PCText, PCTooltip } from '../ui';
5
- import { pcStyles } from '../styles/pcStyles';
6
- import dayjs from 'dayjs';
7
- import relativeTime from 'dayjs/plugin/relativeTime';
8
- import type { Activity } from '../lib/types';
9
- import StatusBadge from '../ui/StatusBadge';
10
- import { getActivityTypeStatus, formatActivityType } from '../lib/utils';
11
- import {
12
- ACTIVITY_COLUMN_SIZES,
13
- ACTIVITY_DESCRIPTION_TRUNCATE_LENGTH,
14
- ACTIVITY_DATE_FORMATS,
15
- } from '../lib/constants';
16
-
17
- dayjs.extend(relativeTime);
18
-
19
- const columnHelper = createColumnHelper<Activity>();
20
-
21
- export const useActivitiesColumns = () => {
22
- const { t } = useTranslation();
23
- return useMemo(
24
- () => [
25
- columnHelper.accessor('title', {
26
- id: 'title',
27
- minSize: ACTIVITY_COLUMN_SIZES.TITLE,
28
- header: t('activity'),
29
- cell: (info) => {
30
- const row = info.row.original;
31
- const title = row.title_display || info.getValue();
32
- return (
33
- <div style={{ paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom }}>
34
- <PCText size="sm" weight="medium" color="primary">
35
- {title}
36
- </PCText>
37
- </div>
38
- );
39
- },
40
- enableSorting: false,
41
- enableColumnFilter: false,
42
- }),
43
- columnHelper.accessor('activity_type', {
44
- id: 'activity_type',
45
- minSize: ACTIVITY_COLUMN_SIZES.ACTIVITY_TYPE,
46
- header: t('activityType'),
47
- cell: (info) => {
48
- const activityType = info.getValue();
49
- const row = info.row.original;
50
- const displayType = row.activity_type_display || formatActivityType(activityType);
51
- const status = getActivityTypeStatus(activityType);
52
-
53
- return (
54
- <div style={{ paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom }}>
55
- <StatusBadge status={status} className="pc-status-badge">
56
- {displayType}
57
- </StatusBadge>
58
- </div>
59
- );
60
- },
61
- enableSorting: false,
62
- enableColumnFilter: false,
63
- }),
64
- columnHelper.accessor('description', {
65
- id: 'description',
66
- minSize: ACTIVITY_COLUMN_SIZES.DESCRIPTION,
67
- header: t('activityDescription'),
68
- cell: (info) => {
69
- const row = info.row.original;
70
- const description = row.description_display || info.getValue();
71
- const truncatedDescription =
72
- description.length > ACTIVITY_DESCRIPTION_TRUNCATE_LENGTH
73
- ? `${description.substring(0, ACTIVITY_DESCRIPTION_TRUNCATE_LENGTH)}...`
74
- : description;
75
-
76
- return (
77
- <div style={{ paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom }}>
78
- {description.length > ACTIVITY_DESCRIPTION_TRUNCATE_LENGTH ? (
79
- <PCTooltip content={description} placement="top">
80
- <span style={{ display: 'block', cursor: 'help' }}>
81
- <PCText size="sm" color="secondary" style={{
82
- display: '-webkit-box',
83
- WebkitLineClamp: 2,
84
- WebkitBoxOrient: 'vertical',
85
- overflow: 'hidden',
86
- }}>
87
- {truncatedDescription}
88
- </PCText>
89
- </span>
90
- </PCTooltip>
91
- ) : (
92
- <PCText size="sm" color="secondary">
93
- {description}
94
- </PCText>
95
- )}
96
- </div>
97
- );
98
- },
99
- enableSorting: false,
100
- enableColumnFilter: false,
101
- }),
102
- columnHelper.accessor('timestamp', {
103
- id: 'timestamp',
104
- minSize: ACTIVITY_COLUMN_SIZES.TIMESTAMP,
105
- header: t('timestamp'),
106
- cell: (info) => {
107
- const timestamp = info.getValue();
108
- const date = dayjs(timestamp);
109
- const formattedDateTime = date.format(ACTIVITY_DATE_FORMATS.DISPLAY);
110
- const relativeTime = date.fromNow();
111
-
112
- return (
113
- <div style={{ paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom }}>
114
- <PCTooltip
115
- content={date.format(ACTIVITY_DATE_FORMATS.TOOLTIP)}
116
- placement="top"
117
- >
118
- <div style={{
119
- ...pcStyles.flex,
120
- ...pcStyles.flexCol,
121
- minWidth: '120px',
122
- gap: '0.125rem',
123
- }}>
124
- <PCText size="sm" weight="medium" color="primary">
125
- {formattedDateTime}
126
- </PCText>
127
- <PCText size="sm" color="secondary" style={{ fontSize: '0.75rem' }}>
128
- {relativeTime}
129
- </PCText>
130
- </div>
131
- </PCTooltip>
132
- </div>
133
- );
134
- },
135
- enableSorting: false,
136
- enableColumnFilter: false,
137
- }),
138
- ],
139
- [t]
140
- );
141
- };