@redacto.io/consent-sdk-react 5.2.0 → 6.0.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 (84) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.d.mts +24 -1
  4. package/dist/index.d.ts +24 -1
  5. package/dist/index.js +149 -206
  6. package/dist/index.mjs +148 -206
  7. package/dist/privacy-center.d.mts +178 -39
  8. package/dist/privacy-center.d.ts +178 -39
  9. package/dist/privacy-center.js +10534 -6795
  10. package/dist/privacy-center.mjs +7922 -4181
  11. package/package.json +1 -1
  12. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +16 -20
  13. package/src/RedactoNoticeConsent/api/index.ts +21 -103
  14. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +5 -4
  15. package/src/RedactoNoticeConsentInline/api/index.ts +64 -117
  16. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +174 -0
  17. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  18. package/src/RedactoPrivacyCenter/api/actions.ts +136 -8
  19. package/src/RedactoPrivacyCenter/api/client.ts +176 -15
  20. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +722 -273
  21. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +187 -0
  22. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +65 -165
  23. package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +179 -0
  24. package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +318 -0
  25. package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +333 -0
  26. package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +102 -0
  27. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +6 -2
  28. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +15 -0
  29. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +8 -5
  30. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +160 -0
  31. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +18 -0
  32. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +54 -0
  33. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +57 -0
  34. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +19 -0
  35. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +92 -0
  36. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +20 -0
  37. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +58 -0
  38. package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +10 -0
  39. package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +112 -0
  40. package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +27 -0
  41. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +105 -55
  42. package/src/RedactoPrivacyCenter/components/types.ts +2 -5
  43. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
  44. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +21 -1
  45. package/src/RedactoPrivacyCenter/lib/api.ts +147 -28
  46. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +4 -2
  47. package/src/RedactoPrivacyCenter/lib/constants.ts +78 -1
  48. package/src/RedactoPrivacyCenter/lib/types.ts +205 -36
  49. package/src/RedactoPrivacyCenter/lib/utils.ts +3 -0
  50. package/src/RedactoPrivacyCenter/locales/as/translation.json +12 -1
  51. package/src/RedactoPrivacyCenter/locales/bho/translation.json +12 -1
  52. package/src/RedactoPrivacyCenter/locales/bn/translation.json +12 -1
  53. package/src/RedactoPrivacyCenter/locales/brx/translation.json +12 -1
  54. package/src/RedactoPrivacyCenter/locales/doi/translation.json +12 -1
  55. package/src/RedactoPrivacyCenter/locales/en/translation.json +42 -1
  56. package/src/RedactoPrivacyCenter/locales/gom/translation.json +12 -1
  57. package/src/RedactoPrivacyCenter/locales/gu/translation.json +12 -1
  58. package/src/RedactoPrivacyCenter/locales/hi/translation.json +15 -1
  59. package/src/RedactoPrivacyCenter/locales/kn/translation.json +12 -1
  60. package/src/RedactoPrivacyCenter/locales/ks/translation.json +12 -1
  61. package/src/RedactoPrivacyCenter/locales/mai/translation.json +12 -1
  62. package/src/RedactoPrivacyCenter/locales/ml/translation.json +12 -1
  63. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +12 -1
  64. package/src/RedactoPrivacyCenter/locales/mr/translation.json +12 -1
  65. package/src/RedactoPrivacyCenter/locales/ne/translation.json +12 -1
  66. package/src/RedactoPrivacyCenter/locales/or/translation.json +12 -1
  67. package/src/RedactoPrivacyCenter/locales/pa/translation.json +12 -1
  68. package/src/RedactoPrivacyCenter/locales/sa/translation.json +12 -1
  69. package/src/RedactoPrivacyCenter/locales/sat/translation.json +12 -1
  70. package/src/RedactoPrivacyCenter/locales/sd/translation.json +12 -1
  71. package/src/RedactoPrivacyCenter/locales/ta/translation.json +12 -1
  72. package/src/RedactoPrivacyCenter/locales/te/translation.json +12 -1
  73. package/src/RedactoPrivacyCenter/locales/ur/translation.json +12 -1
  74. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1027 -0
  75. package/src/RedactoPrivacyCenter/styles/variables.css +5 -0
  76. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +36 -16
  77. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +4 -12
  78. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +1 -1
  79. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +23 -1
  80. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +51 -5
  81. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +2 -4
  82. package/src/index.ts +5 -0
  83. package/src/privacy-center.ts +8 -3
  84. package/src/shared/api-errors.ts +171 -0
