@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +6 -0
- package/README.md +93 -12
- package/dist/index.d.mts +39 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +4454 -334
- package/dist/index.mjs +4454 -335
- package/dist/privacy-center.d.mts +4 -1
- package/dist/privacy-center.d.ts +4 -1
- package/dist/privacy-center.js +3905 -3660
- package/dist/privacy-center.mjs +4172 -3921
- package/package.json +1 -1
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +847 -0
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +2133 -0
- package/src/RedactoNoticeAssisted/api/index.ts +269 -0
- package/src/RedactoNoticeAssisted/api/types.ts +180 -0
- package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeAssisted/constants.ts +51 -0
- package/src/RedactoNoticeAssisted/i18n.ts +1463 -0
- package/src/RedactoNoticeAssisted/index.ts +1 -0
- package/src/RedactoNoticeAssisted/injectStyles.ts +251 -0
- package/src/RedactoNoticeAssisted/styles.ts +730 -0
- package/src/RedactoNoticeAssisted/types.ts +24 -0
- package/src/RedactoNoticeAssisted/useMediaQuery.ts +36 -0
- package/src/RedactoPrivacyCenter/NotificationsPage.tsx +8 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +6 -11
- package/src/RedactoPrivacyCenter/api/client.ts +15 -3
- package/src/RedactoPrivacyCenter/api/fetcher.ts +80 -1
- package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +193 -213
- package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +239 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +3 -1
- package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +8 -1
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +23 -17
- package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +24 -10
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +16 -3
- package/src/RedactoPrivacyCenter/components/constant.ts +25 -0
- package/src/RedactoPrivacyCenter/components/types.ts +32 -1
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +25 -23
- package/src/RedactoPrivacyCenter/lib/constants.ts +23 -10
- package/src/RedactoPrivacyCenter/lib/queries.ts +9 -2
- package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +127 -0
- package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +85 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +213 -63
- package/src/RedactoPrivacyCenter/locales/en/translation.json +3 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +16 -0
- package/src/index.ts +2 -0
- package/src/shared/api-errors.ts +9 -0
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +0 -141
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Internal types for RedactoNoticeAssisted.
|
|
2
|
+
|
|
3
|
+
/** The three screens of the agent-assisted consent flow. */
|
|
4
|
+
export type ConsoleState = "notice" | "otp" | "confirmed";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Public props. The notice is fetched from the PUBLIC notice endpoint, so the
|
|
8
|
+
* organisation / workspace / notice UUIDs and base URL are supplied by the host
|
|
9
|
+
* (typically read from the frontend URL) — there is no auth token.
|
|
10
|
+
*/
|
|
11
|
+
export type RedactoNoticeAssistedProps = {
|
|
12
|
+
/** Organisation UUID (path segment of the public notice endpoint). */
|
|
13
|
+
organisationUuid: string;
|
|
14
|
+
/** Workspace UUID (path segment of the public notice endpoint). */
|
|
15
|
+
workspaceUuid: string;
|
|
16
|
+
/** Notice UUID to fetch and render. */
|
|
17
|
+
noticeUuid: string;
|
|
18
|
+
/** API base URL (defaults to the production consent API). */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/** Called when the agent/customer declines (host should unmount the modal). */
|
|
21
|
+
onDecline?: () => void;
|
|
22
|
+
/** Called when the agent clicks "Proceed" on the confirmed screen. */
|
|
23
|
+
onComplete?: () => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export const useMediaQuery = (query: string): boolean => {
|
|
4
|
+
const getMatches = () =>
|
|
5
|
+
typeof window !== "undefined" ? window.matchMedia(query).matches : false;
|
|
6
|
+
const [matches, setMatches] = useState<boolean>(getMatches);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (typeof window === "undefined") return;
|
|
10
|
+
const media = window.matchMedia(query);
|
|
11
|
+
|
|
12
|
+
// Create event listener
|
|
13
|
+
const listener = (e: MediaQueryListEvent) => {
|
|
14
|
+
setMatches(e.matches);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Add listener (fallback for legacy browsers)
|
|
18
|
+
if (media.addEventListener) {
|
|
19
|
+
media.addEventListener("change", listener);
|
|
20
|
+
} else {
|
|
21
|
+
// @ts-ignore – deprecated fallback
|
|
22
|
+
media.addListener(listener);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Clean up
|
|
26
|
+
return () => {
|
|
27
|
+
if (media.removeEventListener) {
|
|
28
|
+
media.removeEventListener("change", listener);
|
|
29
|
+
} else {
|
|
30
|
+
media.removeListener(listener);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}, [query]);
|
|
34
|
+
|
|
35
|
+
return matches;
|
|
36
|
+
};
|
|
@@ -3,6 +3,7 @@ import { IoChevronBackOutline } from 'react-icons/io5';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { usePageChrome } from './context/PageChromeContext';
|
|
5
5
|
import { fetchNotifications } from './lib/api';
|
|
6
|
+
import { isRefreshBlocked } from './lib/utils';
|
|
6
7
|
import type { Notification, NotificationSummary, NotificationCTA, FilterTab } from './lib/types';
|
|
7
8
|
import { TAB_TO_CATEGORY, POLL_INTERVAL, NOTIFICATION_PAGE_SIZE, useNotificationStatLabels } from './lib/constants';
|
|
8
9
|
import { useCurrentLanguage } from './lib/useCurrentLanguage';
|
|
@@ -39,6 +40,13 @@ export const NotificationsPage = () => {
|
|
|
39
40
|
if (!organizationId || !workspaceId) {
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
43
|
+
// Skip polling/loading work while auth is terminally failed / backing off
|
|
44
|
+
// (incident 2026-06-22); clear spinners so the UI doesn't hang.
|
|
45
|
+
if (isRefreshBlocked(authContext)) {
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
setIsRefreshing(false);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
42
50
|
if (!silent) {
|
|
43
51
|
setIsLoading(true);
|
|
44
52
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BaseRequest } from './client';
|
|
2
|
+
import { isTerminalRefreshError } from './fetcher';
|
|
2
3
|
|
|
3
4
|
export type ConsentApiErrorCode = 'NOMINATOR_NOT_FOUND' | 'NOMINEE_MUST_SPECIFY_NOMINATOR';
|
|
4
5
|
|
|
@@ -219,20 +220,14 @@ export async function refreshOtpTokenServer(
|
|
|
219
220
|
}
|
|
220
221
|
return response;
|
|
221
222
|
} catch (error: any) {
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
errorMessage.includes('Invalid refresh token') ||
|
|
227
|
-
errorMessage.includes('Refresh token') ||
|
|
228
|
-
errorMessage.includes('revoked');
|
|
229
|
-
|
|
230
|
-
// For expired/revoked tokens, return null gracefully (don't log as error)
|
|
231
|
-
if (isExpiredTokenError) {
|
|
223
|
+
// Terminal token errors (expired / not found / already used / 4xx) are an
|
|
224
|
+
// expected outcome — return null so the caller trips the circuit breaker
|
|
225
|
+
// instead of treating it as a retryable exception (incident 2026-06-22).
|
|
226
|
+
if (isTerminalRefreshError(error)) {
|
|
232
227
|
return null;
|
|
233
228
|
}
|
|
234
229
|
|
|
235
|
-
//
|
|
230
|
+
// Transient errors (network / 5xx): re-throw so the caller can back off + retry.
|
|
236
231
|
console.error('Error refreshing token:', error);
|
|
237
232
|
throw error;
|
|
238
233
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiFetch } from "./fetcher";
|
|
1
|
+
import { apiFetch, ApiError } from "./fetcher";
|
|
2
2
|
import type { ApiFetchOptions, ApiResponse } from "./fetcher";
|
|
3
3
|
import type {
|
|
4
4
|
CaseMessage,
|
|
@@ -86,6 +86,8 @@ export function createConsentClient(baseUrl: string) {
|
|
|
86
86
|
// Handle non-OK responses
|
|
87
87
|
if (!response.ok) {
|
|
88
88
|
let errorMessage = `Request failed with status ${response.status}`;
|
|
89
|
+
let errorCode: string | undefined;
|
|
90
|
+
let errorDetail: unknown;
|
|
89
91
|
|
|
90
92
|
try {
|
|
91
93
|
const errorData = (await response.json()) as {
|
|
@@ -94,6 +96,10 @@ export function createConsentClient(baseUrl: string) {
|
|
|
94
96
|
code?: string;
|
|
95
97
|
} | null;
|
|
96
98
|
if (errorData) {
|
|
99
|
+
errorDetail = errorData.detail;
|
|
100
|
+
if (typeof errorData.code === "string") {
|
|
101
|
+
errorCode = errorData.code;
|
|
102
|
+
}
|
|
97
103
|
if ("detail" in errorData && errorData.detail) {
|
|
98
104
|
errorMessage =
|
|
99
105
|
typeof errorData.detail === "string"
|
|
@@ -118,8 +124,14 @@ export function createConsentClient(baseUrl: string) {
|
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
//
|
|
122
|
-
|
|
127
|
+
// Throw a typed error carrying the status/code. The `[status]` prefix is
|
|
128
|
+
// kept in the message for back-compat with callers that branch on it
|
|
129
|
+
// (e.g. getFormData checks for "404").
|
|
130
|
+
throw new ApiError(`[${response.status}] ${errorMessage}`, {
|
|
131
|
+
status: response.status,
|
|
132
|
+
code: errorCode,
|
|
133
|
+
detail: errorDetail,
|
|
134
|
+
});
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
// Parse JSON response
|
|
@@ -17,6 +17,72 @@ export type ApiResponse<T> = {
|
|
|
17
17
|
detail: T;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Error thrown by the fetch wrappers that preserves the HTTP status code and any
|
|
22
|
+
* server-provided error code alongside the human-readable message.
|
|
23
|
+
*
|
|
24
|
+
* This exists so token-refresh classification (and react-query retry guards) can
|
|
25
|
+
* reliably distinguish a *terminal* 4xx (the refresh token is dead — retrying just
|
|
26
|
+
* hammers the server) from a *transient* 5xx/network failure, without resorting to
|
|
27
|
+
* brittle substring matching on localized message strings. See the production
|
|
28
|
+
* `/otp/refresh` retry storm on 2026-06-22.
|
|
29
|
+
*/
|
|
30
|
+
export class ApiError extends Error {
|
|
31
|
+
readonly status?: number;
|
|
32
|
+
readonly code?: string;
|
|
33
|
+
readonly detail?: unknown;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
message: string,
|
|
37
|
+
options: { status?: number; code?: string; detail?: unknown } = {}
|
|
38
|
+
) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "ApiError";
|
|
41
|
+
this.status = options.status;
|
|
42
|
+
this.code = options.code;
|
|
43
|
+
this.detail = options.detail;
|
|
44
|
+
// Restore the prototype chain so `instanceof ApiError` works after the
|
|
45
|
+
// class is transpiled down (tsup → ES targets).
|
|
46
|
+
Object.setPrototypeOf(this, ApiError.prototype);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// HTTP statuses that mean "this refresh token will never succeed" — retrying is
|
|
51
|
+
// pointless and only adds server load. 5xx / 408 / 429 / network errors are NOT
|
|
52
|
+
// in this set: those are transient and handled with backoff instead.
|
|
53
|
+
const TERMINAL_REFRESH_STATUSES = new Set<number>([400, 401, 403, 422]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* True when an error from the OTP refresh endpoint is terminal — i.e. the refresh
|
|
57
|
+
* token is expired, not found, already used, or otherwise invalid, so no amount of
|
|
58
|
+
* immediate retrying will help. Prefers the HTTP status carried by {@link ApiError};
|
|
59
|
+
* falls back to a deliberately narrow message match for non-typed throws.
|
|
60
|
+
*/
|
|
61
|
+
export function isTerminalRefreshError(error: unknown): boolean {
|
|
62
|
+
const e = error as { status?: unknown; message?: unknown } | null | undefined;
|
|
63
|
+
if (!e) return false;
|
|
64
|
+
|
|
65
|
+
// Primary, reliable signal: the HTTP status from the refresh response.
|
|
66
|
+
if (typeof e.status === "number") {
|
|
67
|
+
return TERMINAL_REFRESH_STATUSES.has(e.status);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fallback for errors without a status (older shapes / plain Error). Match only
|
|
71
|
+
// the known terminal token phrases so transient failures aren't misclassified.
|
|
72
|
+
const message = (typeof e.message === "string" ? e.message : "").toLowerCase();
|
|
73
|
+
if (!message) return false;
|
|
74
|
+
return (
|
|
75
|
+
message.includes("expired") ||
|
|
76
|
+
message.includes("revoked") ||
|
|
77
|
+
message.includes("not found") ||
|
|
78
|
+
message.includes("already used") ||
|
|
79
|
+
message.includes("invalid refresh token") ||
|
|
80
|
+
message.includes("unauthorized") ||
|
|
81
|
+
message.includes("[401]") ||
|
|
82
|
+
message.includes("[403]")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
20
86
|
/**
|
|
21
87
|
* Unified fetch wrapper for client-side API calls.
|
|
22
88
|
* Provides typed responses and automatic error handling.
|
|
@@ -84,13 +150,20 @@ export async function apiFetch<T>(
|
|
|
84
150
|
// Handle non-OK responses
|
|
85
151
|
if (!response.ok) {
|
|
86
152
|
let errorMessage = `Request failed with status ${response.status}`;
|
|
153
|
+
let errorCode: string | undefined;
|
|
154
|
+
let errorDetail: unknown;
|
|
87
155
|
|
|
88
156
|
try {
|
|
89
157
|
const errorData = (await response.json()) as {
|
|
90
158
|
detail?: unknown;
|
|
91
159
|
message?: string;
|
|
160
|
+
code?: string;
|
|
92
161
|
} | null;
|
|
93
162
|
if (errorData) {
|
|
163
|
+
errorDetail = errorData.detail;
|
|
164
|
+
if (typeof errorData.code === "string") {
|
|
165
|
+
errorCode = errorData.code;
|
|
166
|
+
}
|
|
94
167
|
if ("detail" in errorData && errorData.detail) {
|
|
95
168
|
errorMessage =
|
|
96
169
|
typeof errorData.detail === "string"
|
|
@@ -112,7 +185,13 @@ export async function apiFetch<T>(
|
|
|
112
185
|
}
|
|
113
186
|
}
|
|
114
187
|
|
|
115
|
-
|
|
188
|
+
// Preserve status + server error code so callers can classify terminal vs.
|
|
189
|
+
// transient failures without brittle string matching (incident 2026-06-22).
|
|
190
|
+
throw new ApiError(errorMessage, {
|
|
191
|
+
status: response.status,
|
|
192
|
+
code: errorCode,
|
|
193
|
+
detail: errorDetail,
|
|
194
|
+
});
|
|
116
195
|
}
|
|
117
196
|
|
|
118
197
|
// Handle empty responses (common for DELETE requests with 204 No Content)
|