@redacto.io/consent-sdk-react 5.1.0 → 5.3.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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +24 -1
  3. package/dist/index.d.ts +24 -1
  4. package/dist/index.js +149 -206
  5. package/dist/index.mjs +148 -206
  6. package/dist/privacy-center.d.mts +81 -2
  7. package/dist/privacy-center.d.ts +81 -2
  8. package/dist/privacy-center.js +2823 -1272
  9. package/dist/privacy-center.mjs +2813 -1263
  10. package/package.json +1 -1
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +16 -20
  12. package/src/RedactoNoticeConsent/api/index.ts +21 -103
  13. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +5 -4
  14. package/src/RedactoNoticeConsentInline/api/index.ts +64 -117
  15. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +174 -0
  16. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  17. package/src/RedactoPrivacyCenter/api/actions.ts +84 -0
  18. package/src/RedactoPrivacyCenter/api/client.ts +118 -0
  19. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +15 -0
  20. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +160 -0
  21. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +18 -0
  22. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +54 -0
  23. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +57 -0
  24. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +19 -0
  25. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +92 -0
  26. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +20 -0
  27. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +58 -0
  28. package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +10 -0
  29. package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +112 -0
  30. package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +27 -0
  31. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -2
  32. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
  33. package/src/RedactoPrivacyCenter/lib/api.ts +77 -0
  34. package/src/RedactoPrivacyCenter/lib/constants.ts +78 -1
  35. package/src/RedactoPrivacyCenter/lib/types.ts +148 -0
  36. package/src/RedactoPrivacyCenter/lib/utils.ts +3 -0
  37. package/src/RedactoPrivacyCenter/locales/en/translation.json +27 -1
  38. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +505 -0
  39. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +23 -1
  40. package/src/index.ts +5 -0
  41. package/src/privacy-center.ts +1 -0
  42. package/src/shared/api-errors.ts +171 -0
@@ -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
+ }