@redacto.io/consent-sdk-react 9.2.0-beta.2 → 9.3.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 (39) hide show
  1. package/.turbo/turbo-build.log +25 -26
  2. package/.turbo/turbo-test.log +203 -0
  3. package/.turbo/turbo-verify-pack.log +6 -0
  4. package/CHANGELOG.md +24 -0
  5. package/dist/privacy-center.js +2428 -2223
  6. package/dist/privacy-center.mjs +2572 -2359
  7. package/package.json +3 -3
  8. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -3
  9. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +36 -5
  10. package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +3 -2
  11. package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +76 -0
  12. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +3 -41
  13. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +169 -0
  14. package/src/RedactoPrivacyCenter/locales/as/translation.json +2 -0
  15. package/src/RedactoPrivacyCenter/locales/bho/translation.json +2 -0
  16. package/src/RedactoPrivacyCenter/locales/bn/translation.json +2 -0
  17. package/src/RedactoPrivacyCenter/locales/brx/translation.json +2 -0
  18. package/src/RedactoPrivacyCenter/locales/doi/translation.json +2 -0
  19. package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -0
  20. package/src/RedactoPrivacyCenter/locales/gom/translation.json +2 -0
  21. package/src/RedactoPrivacyCenter/locales/gu/translation.json +2 -0
  22. package/src/RedactoPrivacyCenter/locales/hi/translation.json +2 -0
  23. package/src/RedactoPrivacyCenter/locales/kn/translation.json +2 -0
  24. package/src/RedactoPrivacyCenter/locales/ks/translation.json +2 -0
  25. package/src/RedactoPrivacyCenter/locales/mai/translation.json +2 -0
  26. package/src/RedactoPrivacyCenter/locales/ml/translation.json +2 -0
  27. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +2 -0
  28. package/src/RedactoPrivacyCenter/locales/mr/translation.json +2 -0
  29. package/src/RedactoPrivacyCenter/locales/ne/translation.json +2 -0
  30. package/src/RedactoPrivacyCenter/locales/or/translation.json +2 -0
  31. package/src/RedactoPrivacyCenter/locales/pa/translation.json +2 -0
  32. package/src/RedactoPrivacyCenter/locales/sa/translation.json +2 -0
  33. package/src/RedactoPrivacyCenter/locales/sat/translation.json +2 -0
  34. package/src/RedactoPrivacyCenter/locales/sd/translation.json +2 -0
  35. package/src/RedactoPrivacyCenter/locales/ta/translation.json +2 -0
  36. package/src/RedactoPrivacyCenter/locales/te/translation.json +2 -0
  37. package/src/RedactoPrivacyCenter/locales/ur/translation.json +2 -0
  38. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1 -1
  39. package/tests/DataAvailabilityContext.test.tsx +123 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "9.2.0-beta.2",
3
+ "version": "9.3.0-beta.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -32,11 +32,11 @@
32
32
  "@types/lodash": "^4.17.0",
33
33
  "@types/react": "^19.1.3",
34
34
  "@types/react-dom": "^19.1.3",
35
- "@vitest/coverage-v8": "3.1.3",
35
+ "@vitest/coverage-v8": "^4.1.8",
36
36
  "jsdom": "^26.1.0",
37
37
  "tsup": "^8.4.0",
38
38
  "typescript": "5.8.2",
39
- "vitest": "^3.1.3"
39
+ "vitest": "^4.1.8"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": "^16.8.0 || ^17 || ^18 || ^19",
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState, useRef, type ReactNode } from 'react';
2
2
  import { OrganizationProvider } from './context/OrganizationContext';
3
+ import { DataAvailabilityProvider } from './context/DataAvailabilityContext';
3
4
  import type { OrganizationDetail } from './lib/types';
4
5
  import {
5
6
  checkAndRefreshAuth,
@@ -161,9 +162,11 @@ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLay
161
162
 
162
163
  return (
163
164
  <OrganizationProvider organization={organization}>
164
- <AppShell onBack={onBack}>
165
- {children}
166
- </AppShell>
165
+ <DataAvailabilityProvider>
166
+ <AppShell onBack={onBack}>
167
+ {children}
168
+ </AppShell>
169
+ </DataAvailabilityProvider>
167
170
  </OrganizationProvider>
168
171
  );
169
172
  }