@@ -89,6 +89,11 @@
89
89
 
90
90
  /* Icon filters for dark theme */
91
91
  --pc-icon-filter: 0;
92
+
93
+ /* Manage Consent toolbar: search, filters, export */
94
+ --pc-consent-toolbar-control-height: 2rem;
95
+ /* Header: language, notifications, theme (same pixel row as compact controls) */
96
+ --pc-navbar-actions-control-height: var(--pc-consent-toolbar-control-height);
92
97
  }
93
98
 
94
99
  [data-theme="dark"] {
@@ -67,6 +67,7 @@ export default function AdvancedTable<TData extends Record<string, any>>({
67
67
  expandedComponent,
68
68
  }: AdvancedTableProps<TData>) {
69
69
  const { t } = useTranslation();
70
+ const [hoveredRowId, setHoveredRowId] = React.useState<string | null>(null);
70
71
  if (!table) return null;
71
72
 
72
73
  return (
@@ -111,8 +112,7 @@ export default function AdvancedTable<TData extends Record<string, any>>({
111
112
  const onRowClick = meta.onRowClick;
112
113
  const handleRowClick = onRowClick
113
114
  ? (e: React.MouseEvent) => {
114
- e.preventDefault();
115
- onRowClick(row.original);
115
+ onRowClick(row.original, row);
116
116
  }
117
117
  : undefined;
118
118
 
@@ -124,8 +124,23 @@ export default function AdvancedTable<TData extends Record<string, any>>({
124
124
  <React.Fragment key={row.id}>
125
125
  <PCTable.Row
126
126
  className={classNames?.rowClassName}
127
- style={onRowClick ? { cursor: 'pointer' } : {}}
127
+ style={{
128
+ ...(onRowClick ? { cursor: 'pointer' } : {}),
129
+ ...(row.getIsExpanded() ? { borderBottom: 'none' } : {}),
130
+ ...(hoveredRowId === row.id
131
+ ? { backgroundColor: getVar('--pc-bg-hover') }
132
+ : {}),
133
+ }}
128
134
  onClick={handleRowClick}
135
+ onMouseEnter={() => setHoveredRowId(row.id)}
136
+ onMouseLeave={(e) => {
137
+ const nextSibling = e.currentTarget.nextElementSibling;
138
+ const relatedTarget = e.relatedTarget as Node | null;
139
+ if (nextSibling && relatedTarget && nextSibling.contains(relatedTarget)) {
140
+ return;
141
+ }
142
+ setHoveredRowId((prev) => (prev === row.id ? null : prev));
143
+ }}
129
144
  >
130
145
  {row.getVisibleCells().map((cell) => (
131
146
  <TableCellBasic
@@ -141,12 +156,16 @@ export default function AdvancedTable<TData extends Record<string, any>>({
141
156
  <PCTable.Row
142
157
  className={classNames?.expandedRowClassName}
143
158
  style={{
144
- overflow: 'hidden',
145
- transition: 'all 0.3s ease-in-out',
146
- maxHeight: row.getIsExpanded() ? '2000px' : '0',
147
- opacity: row.getIsExpanded() ? 1 : 0,
148
- height: row.getIsExpanded() ? 'auto' : '0',
149
- minHeight: row.getIsExpanded() ? 'auto' : '0',
159
+ pointerEvents: row.getIsExpanded() ? 'auto' : 'none',
160
+ }}
161
+ onMouseEnter={() => setHoveredRowId(row.id)}
162
+ onMouseLeave={(e) => {
163
+ const previousSibling = e.currentTarget.previousElementSibling;
164
+ const relatedTarget = e.relatedTarget as Node | null;
165
+ if (previousSibling && relatedTarget && previousSibling.contains(relatedTarget)) {
166
+ return;
167
+ }
168
+ setHoveredRowId((prev) => (prev === row.id ? null : prev));
150
169
  }}
151
170
  >
152
171
  <PCTable.Cell
@@ -154,18 +173,19 @@ export default function AdvancedTable<TData extends Record<string, any>>({
154
173
  className={classNames?.expandedCellClassName}
155
174
  style={{
156
175
  padding: row.getIsExpanded() ? undefined : '0',
157
- transition: 'all 0.3s ease-in-out',
158
- height: row.getIsExpanded() ? 'auto' : '0',
159
- paddingTop: row.getIsExpanded() ? undefined : '0',
160
- paddingBottom: row.getIsExpanded() ? undefined : '0',
176
+ height: row.getIsExpanded() ? undefined : '0',
177
+ border: 'none',
161
178
  }}
162
179
  >
163
180
  <div
164
181
  style={{
165
- transition: 'all 0.3s ease-in-out',
166
- transform: row.getIsExpanded() ? 'translateY(0)' : 'translateY(-8px)',
182
+ overflow: 'hidden',
183
+ maxHeight: row.getIsExpanded() ? '1200px' : '0',
167
184
  opacity: row.getIsExpanded() ? 1 : 0,
168
- height: row.getIsExpanded() ? 'auto' : '0',
185
+ transform: row.getIsExpanded() ? 'translateY(0)' : 'translateY(-6px)',
186
+ transition:
187
+ 'max-height 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease, transform 220ms ease',
188
+ willChange: 'max-height, opacity, transform',
169
189
  }}
170
190
  >
171
191
  {rowExpandedComponent(row)}
@@ -58,10 +58,10 @@ export default function AdvancedTablePagination<
58
58
  ...pcStyles.flexCol,
59
59
  ...pcStyles.itemsCenter,
60
60
  width: '100%',
61
- paddingTop: getVar('--pc-spacing-sm'),
62
- paddingBottom: getVar('--pc-spacing-sm'),
63
- gap: getVar('--pc-spacing-sm'),
64
- minHeight: '52px',
61
+ paddingTop: '0.375rem',
62
+ paddingBottom: '0.125rem',
63
+ gap: '0.375rem',
64
+ minHeight: '44px',
65
65
  opacity: shouldShowPagination ? 1 : 0.5,
66
66
  pointerEvents: shouldShowPagination ? 'auto' : 'none',
67
67
  }}
@@ -148,7 +148,6 @@ export default function AdvancedTablePagination<
148
148
  disabled={!table.getCanPreviousPage()}
149
149
  style={{
150
150
  borderRadius: '0.5rem',
151
- boxShadow: getVar('--pc-shadow-sm'),
152
151
  borderColor: getVar('--pc-border-color'),
153
152
  color: getVar('--pc-text-primary'),
154
153
  backgroundColor: getVar('--pc-bg-primary'),
@@ -176,7 +175,6 @@ export default function AdvancedTablePagination<
176
175
  disabled={!table.getCanPreviousPage()}
177
176
  style={{
178
177
  borderRadius: '0.5rem',
179
- boxShadow: getVar('--pc-shadow-sm'),
180
178
  borderColor: getVar('--pc-border-color'),
181
179
  color: getVar('--pc-text-primary'),
182
180
  backgroundColor: getVar('--pc-bg-primary'),
@@ -204,7 +202,6 @@ export default function AdvancedTablePagination<
204
202
  disabled={!table.getCanNextPage()}
205
203
  style={{
206
204
  borderRadius: '0.5rem',
207
- boxShadow: getVar('--pc-shadow-sm'),
208
205
  borderColor: getVar('--pc-border-color'),
209
206
  color: getVar('--pc-text-primary'),
210
207
  backgroundColor: getVar('--pc-bg-primary'),
@@ -232,7 +229,6 @@ export default function AdvancedTablePagination<
232
229
  disabled={!table.getCanNextPage()}
233
230
  style={{
234
231
  borderRadius: '0.5rem',
235
- boxShadow: getVar('--pc-shadow-sm'),
236
232
  borderColor: getVar('--pc-border-color'),
237
233
  color: getVar('--pc-text-primary'),
238
234
  backgroundColor: getVar('--pc-bg-primary'),
@@ -364,7 +360,6 @@ export default function AdvancedTablePagination<
364
360
  disabled={!table.getCanPreviousPage()}
365
361
  style={{
366
362
  borderRadius: '0.5rem',
367
- boxShadow: getVar('--pc-shadow-sm'),
368
363
  borderColor: getVar('--pc-border-color'),
369
364
  color: getVar('--pc-text-primary'),
370
365
  backgroundColor: getVar('--pc-bg-primary'),
@@ -392,7 +387,6 @@ export default function AdvancedTablePagination<
392
387
  disabled={!table.getCanPreviousPage()}
393
388
  style={{
394
389
  borderRadius: '0.5rem',
395
- boxShadow: getVar('--pc-shadow-sm'),
396
390
  borderColor: getVar('--pc-border-color'),
397
391
  color: getVar('--pc-text-primary'),
398
392
  backgroundColor: getVar('--pc-bg-primary'),
@@ -420,7 +414,6 @@ export default function AdvancedTablePagination<
420
414
  disabled={!table.getCanNextPage()}
421
415
  style={{
422
416
  borderRadius: '0.5rem',
423
- boxShadow: getVar('--pc-shadow-sm'),
424
417
  borderColor: getVar('--pc-border-color'),
425
418
  color: getVar('--pc-text-primary'),
426
419
  backgroundColor: getVar('--pc-bg-primary'),
@@ -448,7 +441,6 @@ export default function AdvancedTablePagination<
448
441
  disabled={!table.getCanNextPage()}
449
442
  style={{
450
443
  borderRadius: '0.5rem',
451
- boxShadow: getVar('--pc-shadow-sm'),
452
444
  borderColor: getVar('--pc-border-color'),
453
445
  color: getVar('--pc-text-primary'),
454
446
  backgroundColor: getVar('--pc-bg-primary'),
@@ -75,7 +75,7 @@ export function PCInput({
75
75
  return (
76
76
  <div className={className} style={wrapperStyle}>
77
77
  {label && <label style={pcStyles.inputLabel}>{label}</label>}
78
- <div style={pcStyles.inputWithPrefix}>
78
+ <div className="pc-input-affix" style={pcStyles.inputWithPrefix}>
79
79
  {prefix && <span style={pcStyles.inputPrefix}>{prefix}</span>}
80
80
  <input
81
81
  ref={inputRef}
@@ -8,6 +8,7 @@ export interface PCPopoverProps {
8
8
  shadow?: 'sm' | 'md' | 'lg';
9
9
  className?: string;
10
10
  style?: CSSProperties;
11
+ onOpen?: () => void;
11
12
  }
12
13
 
13
14
  export function PCPopover({
@@ -17,6 +18,7 @@ export function PCPopover({
17
18
  shadow = 'sm',
18
19
  className = '',
19
20
  style = {},
21
+ onOpen,
20
22
  }: PCPopoverProps) {
21
23
  const [isOpen, setIsOpen] = useState(false);
22
24
  const [position, setPosition] = useState({ top: 0, left: 0 });
@@ -150,7 +152,27 @@ export function PCPopover({
150
152
  <>
151
153
  <div
152
154
  ref={triggerRef}
153
- onClick={() => setIsOpen(!isOpen)}
155
+ role="button"
156
+ tabIndex={0}
157
+ aria-expanded={isOpen}
158
+ aria-haspopup="dialog"
159
+ onClick={() => {
160
+ const opening = !isOpen;
161
+ setIsOpen(opening);
162
+ if (opening && onOpen) {
163
+ onOpen();
164
+ }
165
+ }}
166
+ onKeyDown={(e) => {
167
+ if (e.key === 'Enter' || e.key === ' ') {
168
+ e.preventDefault();
169
+ const opening = !isOpen;
170
+ setIsOpen(opening);
171
+ if (opening && onOpen) {
172
+ onOpen();
173
+ }
174
+ }
175
+ }}
154
176
  style={{ display: 'inline-block', cursor: 'pointer' }}
155
177
  >
156
178
  {children}
@@ -7,7 +7,7 @@ import {
7
7
  useLayoutEffect,
8
8
  } from 'react';
9
9
  import { pcStyles } from '../styles/pcStyles';
10
- import { ChevronDown, X } from 'lucide-react';
10
+ import { ChevronDown, X, Check } from 'lucide-react';
11
11
 
12
12
  const getVar = (varName: string): string => `var(${varName})`;
13
13
  const useIsomorphicLayoutEffect =
@@ -32,6 +32,7 @@ export interface PCSelectProps {
32
32
  disabled?: boolean;
33
33
  size?: 'sm' | 'md' | 'lg';
34
34
  label?: ReactNode;
35
+ prefix?: ReactNode;
35
36
  /**
36
37
  * If true, the dropdown list width will match the trigger width.
37
38
  * Useful in tight filter rows where you want trigger + dropdown aligned.
@@ -40,6 +41,7 @@ export interface PCSelectProps {
40
41
  searchPlaceholder?: string;
41
42
  noOptionsMessage?: string;
42
43
  clearAriaLabel?: string;
44
+ showSelectedCheck?: boolean;
43
45
  }
44
46
 
45
47
  export function PCSelect({
@@ -55,10 +57,12 @@ export function PCSelect({
55
57
  disabled = false,
56
58
  size = 'md',
57
59
  label,
60
+ prefix,
58
61
  dropdownMatchTriggerWidth = false,
59
62
  searchPlaceholder = 'Search...',
60
63
  noOptionsMessage = 'No options found',
61
64
  clearAriaLabel = 'Clear selection',
65
+ showSelectedCheck = false,
62
66
  }: PCSelectProps) {
63
67
  const [isOpen, setIsOpen] = useState(false);
64
68
  const [searchQuery, setSearchQuery] = useState('');
@@ -112,7 +116,7 @@ export function PCSelect({
112
116
  const paddingLeft = parseFloat(computed.paddingLeft || '0') || 0;
113
117
  const paddingRight = parseFloat(computed.paddingRight || '0') || 0;
114
118
  // Chevron + spacing (and optional clear button) lives on the right
115
- const iconAreaPx = clearable ? 52 : 32;
119
+ const iconAreaPx = clearable ? 52 : showSelectedCheck ? 46 : 32;
116
120
 
117
121
  const maxLabelWidth = options.reduce((max, opt) => {
118
122
  const w = measureTextWidth(opt.label ?? '', font);
@@ -267,6 +271,17 @@ export function PCSelect({
267
271
  setIsOpen((prev) => !prev);
268
272
  }}
269
273
  >
274
+ {prefix && (
275
+ <span style={{
276
+ display: 'inline-flex',
277
+ alignItems: 'center',
278
+ flexShrink: 0,
279
+ marginRight: '0.375rem',
280
+ color: pcStyles.textSecondary.color,
281
+ }}>
282
+ {prefix}
283
+ </span>
284
+ )}
270
285
  <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
271
286
  {value ? value.label : <span style={{ color: pcStyles.textSecondary.color }}>{placeholder}</span>}
272
287
  </span>
@@ -349,10 +364,14 @@ export function PCSelect({
349
364
  <div
350
365
  key={String(option.value)}
351
366
  className="pc-select-option"
367
+ data-selected={value?.value === option.value ? 'true' : 'false'}
352
368
  style={{
353
369
  ...optionStyle,
354
370
  backgroundColor: getVar('--pc-bg-primary'),
355
- color: getVar('--pc-text-primary'),
371
+ color:
372
+ value?.value === option.value && showSelectedCheck
373
+ ? getVar('--pc-primary')
374
+ : getVar('--pc-text-primary'),
356
375
  transition: 'background-color 0.2s ease, color 0.2s ease',
357
376
  whiteSpace: 'nowrap',
358
377
  overflow: 'hidden',
@@ -360,7 +379,9 @@ export function PCSelect({
360
379
  maxWidth: '100%',
361
380
  ...(option.disabled && { opacity: 0.5, cursor: 'not-allowed' }),
362
381
  ...(value?.value === option.value && {
363
- backgroundColor: getVar('--pc-bg-hover'),
382
+ backgroundColor: showSelectedCheck
383
+ ? 'rgba(59, 130, 246, 0.12)'
384
+ : getVar('--pc-bg-hover'),
364
385
  }),
365
386
  }}
366
387
  onClick={() => handleSelect(option)}
@@ -375,7 +396,32 @@ export function PCSelect({
375
396
  : getVar('--pc-bg-primary');
376
397
  }}
377
398
  >
378
- {option.label}
399
+ <span
400
+ style={{
401
+ display: 'flex',
402
+ alignItems: 'center',
403
+ justifyContent: 'space-between',
404
+ gap: '0.5rem',
405
+ width: '100%',
406
+ minWidth: 0,
407
+ }}
408
+ >
409
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
410
+ {option.label}
411
+ </span>
412
+ {showSelectedCheck && (
413
+ <span
414
+ style={{
415
+ display: 'inline-flex',
416
+ alignItems: 'center',
417
+ opacity: value?.value === option.value ? 1 : 0,
418
+ flexShrink: 0,
419
+ }}
420
+ >
421
+ <Check size={14} strokeWidth={2.5} />
422
+ </span>
423
+ )}
424
+ </span>
379
425
  </div>
380
426
  ))
381
427
  )}
@@ -54,16 +54,14 @@ export default function ThemeToggle({ className }: ThemeToggleProps) {
54
54
  onClick={handleClick}
55
55
  aria-label={tooltipLabel}
56
56
  style={{
57
- width: '32px',
58
- height: '32px',
59
57
  color: getVar('--pc-text-primary'),
60
58
  transition: 'color 0.3s ease, transform 0.2s ease',
61
59
  }}
62
60
  >
63
61
  {theme === 'light' ? (
64
- <IoMoonOutline size={18} />
62
+ <IoMoonOutline size={16} />
65
63
  ) : (
66
- <IoSunnyOutline size={18} />
64
+ <IoSunnyOutline size={16} />
67
65
  )}
68
66
  </PCActionIcon>
69
67
  </div>
package/src/index.ts CHANGED
@@ -10,3 +10,8 @@ export type {
10
10
  ConsentFlowState,
11
11
  GuardianVerificationErrorCode,
12
12
  } from "./RedactoNoticeConsent/api/types";
13
+
14
+ // API errors — consumers can narrow caught errors to ApiError to read
15
+ // the typed error code, status, or developer-facing server message.
16
+ export { ApiError } from "./shared/api-errors";
17
+ export type { ApiErrorCode } from "./shared/api-errors";
@@ -6,6 +6,7 @@ export { PrivacyCenterConsentManagerPage } from './RedactoPrivacyCenter/PrivacyC
6
6
  export { PrivacyCenterFormPage } from './RedactoPrivacyCenter/PrivacyCenterFormPage';
7
7
  export { PrivacyCenterActivityPage } from './RedactoPrivacyCenter/PrivacyCenterActivityPage';
8
8
  export { PrivacyCenterCaseDetailsPage } from './RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage';
9
+ export { NotificationsPage } from './RedactoPrivacyCenter/NotificationsPage';
9
10
 
10
11
  // Privacy Center - Headless Core (API client & types)
11
12
  export { createConsentClient } from './RedactoPrivacyCenter/api/client';
@@ -36,11 +37,14 @@ export type {
36
37
  TokenExchangeResponse,
37
38
  FetchOrgParams,
38
39
  ConsentDataElement,
39
- UserConsent,
40
+ PurposeItem,
41
+ ConsentGroup,
42
+ DisplayPurposeRow,
43
+ ConsentModalRecord,
44
+ GroupPurposesOut,
45
+ PaginationMeta,
40
46
  NominatorInfo,
41
47
  ConsentHistoryStatusSummary,
42
- ProductConsentHistoryGroup,
43
- NominatedPurposesGroup,
44
48
  Pagination,
45
49
  UserConsentDetail,
46
50
  UserConsentResponse,
@@ -69,6 +73,7 @@ export {
69
73
  createCaseRequest,
70
74
  fetchProducts,
71
75
  fetchUserConsents,
76
+ fetchGroupPurposes,
72
77
  manageConsent,
73
78
  fetchActivities,
74
79
  fetchCaseHistory,
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Shared API error handling for the consent-sdk-react package.
3
+ *
4
+ * The backend (consent-server) owns the user-facing copy. Each error
5
+ * response carries:
6
+ * - `message`: user-safe text the SDK displays verbatim
7
+ * - `error_code`: stable identifier for programmatic branching
8
+ *
9
+ * The SDK never re-translates a code into a message. To change wording,
10
+ * update the server titles registry -- no SDK release required.
11
+ */
12
+
13
+ export type ApiErrorCode =
14
+ | "INVALID_TOKEN"
15
+ | "TOKEN_TENANT_MISMATCH"
16
+ | "UNAUTHORIZED"
17
+ | "USER_NOT_FOUND"
18
+ | "USER_EMAIL_MISMATCH"
19
+ | "USER_MOBILE_MISMATCH"
20
+ | "USER_CONTACT_MISMATCH"
21
+ | "NO_IDENTIFIER_PROVIDED"
22
+ | "INVALID_PAYLOAD"
23
+ | "VALIDATION_ERROR"
24
+ | "NOTICE_NOT_FOUND"
25
+ | "NOTICE_CONFIGURATION_NOT_FOUND"
26
+ | "NOTICE_STILL_IN_DRAFT"
27
+ | "CONSENT_ALREADY_PROVIDED"
28
+ | "PURPOSE_NOT_FOUND"
29
+ | "INVALID_PURPOSE"
30
+ | "INVALID_DATA_ELEMENTS"
31
+ | "MISSING_PURPOSE"
32
+ | "MISSING_DATA_ELEMENTS"
33
+ | "REQUIRED_DATA_ELEMENT_NOT_ENABLED"
34
+ | "AUDIO_NOT_FOUND"
35
+ | "INVALID_LANGUAGE"
36
+ | "VERIFICATION_FAILED"
37
+ | "VERIFICATION_EXPIRED"
38
+ | "GUARDIAN_INFO_NOT_FOUND"
39
+ | "GUARDIAN_SESSION_INVALID"
40
+ | "SESSION_EXPIRED"
41
+ | "INTERNAL_ERROR";
42
+
43
+ interface ApiErrorDetail {
44
+ message?: string;
45
+ error_code?: string | null;
46
+ // Ninja's auth-pipeline auto-401 returns `{detail: {detail: "Unauthorized"}}`
47
+ // -- the inner `detail` is a string. Captured here so the parser can use
48
+ // it as a last-resort message extraction.
49
+ detail?: string;
50
+ }
51
+
52
+ interface ApiErrorBody {
53
+ message?: string;
54
+ error_code?: string | null;
55
+ // `detail` may arrive as:
56
+ // { message: "...", error_code: "..." } -- our MessageResponse via Ninja
57
+ // { detail: "..." } -- Ninja auto-401
58
+ // "..." -- direct string from some endpoints
59
+ // [...] -- Ninja auto-422 Pydantic errors
60
+ detail?: ApiErrorDetail | string | unknown[];
61
+ }
62
+
63
+ // `ApiErrorCode | (string & {})` keeps literal autocomplete on `code` while
64
+ // still permitting unknown strings from forward-compatible server versions.
65
+ // A bare `ApiErrorCode | string` would widen to `string` and lose the
66
+ // literal suggestions.
67
+ export type ApiErrorCodeLike = ApiErrorCode | (string & {});
68
+
69
+ export class ApiError extends Error {
70
+ readonly status: number;
71
+ readonly code?: ApiErrorCodeLike;
72
+
73
+ constructor(opts: {
74
+ status: number;
75
+ code?: ApiErrorCodeLike;
76
+ message: string;
77
+ }) {
78
+ super(opts.message);
79
+ this.name = "ApiError";
80
+ this.status = opts.status;
81
+ this.code = opts.code;
82
+ }
83
+ }
84
+
85
+ // Tier-3 fallbacks for failure modes where the server did not produce
86
+ // a parseable body with `message`. Common case: Ninja's auth pipeline
87
+ // rejects a request before reaching our handler, returning
88
+ // `{detail: {detail: "Unauthorized"}}` with no message field. The
89
+ // fallback is a static, user-safe hint -- never an override of a
90
+ // server-supplied message.
91
+ function statusFallback(status: number): string | undefined {
92
+ if (status === 401) {
93
+ return "Your session has expired. Please request a new access link.";
94
+ }
95
+ if (status === 403) {
96
+ return "You do not have access to this resource.";
97
+ }
98
+ if (status >= 500) {
99
+ return "Something went wrong on our end. Please try again later.";
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /**
105
+ * Build an ApiError from a non-OK fetch Response.
106
+ *
107
+ * Order of preference for the user-facing message:
108
+ * 1. `message` field from the backend response body (preferred -- the
109
+ * SDK-consumed endpoints always populate this with user-safe copy).
110
+ * 2. Status-based fallback (only when the server omitted a message,
111
+ * e.g. Ninja's auto-401 for an expired JWT).
112
+ * 3. The caller-supplied fallback (used when the body is missing or
113
+ * unparseable -- network error, non-JSON proxy page).
114
+ *
115
+ * The `code` field is exposed as-is for programmatic branching. Consumers
116
+ * that want compile-time safety should narrow against `ApiErrorCode`.
117
+ */
118
+ function extractFromDetail(
119
+ detail: ApiErrorBody["detail"]
120
+ ): { message?: string; code?: string | null } {
121
+ // String detail (e.g. some endpoints return `{detail: "..."}` directly).
122
+ if (typeof detail === "string") {
123
+ return { message: detail };
124
+ }
125
+ // Array detail (Ninja's auto-422 Pydantic validation errors): no usable
126
+ // single message; let the caller fall back to status/generic copy.
127
+ if (Array.isArray(detail)) {
128
+ return {};
129
+ }
130
+ // Object detail covers both shapes:
131
+ // our MessageResponse: { message, error_code }
132
+ // Ninja auto-401: { detail: "Unauthorized" }
133
+ if (detail && typeof detail === "object") {
134
+ const obj = detail as ApiErrorDetail;
135
+ return { message: obj.message ?? obj.detail, code: obj.error_code };
136
+ }
137
+ return {};
138
+ }
139
+
140
+ export async function parseApiError(
141
+ response: Response,
142
+ fallbackMessage: string
143
+ ): Promise<ApiError> {
144
+ let body: ApiErrorBody | undefined;
145
+ let parsedBody = false;
146
+ try {
147
+ body = (await response.json()) as ApiErrorBody;
148
+ parsedBody = true;
149
+ } catch {
150
+ body = undefined;
151
+ }
152
+
153
+ const fromDetail = extractFromDetail(body?.detail);
154
+ const serverMessage = body?.message ?? fromDetail.message;
155
+ // Status fallback only applies when the body parsed but had no message
156
+ // (e.g. Ninja auto-401's `{detail: {detail: "Unauthorized"}}`). When the
157
+ // body is unparseable (proxy HTML 502, empty body, network jank) the
158
+ // caller's per-endpoint fallback is the more accurate hint -- a 401 from
159
+ // a misbehaving proxy is not necessarily an expired session.
160
+ const message =
161
+ serverMessage ??
162
+ (parsedBody ? statusFallback(response.status) : undefined) ??
163
+ fallbackMessage;
164
+ const code = body?.error_code ?? fromDetail.code ?? undefined;
165
+
166
+ return new ApiError({
167
+ status: response.status,
168
+ code,
169
+ message,
170
+ });
171
+ }