@@ -19,6 +19,9 @@ import { TopAppBar } from './TopAppBar';
19
19
  import { SidebarNav } from './SidebarNav';
20
20
  import { MobileNavDrawer } from './MobileNavDrawer';
21
21
  import { PageHeader } from './PageHeader';
22
+ import { PCLoader } from '../../ui';
23
+ import { useDataAvailability } from '../../context/DataAvailabilityContext';
24
+ import { PrivacyCenterEmptyState } from '../PrivacyCenterEmptyState';
22
25
  import type { PrivacyCenterLayoutProps } from '../types';
23
26
 
24
27
  export default function AppShell({
@@ -35,6 +38,13 @@ export default function AppShell({
35
38
 
36
39
  const isMobile = useMediaQuery('(max-width: 639px)');
37
40
 
41
+ // Keep the shell minimal — top bar + a content spinner — until we've CONFIRMED
42
+ // the user has data. Hiding the sidebar/tabs/bell during 'checking' (not just
43
+ // 'empty') stops the nav from flashing in on refresh only to disappear when
44
+ // the API resolves to "no data". The branded top bar stays visible throughout.
45
+ const { status } = useDataAvailability();
46
+ const hideNav = status !== 'has-data';
47
+
38
48
  // Close mobile drawer when crossing the breakpoint
39
49
  useEffect(() => {
40
50
  if (!isMobile && isMobileNavOpen) setMobileNavOpen(false);
@@ -110,14 +120,15 @@ export default function AppShell({
110
120
  <main className="pc-root pc-shell-root" style={rootStyle}>
111
121
  <TopAppBar
112
122
  ref={hamburgerRef}
113
- showHamburger={isMobile}
123
+ showHamburger={isMobile && !hideNav}
114
124
  onOpenMobileNav={() => setMobileNavOpen(true)}
115
125
  locale={locale}
116
126
  onLocaleChange={setLocale}
117
127
  onBack={onBack}
128
+ showNotifications={!hideNav}
118
129
  />
119
130
  <div className="pc-shell-body" style={bodyStyle}>
120
- {!isMobile && (
131
+ {!isMobile && !hideNav && (
121
132
  <SidebarNav
122
133
  className={sidebarClassName}
123
134
  isExpanded={sidebar.isExpanded}
@@ -126,8 +137,28 @@ export default function AppShell({
126
137
  )}
127
138
  <section className="pc-shell-content" style={mainStyle}>
128
139
  <div className={contentClassName} style={contentWrapperStyle}>
129
- <PageHeader />
130
- <div style={{ flex: 1, minWidth: 0, width: '100%' }}>{children}</div>
140
+ {status === 'checking' ? (
141
+ <div
142
+ style={{
143
+ ...pcStyles.flex,
144
+ ...pcStyles.itemsCenter,
145
+ ...pcStyles.justifyCenter,
146
+ flex: 1,
147
+ minHeight: 240,
148
+ }}
149
+ >
150
+ <PCLoader variant="spinner" color="info" size="lg" />
151
+ </div>
152
+ ) : status === 'empty' ? (
153
+ <div style={{ flex: 1, minWidth: 0, width: '100%' }}>
154
+ <PrivacyCenterEmptyState />
155
+ </div>
156
+ ) : (
157
+ <>
158
+ <PageHeader />
159
+ <div style={{ flex: 1, minWidth: 0, width: '100%' }}>{children}</div>
160
+ </>
161
+ )}
131
162
  <div
132
163
  style={{
133
164
  ...pcStyles.flex,
@@ -143,7 +174,7 @@ export default function AppShell({
143
174
  </div>
144
175
  </section>
145
176
  </div>
146
- {isMobile && (
177
+ {isMobile && !hideNav && (
147
178
  <MobileNavDrawer
148
179
  isOpen={isMobileNavOpen}
149
180
  onClose={() => setMobileNavOpen(false)}
@@ -26,10 +26,11 @@ type TopAppBarProps = {
26
26
  locale: string;
27
27
  onLocaleChange: (next: string) => void;
28
28
  onBack?: 'back' | 'signout';
29
+ showNotifications?: boolean;
29
30
  };
30
31
 
31
32
  export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function TopAppBar(
32
- { onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack },
33
+ { onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack, showNotifications = true },
33
34
  hamburgerRef,
34
35
  ) {
35
36
  const { t } = useTranslation();
@@ -170,7 +171,7 @@ export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function
170
171
  onChange={handleLanguageChange}
171
172
  />
172
173
  )}
173
- <NotificationBell />
174
+ {showNotifications && <NotificationBell />}
174
175
  <UserMenu onBack={onBack} />
175
176
  </div>
176
177
  </header>
@@ -0,0 +1,76 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import character from '../assets/character.svg';
3
+ import { PCText, PCTitle } from '../ui';
4
+ import { pcStyles } from '../styles/pcStyles';
5
+ import { getVar } from '../lib/utils';
6
+
7
+ /**
8
+ * Full-screen content shown (in place of the consent manager / tabs) when the
9
+ * user has no data anywhere — no consents, no data-request cases, no activity.
10
+ * Purely presentational: a friendly illustration, heading, and subtitle.
11
+ */
12
+ export function PrivacyCenterEmptyState() {
13
+ const { t } = useTranslation();
14
+
15
+ return (
16
+ <div
17
+ style={{
18
+ ...pcStyles.flex,
19
+ ...pcStyles.flexCol,
20
+ ...pcStyles.itemsCenter,
21
+ ...pcStyles.justifyCenter,
22
+ ...pcStyles.textCenter,
23
+ width: '100%',
24
+ maxWidth: 460,
25
+ margin: '0 auto',
26
+ minHeight: 'min(62vh, 540px)',
27
+ paddingTop: getVar('--pc-spacing-2xl'),
28
+ paddingBottom: getVar('--pc-spacing-2xl'),
29
+ gap: getVar('--pc-spacing-xl'),
30
+ }}
31
+ >
32
+ <div
33
+ aria-hidden="true"
34
+ style={{
35
+ display: 'flex',
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ width: 168,
39
+ height: 168,
40
+ borderRadius: '50%',
41
+ background:
42
+ 'color-mix(in srgb, var(--pc-primary) 8%, var(--pc-bg-shell))',
43
+ flexShrink: 0,
44
+ }}
45
+ >
46
+ <img
47
+ src={character}
48
+ alt=""
49
+ style={{ width: 104, height: 'auto', display: 'block' }}
50
+ />
51
+ </div>
52
+
53
+ <div
54
+ style={{
55
+ ...pcStyles.flex,
56
+ ...pcStyles.flexCol,
57
+ ...pcStyles.itemsCenter,
58
+ gap: getVar('--pc-spacing-sm'),
59
+ }}
60
+ >
61
+ <PCTitle as="h2" style={{ margin: 0 }}>
62
+ {t('noDataTitle')}
63
+ </PCTitle>
64
+ <PCText
65
+ as="p"
66
+ size="base"
67
+ color="secondary"
68
+ align="center"
69
+ style={{ margin: 0, maxWidth: 400 }}
70
+ >
71
+ {t('noDataDescription')}
72
+ </PCText>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -13,7 +13,7 @@ import { useAuth } from '../context/AuthContext';
13
13
  import { useBaseUrl } from '../context/BaseUrlContext';
14
14
  import { useCurrentLanguage } from '../lib/useCurrentLanguage';
15
15
  import { toast } from '../utils/toast';
16
- import type { Receipt, ReceiptEventType, ReceiptPurposeStatus } from '../lib/types';
16
+ import type { Receipt, ReceiptEventType } from '../lib/types';
17
17
  import { useReceiptColumns } from './receiptColumns';
18
18
 
19
19
  /**
@@ -89,7 +89,6 @@ export default function ReceiptsList() {
89
89
 
90
90
  // ─── Filters ────────────────────────────────────────────────────────────────
91
91
  const [eventType, setEventType] = useState<ReceiptEventType | ''>('');
92
- const [status, setStatus] = useState<ReceiptPurposeStatus | ''>('');
93
92
  const [datePreset, setDatePreset] = useState<string>(''); // '' | '1' | '7' | '30' | '90' | 'custom'
94
93
  const [customFrom, setCustomFrom] = useState<string>('');
95
94
  const [customTo, setCustomTo] = useState<string>('');
@@ -104,15 +103,6 @@ export default function ReceiptsList() {
104
103
  ],
105
104
  [t]
106
105
  );
107
- const statusOptions = useMemo<SelectOption[]>(
108
- () => [
109
- { value: '', label: t('all') },
110
- { value: 'CONSENTED', label: t('consented') },
111
- { value: 'DECLINED', label: t('declined') },
112
- { value: 'WITHDRAWN', label: t('withdrawn') },
113
- ],
114
- [t]
115
- );
116
106
  const dateOptions = useMemo<SelectOption[]>(
117
107
  () => [
118
108
  { value: '', label: t('allTime') },
@@ -126,9 +116,8 @@ export default function ReceiptsList() {
126
116
  );
127
117
 
128
118
  const eventValue = eventTypeOptions.find((o) => o.value === eventType) ?? null;
129
- const statusValue = statusOptions.find((o) => o.value === status) ?? null;
130
119
  const dateValue = dateOptions.find((o) => o.value === datePreset) ?? null;
131
- const hasActiveFilters = Boolean(eventType || status || datePreset);
120
+ const hasActiveFilters = Boolean(eventType || datePreset);
132
121
  const isCustomRangeInvalid = useMemo(
133
122
  () =>
134
123
  datePreset === 'custom' &&
@@ -142,9 +131,6 @@ export default function ReceiptsList() {
142
131
  if (eventType && eventValue?.label) {
143
132
  filters.push({ id: 'event', label: t('event'), value: eventValue.label });
144
133
  }
145
- if (status && statusValue?.label) {
146
- filters.push({ id: 'status', label: t('status'), value: statusValue.label });
147
- }
148
134
  if (datePreset && dateValue?.label) {
149
135
  let dateLabel = dateValue.label;
150
136
  if (datePreset === 'custom') {
@@ -156,16 +142,12 @@ export default function ReceiptsList() {
156
142
  }
157
143
 
158
144
  return filters;
159
- }, [eventType, eventValue, status, statusValue, datePreset, dateValue, customFrom, customTo, t]);
145
+ }, [eventType, eventValue, datePreset, dateValue, customFrom, customTo, t]);
160
146
 
161
147
  const handleEventChange = (opt: SelectOption | null) => {
162
148
  setEventType((opt ? String(opt.value) : '') as ReceiptEventType | '');
163
149
  setCurrentPage(1);
164
150
  };
165
- const handleStatusChange = (opt: SelectOption | null) => {
166
- setStatus((opt ? String(opt.value) : '') as ReceiptPurposeStatus | '');
167
- setCurrentPage(1);
168
- };
169
151
  const handleDateChange = (opt: SelectOption | null) => {
170
152
  const nextDatePreset = opt ? String(opt.value) : '';
171
153
  setDatePreset(nextDatePreset);
@@ -177,7 +159,6 @@ export default function ReceiptsList() {
177
159
  };
178
160
  const handleClearFilters = () => {
179
161
  setEventType('');
180
- setStatus('');
181
162
  setDatePreset('');
182
163
  setCustomFrom('');
183
164
  setCustomTo('');
@@ -199,7 +180,6 @@ export default function ReceiptsList() {
199
180
  skip,
200
181
  limit: size,
201
182
  event_type: eventType || undefined,
202
- status: status || undefined,
203
183
  created_after,
204
184
  created_before,
205
185
  language: langParam,
@@ -229,7 +209,6 @@ export default function ReceiptsList() {
229
209
  currentPage,
230
210
  pageSize,
231
211
  eventType,
232
- status,
233
212
  datePreset,
234
213
  customFrom,
235
214
  customTo,
@@ -375,23 +354,6 @@ export default function ReceiptsList() {
375
354
  dropdownMatchTriggerWidth
376
355
  style={{ width: '100%' }}
377
356
  />
378
- <PCSelect
379
- className="pc-filter-select pc-receipts-filter-control"
380
- size="sm"
381
- label={t('status')}
382
- placeholder={t('all')}
383
- options={statusOptions}
384
- value={statusValue}
385
- onChange={handleStatusChange}
386
- onClear={() => {
387
- setStatus('');
388
- setCurrentPage(1);
389
- }}
390
- clearable={Boolean(status)}
391
- showSelectedCheck
392
- dropdownMatchTriggerWidth
393
- style={{ width: '100%' }}
394
- />
395
357
  <PCSelect
396
358
  className="pc-filter-select pc-receipts-filter-control"
397
359
  size="sm"
@@ -0,0 +1,169 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { fetchUserConsents, fetchActivities, fetchCaseHistory } from '../lib/api';
12
+ import { useCurrentLanguage } from '../lib/useCurrentLanguage';
13
+ import { useOrganization } from './OrganizationContext';
14
+ import { useAuth } from './AuthContext';
15
+ import { useBaseUrl } from './BaseUrlContext';
16
+
17
+ export type DataAvailabilityStatus = 'checking' | 'has-data' | 'empty';
18
+
19
+ type DataAvailabilityContextValue = {
20
+ status: DataAvailabilityStatus;
21
+ };
22
+
23
+ const DataAvailabilityContext = createContext<DataAvailabilityContextValue | null>(null);
24
+
25
+ /**
26
+ * Determines whether the authenticated user has ANY data in the Privacy Center
27
+ * (consents, data-request cases, or activity). When all three are empty we
28
+ * surface a dedicated empty state instead of dropping the user into a shell
29
+ * full of empty tables.
30
+ *
31
+ * Fail-open by design: the underlying fetchers return `null` on non-auth
32
+ * errors (and may reject if a post-refresh retry throws). A `null`/rejected
33
+ * signal is treated as "has data" so a transient or auth error never wrongly
34
+ * locks a user out — the empty state shows ONLY when all three signals
35
+ * conclusively resolve to empty.
36
+ */
37
+ export const DataAvailabilityProvider = ({ children }: { children: ReactNode }) => {
38
+ const { organization } = useOrganization();
39
+ const { accessToken, refreshToken, onError, updateTokens } = useAuth();
40
+ const { consentClient } = useBaseUrl();
41
+ const { langParam } = useCurrentLanguage();
42
+
43
+ const organizationId = organization?.uuid;
44
+ const workspaceId = organization?.workspaces[0]?.uuid;
45
+
46
+ // Memoized to keep the fetch identity stable across renders (mirrors the
47
+ // canonical pattern in ActivitiesList / AuthContext).
48
+ const authContext = useMemo(
49
+ () => ({ accessToken, refreshToken, onError, updateTokens }),
50
+ [accessToken, refreshToken, onError, updateTokens],
51
+ );
52
+
53
+ const [status, setStatus] = useState<DataAvailabilityStatus>('checking');
54
+
55
+ // Monotonic run id guards against a stale in-flight check (org/token changed)
56
+ // overwriting a newer one; mountedRef guards against setState after unmount.
57
+ const runIdRef = useRef(0);
58
+ const mountedRef = useRef(true);
59
+ useEffect(() => {
60
+ mountedRef.current = true;
61
+ return () => {
62
+ mountedRef.current = false;
63
+ };
64
+ }, []);
65
+
66
+ const runCheck = useCallback(async () => {
67
+ if (!organizationId || !workspaceId) return;
68
+
69
+ const runId = ++runIdRef.current;
70
+ setStatus('checking');
71
+
72
+ // Cases require the user's email (same source the request Form reads).
73
+ const contact =
74
+ typeof window !== 'undefined' ? sessionStorage.getItem('email') : null;
75
+
76
+ // Each signal resolves to a boolean "is empty". A `null` result (the
77
+ // fetchers' non-auth error path) or a rejection (rare post-refresh retry
78
+ // throw) resolves to `false` so the signal counts as "has data" — the
79
+ // empty state shows ONLY when all three conclusively resolve to empty.
80
+ const consentsEmptyP: Promise<boolean> = fetchUserConsents({
81
+ organizationId,
82
+ workspaceId,
83
+ limit: 1,
84
+ nominatedLimit: 1,
85
+ language: langParam,
86
+ authContext,
87
+ consentClient,
88
+ })
89
+ .then(
90
+ (v) =>
91
+ !!v &&
92
+ v.status_summary?.total_purposes === 0 &&
93
+ (v.direct?.length ?? 0) === 0 &&
94
+ (v.nominated?.length ?? 0) === 0 &&
95
+ (v.nominators?.length ?? 0) === 0,
96
+ )
97
+ .catch(() => false);
98
+
99
+ // No contact => we can't check cases, so treat as empty and rely on the
100
+ // other two signals' fail-open. With a contact, a null/rejected result
101
+ // fails open (not empty).
102
+ const casesEmptyP: Promise<boolean> = contact
103
+ ? fetchCaseHistory(
104
+ organizationId,
105
+ workspaceId,
106
+ contact,
107
+ 0,
108
+ 1,
109
+ authContext,
110
+ consentClient,
111
+ langParam,
112
+ )
113
+ .then((v) => !!v && (v.data?.length ?? 0) === 0)
114
+ .catch(() => false)
115
+ : Promise.resolve(true);
116
+
117
+ const activitiesEmptyP: Promise<boolean> = fetchActivities(
118
+ organizationId,
119
+ workspaceId,
120
+ 0,
121
+ 1,
122
+ authContext,
123
+ consentClient,
124
+ langParam,
125
+ )
126
+ .then((v) => !!v && (v.pagination?.total_count ?? 0) === 0)
127
+ .catch(() => false);
128
+
129
+ const [consentsEmpty, casesEmpty, activitiesEmpty] = await Promise.all([
130
+ consentsEmptyP,
131
+ casesEmptyP,
132
+ activitiesEmptyP,
133
+ ]);
134
+
135
+ // Ignore if a newer check superseded this one or we unmounted.
136
+ if (!mountedRef.current || runId !== runIdRef.current) return;
137
+
138
+ setStatus(
139
+ consentsEmpty && casesEmpty && activitiesEmpty ? 'empty' : 'has-data',
140
+ );
141
+ }, [organizationId, workspaceId, authContext, consentClient, langParam]);
142
+
143
+ useEffect(() => {
144
+ if (organizationId && workspaceId && accessToken) {
145
+ void runCheck();
146
+ }
147
+ }, [organizationId, workspaceId, accessToken, runCheck]);
148
+
149
+ const value = useMemo<DataAvailabilityContextValue>(
150
+ () => ({ status }),
151
+ [status],
152
+ );
153
+
154
+ return (
155
+ <DataAvailabilityContext.Provider value={value}>
156
+ {children}
157
+ </DataAvailabilityContext.Provider>
158
+ );
159
+ };
160
+
161
+ export const useDataAvailability = (): DataAvailabilityContextValue => {
162
+ const ctx = useContext(DataAvailabilityContext);
163
+ if (!ctx) {
164
+ throw new Error(
165
+ 'useDataAvailability must be used within a DataAvailabilityProvider',
166
+ );
167
+ }
168
+ return ctx;
169
+ };
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "অভিযোগৰ প্ৰকাৰ",
183
183
  "close": "বন্ধ কৰক",
184
184
  "noData": "ডেটা নাই",
185
+ "noDataTitle": "এতিয়ালৈকে দেখুৱাবলৈ একো নাই",
186
+ "noDataDescription": "আপোনাৰ একাউণ্টৰ সৈতে সংযুক্ত কোনো সন্মতি, অনুৰোধ বা কাৰ্য্যাৱলী আমি এতিয়ালৈকে বিচাৰি পোৱা নাই। উপলব্ধ হোৱাৰ লগে লগে আপোনাৰ গোপনীয়তা ৰেকৰ্ডসমূহ ইয়াত দেখা যাব।",
185
187
  "rowsPerPage": "পৃষ্ঠাত শাৰী:",
186
188
  "pageOf": "পৃষ্ঠা {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}}-ৰ {{count}} শাৰী নিৰ্বাচিত",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "शिकायत प्रकार",
183
183
  "close": "बन्द करीं",
184
184
  "noData": "कोनो डेटा नै",
185
+ "noDataTitle": "अबहीं देखावे खातिर कुछ नइखे",
186
+ "noDataDescription": "रउरा खाता से जुड़ल कवनो सहमति, अनुरोध, या गतिविधि अबहीं तक ना मिलल। रउरा गोपनीयता रिकॉर्ड उपलब्ध होते ई इहाँ देखाई दिही।",
185
187
  "rowsPerPage": "पृष्ठ प्रति पंक्ति:",
186
188
  "pageOf": "पृष्ठ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} में से {{count}} पंक्ति चुनल गइल",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "অভিযোগের ধরন",
183
183
  "close": "বন্ধ করুন",
184
184
  "noData": "কোনো ডেটা নেই",
185
+ "noDataTitle": "এখনো দেখানোর মতো কিছু নেই",
186
+ "noDataDescription": "আপনার অ্যাকাউন্টের সাথে সম্পর্কিত কোনো সম্মতি, অনুরোধ বা ক্রিয়াকলাপ আমরা এখনো খুঁজে পাইনি। উপলব্ধ হওয়ার সাথে সাথেই আপনার গোপনীয়তা রেকর্ডগুলি এখানে প্রদর্শিত হবে।",
185
187
  "rowsPerPage": "প্রতি পৃষ্ঠায় সারি:",
186
188
  "pageOf": "পৃষ্ঠা {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}}-এর মধ্যে {{count}} সারি নির্বাচিত",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "गुनासो थाखो",
183
183
  "close": "बन्द खालाम",
184
184
  "noData": "डेटा नंगौ",
185
+ "noDataTitle": "दानो दिनथिनो थाखाय जेबो गैया",
186
+ "noDataDescription": "दानो सम नोंथांनि एकाउन्टजों सोमोन्दो थानाय जेबो सहमति, हाबाय एबा खामानि जों मोननो हायाखै। नोंथांनि गोपनीयथा रिकर्डफोर मोननो हानाय बादि बेयावनो दिनथिजागोन।",
185
187
  "rowsPerPage": "पृष्ठनि पंक्ति:",
186
188
  "pageOf": "पृष्ठ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} निफ्राय {{count}} पंक्ति सायख 'खै",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "शिकायत प्रकार",
183
183
  "close": "बंद करो",
184
184
  "noData": "कोनो डेटा नै",
185
+ "noDataTitle": "अजें दिक्खाने आस्तै किछ नेईं",
186
+ "noDataDescription": "तुंदे खाते कन्नै जुड़ी दी कोई बी सहमति, बेनती जां गतिविधि अजें तक नेईं मिली। तुंदे गोपनीयता रिकॉर्ड उपलब्ध होंदे गै इत्थें दिक्खने गी मिलङन।",
185
187
  "rowsPerPage": "पृष्ठ प्रति पंक्ति:",
186
188
  "pageOf": "पृष्ठ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्ति चुनल गेल",
@@ -195,6 +195,8 @@
195
195
  "grievanceTypes": "Grievance Types",
196
196
  "close": "Close",
197
197
  "noData": "No Data",
198
+ "noDataTitle": "Nothing to show yet",
199
+ "noDataDescription": "We couldn't find any consents, requests, or activity linked to your account yet. Your privacy records will appear here once they're available.",
198
200
  "rowsPerPage": "Rows per page:",
199
201
  "pageOf": "Page {{current}} of {{total}}",
200
202
  "rowsSelected": "{{count}} of {{total}} row(s) selected",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "तक्रार प्रकार",
183
183
  "close": "बंद करात",
184
184
  "noData": "डेटा ना",
185
+ "noDataTitle": "अजून दाखोवपाक कांय ना",
186
+ "noDataDescription": "तुमच्या खात्याकडेन जोडिल्ली कसलीच संमती, मागण्यो वा कृती आमकां अजून मेळूंक नात. उपलब्ध जातकच तुमच्यो गोपनीयता नोंदी हांगा दिसतल्यो.",
185
187
  "rowsPerPage": "पानाक पंक्ती:",
186
188
  "pageOf": "पान {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्ती निवडली",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "ફરિયાદ પ્રકારો",
183
183
  "close": "બંધ કરો",
184
184
  "noData": "ડેટા નથી",
185
+ "noDataTitle": "હજુ સુધી બતાવવા માટે કંઈ નથી",
186
+ "noDataDescription": "તમારા ખાતા સાથે જોડાયેલી કોઈ સંમતિઓ, વિનંતીઓ અથવા પ્રવૃત્તિ અમને હજુ સુધી મળી નથી. તમારા ગોપનીયતા રેકોર્ડ ઉપલબ્ધ થતાં જ અહીં દેખાશે.",
185
187
  "rowsPerPage": "દર પૃષ્ઠ પંક્તિઓ:",
186
188
  "pageOf": "પૃષ્ઠ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} માંથી {{count}} પંક્તિ પસંદ",
@@ -185,6 +185,8 @@
185
185
  "grievanceTypes": "शिकायत प्रकार",
186
186
  "close": "बंद करें",
187
187
  "noData": "कोई डेटा नहीं",
188
+ "noDataTitle": "अभी दिखाने के लिए कुछ नहीं",
189
+ "noDataDescription": "हमें अभी तक आपके खाते से जुड़ी कोई सहमति, अनुरोध या गतिविधि नहीं मिली। उपलब्ध होते ही आपके गोपनीयता रिकॉर्ड यहाँ दिखाई देंगे।",
188
190
  "rowsPerPage": "प्रति पृष्ठ पंक्तियाँ:",
189
191
  "pageOf": "पृष्ठ {{current}} / {{total}}",
190
192
  "rowsSelected": "कुल {{total}} में से {{count}} पंक्तियाँ चयनित",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "ಪರಾತಿ ವಿಧಗಳು",
183
183
  "close": "ಮುಚ್ಚಿ",
184
184
  "noData": "ಡೇಟಾ ಇಲ್ಲ",
185
+ "noDataTitle": "ಇನ್ನೂ ತೋರಿಸಲು ಏನೂ ಇಲ್ಲ",
186
+ "noDataDescription": "ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಬಂಧಿಸಿದ ಯಾವುದೇ ಸಮ್ಮತಿಗಳು, ವಿನಂತಿಗಳು ಅಥವಾ ಚಟುವಟಿಕೆ ನಮಗೆ ಇನ್ನೂ ಕಂಡುಬಂದಿಲ್ಲ. ನಿಮ್ಮ ಗೌಪ್ಯತಾ ದಾಖಲೆಗಳು ಲಭ್ಯವಾದ ತಕ್ಷಣ ಅವು ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ.",
185
187
  "rowsPerPage": "ಪುಟಕ್ಕೆ ಸಾಲುಗಳು:",
186
188
  "pageOf": "ಪುಟ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} ರಲ್ಲಿ {{count}} ಸಾಲು ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "شکایت قسم",
183
183
  "close": "بند کرو",
184
184
  "noData": "ڈیٹا کانہہ",
185
+ "noDataTitle": "ابٕ تام ہاوُن خٲطرٕ کانٛہہ نٕ",
186
+ "noDataDescription": "اسہِ تُہنٛدِ اکاؤنٹَس سٕتۍ جُڑٕمٕژ کانٛہہِ رضامندی، درخواستٕ یا سرگرمی ابٕ تام نٕ لَب۔ تُہنٛد رازداری ریکارڈ یہِ دستیاب سپدِتھ یِتہِ کنہِ وُچھنہٕ یِیِہٕ۔",
185
187
  "rowsPerPage": "صفحہ فی قطاراں:",
186
188
  "pageOf": "صفحہ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} منز {{count}} قطار چُنِت",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "शिकायत प्रकार",
183
183
  "close": "बन्द करू",
184
184
  "noData": "कोनो डेटा नै",
185
+ "noDataTitle": "अखन तक देखाबय लेल किछु नहि",
186
+ "noDataDescription": "अहाँक खाता सँ जुड़ल कोनो सहमति, अनुरोध, वा गतिविधि अखन तक नहि भेटल। उपलब्ध होइते अहाँक गोपनीयता रिकॉर्ड एतय देखाएत।",
185
187
  "rowsPerPage": "पृष्ठ प्रति पंक्ति:",
186
188
  "pageOf": "पृष्ठ {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्ति चुनल गेल",
@@ -182,6 +182,8 @@
182
182
  "grievanceTypes": "പരാതി തരങ്ങൾ",
183
183
  "close": "അടയ്ക്കുക",
184
184
  "noData": "ഡാറ്റയില്ല",
185
+ "noDataTitle": "ഇതുവരെ കാണിക്കാൻ ഒന്നുമില്ല",
186
+ "noDataDescription": "നിങ്ങളുടെ അക്കൗണ്ടുമായി ബന്ധിപ്പിച്ച സമ്മതങ്ങളോ അഭ്യർത്ഥനകളോ പ്രവൃത്തിയോ ഇതുവരെ ഞങ്ങൾക്ക് കണ്ടെത്താനായില്ല. നിങ്ങളുടെ സ്വകാര്യതാ രേഖകൾ ലഭ്യമാകുമ്പോൾ അവ ഇവിടെ ദൃശ്യമാകും.",
185
187
  "rowsPerPage": "പേജിന് വരികൾ:",
186
188
  "pageOf": "പേജ് {{current}} / {{total}}",
187
189
  "rowsSelected": "{{total}}-ൽ {{count}} വരി തിരഞ്ഞെടുത്തു",