@redacto.io/consent-sdk-react 9.4.0-beta.0 → 9.5.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 (43) hide show
  1. package/.turbo/turbo-build.log +12 -12
  2. package/CHANGELOG.md +13 -0
  3. package/dist/privacy-center.d.mts +1 -0
  4. package/dist/privacy-center.d.ts +1 -0
  5. package/dist/privacy-center.js +1516 -1378
  6. package/dist/privacy-center.mjs +1199 -1061
  7. package/package.json +1 -1
  8. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -4
  9. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +5 -0
  10. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +6 -16
  11. package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +36 -0
  12. package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +97 -0
  13. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +29 -6
  14. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +92 -0
  15. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +15 -3
  16. package/src/RedactoPrivacyCenter/context/types.ts +4 -0
  17. package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +109 -0
  18. package/src/RedactoPrivacyCenter/lib/utils.ts +83 -79
  19. package/src/RedactoPrivacyCenter/locales/as/translation.json +2 -0
  20. package/src/RedactoPrivacyCenter/locales/bho/translation.json +2 -0
  21. package/src/RedactoPrivacyCenter/locales/bn/translation.json +2 -0
  22. package/src/RedactoPrivacyCenter/locales/brx/translation.json +2 -0
  23. package/src/RedactoPrivacyCenter/locales/doi/translation.json +2 -0
  24. package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -0
  25. package/src/RedactoPrivacyCenter/locales/gom/translation.json +2 -0
  26. package/src/RedactoPrivacyCenter/locales/gu/translation.json +2 -0
  27. package/src/RedactoPrivacyCenter/locales/hi/translation.json +2 -0
  28. package/src/RedactoPrivacyCenter/locales/kn/translation.json +2 -0
  29. package/src/RedactoPrivacyCenter/locales/ks/translation.json +2 -0
  30. package/src/RedactoPrivacyCenter/locales/mai/translation.json +2 -0
  31. package/src/RedactoPrivacyCenter/locales/ml/translation.json +2 -0
  32. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +2 -0
  33. package/src/RedactoPrivacyCenter/locales/mr/translation.json +2 -0
  34. package/src/RedactoPrivacyCenter/locales/ne/translation.json +2 -0
  35. package/src/RedactoPrivacyCenter/locales/or/translation.json +2 -0
  36. package/src/RedactoPrivacyCenter/locales/pa/translation.json +2 -0
  37. package/src/RedactoPrivacyCenter/locales/sa/translation.json +2 -0
  38. package/src/RedactoPrivacyCenter/locales/sat/translation.json +2 -0
  39. package/src/RedactoPrivacyCenter/locales/sd/translation.json +2 -0
  40. package/src/RedactoPrivacyCenter/locales/ta/translation.json +2 -0
  41. package/src/RedactoPrivacyCenter/locales/te/translation.json +2 -0
  42. package/src/RedactoPrivacyCenter/locales/ur/translation.json +2 -0
  43. package/tests/resolveContactFromJwt.test.ts +44 -0
@@ -28,6 +28,30 @@ let deadRefreshToken: string | null = null;
28
28
  let transientRefreshFailureUntil = 0;
29
29
  let transientRefreshBackoffMs = 0;
30
30
 
31
+ // ─── Terminal "session expired" signal ──────────────────────────────────────
32
+ // The circuit breaker above stops the *network* (a dead refresh token costs one
33
+ // request). This listener lets the React tree learn that the session is
34
+ // terminally over so it can show a dedicated "session expired" screen and gate
35
+ // every further fetch — instead of silently rendering empty/stale content or
36
+ // hard-navigating away. A single module-level listener, consistent with the
37
+ // single-instance breaker singleton above (one Privacy Center per page).
38
+ let sessionExpiredListener: (() => void) | null = null;
39
+
40
+ /**
41
+ * Register (or clear, with `null`) the callback fired when a refresh terminally
42
+ * fails — i.e. the refresh token is expired/invalid and re-authentication is
43
+ * required. AuthProvider registers its `markSessionExpired` here. Exported for
44
+ * that wiring and for tests; not part of the package's public API.
45
+ */
46
+ export const setSessionExpiredListener = (fn: (() => void) | null): void => {
47
+ sessionExpiredListener = fn;
48
+ };
49
+
50
+ /** Notify the React tree that the session has terminally expired. */
51
+ const notifySessionExpired = (): void => {
52
+ sessionExpiredListener?.();
53
+ };
54
+
31
55
  // Buffer time in seconds before token expiration to proactively refresh
32
56
  // Using 30 seconds to ensure tokens are refreshed before they expire
33
57
  const TOKEN_EXPIRY_BUFFER_SECONDS = 30;
@@ -220,6 +244,34 @@ export const decodeJwtToken = (token: string): { organisation_uuid?: string; org
220
244
  }
221
245
  };
222
246
 
247
+ /**
248
+ * Resolve the user's contact identifier from a decoded JWT.
249
+ *
250
+ * Identifiers live under `user_data` (primary_email / primary_mobile /
251
+ * org_user_id); some token shapes also expose `contact`/`primary_email` at the
252
+ * top level. Email is preferred, but a mobile-only login must still resolve to
253
+ * the phone (or org_user_id) so case/grievance and consent-management actions —
254
+ * which key off this contact — work without an email. The backend matches
255
+ * identifiers type-aware (phone-to-phone), so storing the mobile here is safe.
256
+ */
257
+ export const resolveContactFromJwt = (
258
+ decoded: { [key: string]: any } | null,
259
+ ): string | null => {
260
+ if (!decoded) {
261
+ return null;
262
+ }
263
+ const userData = (decoded.user_data as Record<string, unknown>) || {};
264
+ const candidate =
265
+ decoded.contact ||
266
+ decoded.primary_email ||
267
+ userData.primary_email ||
268
+ userData.primary_mobile ||
269
+ userData.org_user_id;
270
+ return typeof candidate === 'string' && candidate.trim() !== ''
271
+ ? candidate
272
+ : null;
273
+ };
274
+
223
275
  /**
224
276
  * Decode JWT payload and check if token is expired or will expire soon
225
277
  * @param token - JWT token string
@@ -350,8 +402,9 @@ export const checkAndRefreshAuth = async (
350
402
 
351
403
  const refreshToken = getStoredRefreshToken();
352
404
  if (!refreshToken) {
353
- // No refresh token available - user needs to sign in again
405
+ // No refresh token available - terminal: surface the session-expired screen.
354
406
  signOutPrivacyCenter();
407
+ notifySessionExpired();
355
408
  return false;
356
409
  }
357
410
 
@@ -376,6 +429,7 @@ export const checkAndRefreshAuth = async (
376
429
  console.error('Refreshed access token is already expired - clearing auth state');
377
430
  markRefreshTokenDead(refreshToken);
378
431
  signOutPrivacyCenter();
432
+ notifySessionExpired();
379
433
  return false;
380
434
  }
381
435
  sessionStorage.setItem('access_token', newAccessToken);
@@ -386,6 +440,7 @@ export const checkAndRefreshAuth = async (
386
440
  console.error('Refresh response missing access token - clearing auth state');
387
441
  markRefreshTokenDead(refreshToken);
388
442
  signOutPrivacyCenter();
443
+ notifySessionExpired();
389
444
  return false;
390
445
  }
391
446
 
@@ -401,10 +456,10 @@ export const checkAndRefreshAuth = async (
401
456
  }
402
457
  return isValid;
403
458
  } else {
404
- // Refresh token expired/revoked or server returned failure
405
- // This is an expected scenario - silently handle it
459
+ // Refresh token expired/revoked or server returned failure - terminal.
406
460
  markRefreshTokenDead(refreshToken);
407
461
  signOutPrivacyCenter();
462
+ notifySessionExpired();
408
463
  return false;
409
464
  }
410
465
  } catch (error: any) {
@@ -412,6 +467,7 @@ export const checkAndRefreshAuth = async (
412
467
  // Refresh token is expired / not found / already used - trip the breaker.
413
468
  markRefreshTokenDead(refreshToken);
414
469
  signOutPrivacyCenter();
470
+ notifySessionExpired();
415
471
  return false;
416
472
  }
417
473
 
@@ -559,7 +615,9 @@ export const ensureValidToken = async (
559
615
  const performTokenRefresh = async (
560
616
  organizationId?: string,
561
617
  workspaceId?: string,
562
- slug?: string,
618
+ // Retained for call-site compatibility; redirect-by-slug was removed in favor
619
+ // of the terminal "session expired" screen, so the value is intentionally unused.
620
+ _slug?: string,
563
621
  authContext?: {
564
622
  accessToken?: string;
565
623
  refreshToken?: string;
@@ -571,20 +629,15 @@ const performTokenRefresh = async (
571
629
  // Get organizationId and workspaceId from sessionStorage if not provided
572
630
  const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
573
631
  const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
574
- const orgSlug = slug || sessionStorage.getItem('pc_org_slug') || '';
575
632
 
576
633
  if (!orgId || !wsId) {
577
634
  console.error('[Token Refresh] Missing organizationId or workspaceId');
578
635
  const error = new Error('Missing organizationId or workspaceId');
579
- // Call onError if provided, check return value before proceeding
580
- if (authContext?.onError) {
581
- const shouldProceed = authContext.onError(error);
582
- if (!shouldProceed) {
583
- return null;
584
- }
585
- }
636
+ // Inform the host (informational only — we no longer auto-navigate; the
637
+ // component surfaces a terminal "session expired" screen instead).
638
+ authContext?.onError?.(error);
586
639
  signOutPrivacyCenter();
587
- redirectToSignIn(orgSlug, authContext?.onError);
640
+ notifySessionExpired();
588
641
  return null;
589
642
  }
590
643
 
@@ -603,15 +656,10 @@ const performTokenRefresh = async (
603
656
  hasRefreshTokenInStorage: !!getStoredRefreshToken(),
604
657
  });
605
658
  const error = new Error('No refresh token available');
606
- // Call onError if provided, check return value before proceeding
607
- if (authContext?.onError) {
608
- const shouldProceed = authContext.onError(error);
609
- if (!shouldProceed) {
610
- return null;
611
- }
612
- }
659
+ // Inform the host (informational only — we no longer auto-navigate).
660
+ authContext?.onError?.(error);
613
661
  signOutPrivacyCenter();
614
- redirectToSignIn(orgSlug, authContext?.onError);
662
+ notifySessionExpired();
615
663
  return null;
616
664
  }
617
665
 
@@ -631,15 +679,10 @@ const performTokenRefresh = async (
631
679
  console.error('[Token Refresh] New access token is already expired');
632
680
  markRefreshTokenDead(refreshToken);
633
681
  const error = new Error('New access token is already expired');
634
- // Call onError if provided, check return value before proceeding
635
- if (authContext?.onError) {
636
- const shouldProceed = authContext.onError(error);
637
- if (!shouldProceed) {
638
- return null;
639
- }
640
- }
682
+ // Inform the host (informational only — we no longer auto-navigate).
683
+ authContext?.onError?.(error);
641
684
  signOutPrivacyCenter();
642
- redirectToSignIn(orgSlug, authContext?.onError);
685
+ notifySessionExpired();
643
686
  return null;
644
687
  }
645
688
 
@@ -662,47 +705,32 @@ const performTokenRefresh = async (
662
705
  console.error('[Token Refresh] Response missing access token');
663
706
  markRefreshTokenDead(refreshToken);
664
707
  const error = new Error('Response missing access token');
665
- // Call onError if provided, check return value before proceeding
666
- if (authContext?.onError) {
667
- const shouldProceed = authContext.onError(error);
668
- if (!shouldProceed) {
669
- return null;
670
- }
671
- }
708
+ // Inform the host (informational only — we no longer auto-navigate).
709
+ authContext?.onError?.(error);
672
710
  signOutPrivacyCenter();
673
- redirectToSignIn(orgSlug, authContext?.onError);
711
+ notifySessionExpired();
674
712
  return null;
675
713
  }
676
714
  } else {
677
- // Refresh token expired/revoked or server returned failure
715
+ // Refresh token expired/revoked or server returned failure - terminal.
678
716
  markRefreshTokenDead(refreshToken);
679
717
  const error = new Error('Refresh token expired or revoked');
680
- // Call onError if provided, check return value before proceeding
681
- if (authContext?.onError) {
682
- const shouldProceed = authContext.onError(error);
683
- if (!shouldProceed) {
684
- return null;
685
- }
686
- }
718
+ // Inform the host (informational only — we no longer auto-navigate).
719
+ authContext?.onError?.(error);
687
720
  signOutPrivacyCenter();
688
- redirectToSignIn(orgSlug, authContext?.onError);
721
+ notifySessionExpired();
689
722
  return null;
690
723
  }
691
724
  } catch (error: any) {
692
725
  if (isTerminalRefreshError(error)) {
693
726
  // 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).
727
+ // breaker so this dead token is never re-submitted, then surface the
728
+ // session-expired screen (onError is informational; we no longer navigate).
696
729
  markRefreshTokenDead(refreshToken);
697
730
  const err = new Error('Refresh token expired or revoked');
698
- if (authContext?.onError) {
699
- const shouldProceed = authContext.onError(err);
700
- if (!shouldProceed) {
701
- return null;
702
- }
703
- }
731
+ authContext?.onError?.(err);
704
732
  signOutPrivacyCenter();
705
- redirectToSignIn(orgSlug, authContext?.onError);
733
+ notifySessionExpired();
706
734
  return null;
707
735
  }
708
736
 
@@ -718,30 +746,6 @@ const performTokenRefresh = async (
718
746
  }
719
747
  };
720
748
 
721
- /**
722
- * Helper to redirect to sign-in page
723
- */
724
- const redirectToSignIn = (slug?: string, onError?: (error: Error) => boolean): void => {
725
- if (typeof window === 'undefined') return;
726
-
727
- // If onError is provided, call it and check return value
728
- if (onError) {
729
- const error = new Error('Authentication failed - redirecting to sign-in');
730
- const shouldProceed = onError(error);
731
- if (!shouldProceed) {
732
- // User wants to handle navigation themselves
733
- return;
734
- }
735
- }
736
-
737
- const orgSlug = slug || sessionStorage.getItem('pc_org_slug') || '';
738
- if (orgSlug) {
739
- window.location.href = `/privacy-center/${orgSlug}/sign-in`;
740
- } else {
741
- window.location.href = '/privacy-center';
742
- }
743
- };
744
-
745
749
  // Activity helper functions
746
750
 
747
751
  export type ActivityBadgeStatus =
@@ -184,6 +184,8 @@
184
184
  "noData": "ডেটা নাই",
185
185
  "noDataTitle": "এতিয়ালৈকে দেখুৱাবলৈ একো নাই",
186
186
  "noDataDescription": "আপোনাৰ একাউণ্টৰ সৈতে সংযুক্ত কোনো সন্মতি, অনুৰোধ বা কাৰ্য্যাৱলী আমি এতিয়ালৈকে বিচাৰি পোৱা নাই। উপলব্ধ হোৱাৰ লগে লগে আপোনাৰ গোপনীয়তা ৰেকৰ্ডসমূহ ইয়াত দেখা যাব।",
187
+ "sessionExpiredTitle": "আপোনাৰ ছেচন শেষ হৈছে",
188
+ "sessionExpiredDescription": "অব্যাহত ৰাখিবলৈ অনুগ্ৰহ কৰি পুনৰ ছাইন ইন কৰক।",
187
189
  "rowsPerPage": "পৃষ্ঠাত শাৰী:",
188
190
  "pageOf": "পৃষ্ঠা {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}}-ৰ {{count}} শাৰী নিৰ্বাচিত",
@@ -184,6 +184,8 @@
184
184
  "noData": "कोनो डेटा नै",
185
185
  "noDataTitle": "अबहीं देखावे खातिर कुछ नइखे",
186
186
  "noDataDescription": "रउरा खाता से जुड़ल कवनो सहमति, अनुरोध, या गतिविधि अबहीं तक ना मिलल। रउरा गोपनीयता रिकॉर्ड उपलब्ध होते ई इहाँ देखाई दिही।",
187
+ "sessionExpiredTitle": "रउआ के सत्र खतम हो गइल बा",
188
+ "sessionExpiredDescription": "जारी रखे खातिर कृपया फेर से साइन इन करीं।",
187
189
  "rowsPerPage": "पृष्ठ प्रति पंक्ति:",
188
190
  "pageOf": "पृष्ठ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} में से {{count}} पंक्ति चुनल गइल",
@@ -184,6 +184,8 @@
184
184
  "noData": "কোনো ডেটা নেই",
185
185
  "noDataTitle": "এখনো দেখানোর মতো কিছু নেই",
186
186
  "noDataDescription": "আপনার অ্যাকাউন্টের সাথে সম্পর্কিত কোনো সম্মতি, অনুরোধ বা ক্রিয়াকলাপ আমরা এখনো খুঁজে পাইনি। উপলব্ধ হওয়ার সাথে সাথেই আপনার গোপনীয়তা রেকর্ডগুলি এখানে প্রদর্শিত হবে।",
187
+ "sessionExpiredTitle": "আপনার সেশন মেয়াদ শেষ হয়ে গেছে",
188
+ "sessionExpiredDescription": "চালিয়ে যেতে অনুগ্রহ করে আবার সাইন ইন করুন।",
187
189
  "rowsPerPage": "প্রতি পৃষ্ঠায় সারি:",
188
190
  "pageOf": "পৃষ্ঠা {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}}-এর মধ্যে {{count}} সারি নির্বাচিত",
@@ -184,6 +184,8 @@
184
184
  "noData": "डेटा नंगौ",
185
185
  "noDataTitle": "दानो दिनथिनो थाखाय जेबो गैया",
186
186
  "noDataDescription": "दानो सम नोंथांनि एकाउन्टजों सोमोन्दो थानाय जेबो सहमति, हाबाय एबा खामानि जों मोननो हायाखै। नोंथांनि गोपनीयथा रिकर्डफोर मोननो हानाय बादि बेयावनो दिनथिजागोन।",
187
+ "sessionExpiredTitle": "नोंथांनि सेसन जोबबाय",
188
+ "sessionExpiredDescription": "सालायनो थाखाय अननानै फिन साइन इन खालाम।",
187
189
  "rowsPerPage": "पृष्ठनि पंक्ति:",
188
190
  "pageOf": "पृष्ठ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} निफ्राय {{count}} पंक्ति सायख 'खै",
@@ -184,6 +184,8 @@
184
184
  "noData": "कोनो डेटा नै",
185
185
  "noDataTitle": "अजें दिक्खाने आस्तै किछ नेईं",
186
186
  "noDataDescription": "तुंदे खाते कन्नै जुड़ी दी कोई बी सहमति, बेनती जां गतिविधि अजें तक नेईं मिली। तुंदे गोपनीयता रिकॉर्ड उपलब्ध होंदे गै इत्थें दिक्खने गी मिलङन।",
187
+ "sessionExpiredTitle": "तुंदा सत्र खतम होई गेआ ऐ",
188
+ "sessionExpiredDescription": "जारी रक्खने आस्तै कृपा करियै फिरी साइन इन करो।",
187
189
  "rowsPerPage": "पृष्ठ प्रति पंक्ति:",
188
190
  "pageOf": "पृष्ठ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्ति चुनल गेल",
@@ -200,6 +200,8 @@
200
200
  "noData": "No Data",
201
201
  "noDataTitle": "Nothing to show yet",
202
202
  "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.",
203
+ "sessionExpiredTitle": "Your session has expired",
204
+ "sessionExpiredDescription": "Please sign in again to continue.",
203
205
  "rowsPerPage": "Rows per page:",
204
206
  "pageOf": "Page {{current}} of {{total}}",
205
207
  "rowsSelected": "{{count}} of {{total}} row(s) selected",
@@ -184,6 +184,8 @@
184
184
  "noData": "डेटा ना",
185
185
  "noDataTitle": "अजून दाखोवपाक कांय ना",
186
186
  "noDataDescription": "तुमच्या खात्याकडेन जोडिल्ली कसलीच संमती, मागण्यो वा कृती आमकां अजून मेळूंक नात. उपलब्ध जातकच तुमच्यो गोपनीयता नोंदी हांगा दिसतल्यो.",
187
+ "sessionExpiredTitle": "तुमचें सत्र सोंपलां",
188
+ "sessionExpiredDescription": "फुडें वचपाक उपकार करून परतून साइन इन करात.",
187
189
  "rowsPerPage": "पानाक पंक्ती:",
188
190
  "pageOf": "पान {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्ती निवडली",
@@ -184,6 +184,8 @@
184
184
  "noData": "ડેટા નથી",
185
185
  "noDataTitle": "હજુ સુધી બતાવવા માટે કંઈ નથી",
186
186
  "noDataDescription": "તમારા ખાતા સાથે જોડાયેલી કોઈ સંમતિઓ, વિનંતીઓ અથવા પ્રવૃત્તિ અમને હજુ સુધી મળી નથી. તમારા ગોપનીયતા રેકોર્ડ ઉપલબ્ધ થતાં જ અહીં દેખાશે.",
187
+ "sessionExpiredTitle": "તમારું સત્ર સમાપ્ત થઈ ગયું છે",
188
+ "sessionExpiredDescription": "ચાલુ રાખવા માટે કૃપા કરીને ફરીથી સાઇન ઇન કરો.",
187
189
  "rowsPerPage": "દર પૃષ્ઠ પંક્તિઓ:",
188
190
  "pageOf": "પૃષ્ઠ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} માંથી {{count}} પંક્તિ પસંદ",
@@ -187,6 +187,8 @@
187
187
  "noData": "कोई डेटा नहीं",
188
188
  "noDataTitle": "अभी दिखाने के लिए कुछ नहीं",
189
189
  "noDataDescription": "हमें अभी तक आपके खाते से जुड़ी कोई सहमति, अनुरोध या गतिविधि नहीं मिली। उपलब्ध होते ही आपके गोपनीयता रिकॉर्ड यहाँ दिखाई देंगे।",
190
+ "sessionExpiredTitle": "आपका सत्र समाप्त हो गया है",
191
+ "sessionExpiredDescription": "जारी रखने के लिए कृपया फिर से साइन इन करें।",
190
192
  "rowsPerPage": "प्रति पृष्ठ पंक्तियाँ:",
191
193
  "pageOf": "पृष्ठ {{current}} / {{total}}",
192
194
  "rowsSelected": "कुल {{total}} में से {{count}} पंक्तियाँ चयनित",
@@ -184,6 +184,8 @@
184
184
  "noData": "ಡೇಟಾ ಇಲ್ಲ",
185
185
  "noDataTitle": "ಇನ್ನೂ ತೋರಿಸಲು ಏನೂ ಇಲ್ಲ",
186
186
  "noDataDescription": "ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಬಂಧಿಸಿದ ಯಾವುದೇ ಸಮ್ಮತಿಗಳು, ವಿನಂತಿಗಳು ಅಥವಾ ಚಟುವಟಿಕೆ ನಮಗೆ ಇನ್ನೂ ಕಂಡುಬಂದಿಲ್ಲ. ನಿಮ್ಮ ಗೌಪ್ಯತಾ ದಾಖಲೆಗಳು ಲಭ್ಯವಾದ ತಕ್ಷಣ ಅವು ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ.",
187
+ "sessionExpiredTitle": "ನಿಮ್ಮ ಸೆಷನ್ ಅವಧಿ ಮುಗಿದಿದೆ",
188
+ "sessionExpiredDescription": "ಮುಂದುವರಿಸಲು ದಯವಿಟ್ಟು ಮತ್ತೆ ಸೈನ್ ಇನ್ ಮಾಡಿ.",
187
189
  "rowsPerPage": "ಪುಟಕ್ಕೆ ಸಾಲುಗಳು:",
188
190
  "pageOf": "ಪುಟ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} ರಲ್ಲಿ {{count}} ಸಾಲು ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ",
@@ -184,6 +184,8 @@
184
184
  "noData": "ڈیٹا کانہہ",
185
185
  "noDataTitle": "ابٕ تام ہاوُن خٲطرٕ کانٛہہ نٕ",
186
186
  "noDataDescription": "اسہِ تُہنٛدِ اکاؤنٹَس سٕتۍ جُڑٕمٕژ کانٛہہِ رضامندی، درخواستٕ یا سرگرمی ابٕ تام نٕ لَب۔ تُہنٛد رازداری ریکارڈ یہِ دستیاب سپدِتھ یِتہِ کنہِ وُچھنہٕ یِیِہٕ۔",
187
+ "sessionExpiredTitle": "تُہنٛد سیشَن ختم گۄمُت",
188
+ "sessionExpiredDescription": "جاری رکھنہٕ خٲطرٕ مہربانی کرِتھ دوبارٕ سائن اِن کریو۔",
187
189
  "rowsPerPage": "صفحہ فی قطاراں:",
188
190
  "pageOf": "صفحہ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} منز {{count}} قطار چُنِت",
@@ -184,6 +184,8 @@
184
184
  "noData": "कोनो डेटा नै",
185
185
  "noDataTitle": "अखन तक देखाबय लेल किछु नहि",
186
186
  "noDataDescription": "अहाँक खाता सँ जुड़ल कोनो सहमति, अनुरोध, वा गतिविधि अखन तक नहि भेटल। उपलब्ध होइते अहाँक गोपनीयता रिकॉर्ड एतय देखाएत।",
187
+ "sessionExpiredTitle": "अहाँक सत्र समाप्त भऽ गेल अछि",
188
+ "sessionExpiredDescription": "जारी राखबाक लेल कृपया फेर सँ साइन इन करू।",
187
189
  "rowsPerPage": "पृष्ठ प्रति पंक्ति:",
188
190
  "pageOf": "पृष्ठ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्ति चुनल गेल",
@@ -184,6 +184,8 @@
184
184
  "noData": "ഡാറ്റയില്ല",
185
185
  "noDataTitle": "ഇതുവരെ കാണിക്കാൻ ഒന്നുമില്ല",
186
186
  "noDataDescription": "നിങ്ങളുടെ അക്കൗണ്ടുമായി ബന്ധിപ്പിച്ച സമ്മതങ്ങളോ അഭ്യർത്ഥനകളോ പ്രവൃത്തിയോ ഇതുവരെ ഞങ്ങൾക്ക് കണ്ടെത്താനായില്ല. നിങ്ങളുടെ സ്വകാര്യതാ രേഖകൾ ലഭ്യമാകുമ്പോൾ അവ ഇവിടെ ദൃശ്യമാകും.",
187
+ "sessionExpiredTitle": "നിങ്ങളുടെ സെഷൻ കാലഹരണപ്പെട്ടു",
188
+ "sessionExpiredDescription": "തുടരാൻ ദയവായി വീണ്ടും സൈൻ ഇൻ ചെയ്യുക.",
187
189
  "rowsPerPage": "പേജിന് വരികൾ:",
188
190
  "pageOf": "പേജ് {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}}-ൽ {{count}} വരി തിരഞ്ഞെടുത്തു",
@@ -184,6 +184,8 @@
184
184
  "noData": "ꯗꯦꯇꯥ ꯂꯩꯇꯦ",
185
185
  "noDataTitle": "ꯍꯧꯖꯤꯛ ꯐꯥꯎꯕ ꯎꯠꯅꯕ ꯀꯔꯤꯁꯨ ꯂꯩꯇꯦ",
186
186
  "noDataDescription": "ꯍꯧꯖꯤꯛ ꯐꯥꯎꯕ ꯑꯗꯣꯝꯒꯤ ꯑꯀꯥꯎꯟꯇꯀ ꯁꯝꯅꯗꯨꯅ ꯀꯅꯁꯦꯟꯇ, ꯍꯧꯔꯛꯐꯝ, ꯅꯠꯇ꯭ꯔꯒ ꯈꯨꯗꯝꯕꯤꯠ ꯑꯃꯠꯇ ꯐꯪꯗꯔꯦ꯫ ꯑꯗꯣꯝꯒꯤ ꯄ꯭ꯔꯥꯏꯚꯦꯁꯤ ꯔꯤꯀꯣꯔꯗꯁꯤꯡ ꯐꯪꯂꯛꯄ ꯃꯇꯃꯗ ꯃꯐꯝ ꯑꯁꯤꯗ ꯎꯔꯛꯀꯅꯤ꯫",
187
+ "sessionExpiredTitle": "ꯑꯗꯣꯝꯒꯤ ꯁꯦꯁꯟ ꯂꯣꯏꯔꯦ",
188
+ "sessionExpiredDescription": "ꯃꯈꯥ ꯆꯠꯊꯔꯛꯅꯕ ꯆꯥꯅꯕꯤꯗꯨꯅ ꯑꯃꯨꯛ ꯍꯟꯅ ꯁꯥꯏꯟ ꯏꯟ ꯇꯧꯕꯤꯌꯨ꯫",
187
189
  "rowsPerPage": "ꯂꯦꯡꯒꯤ ꯃꯔꯨꯑꯣꯏꯕ:",
188
190
  "pageOf": "ꯂꯦꯡ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} ꯒꯤ ꯃꯔꯨꯑꯣꯏꯕ {{count}} ꯁꯤꯄ ꯈꯨꯟꯁꯥꯖꯈꯔꯦ",
@@ -184,6 +184,8 @@
184
184
  "noData": "डेटा नाही",
185
185
  "noDataTitle": "अद्याप दाखवण्यासारखे काही नाही",
186
186
  "noDataDescription": "तुमच्या खात्याशी संबंधित कोणत्याही संमती, विनंत्या किंवा क्रियाकलाप अद्याप आम्हाला आढळले नाहीत. उपलब्ध होताच तुमच्या गोपनीयता नोंदी येथे दिसतील.",
187
+ "sessionExpiredTitle": "तुमचे सत्र संपले आहे",
188
+ "sessionExpiredDescription": "सुरू ठेवण्यासाठी कृपया पुन्हा साइन इन करा.",
187
189
  "rowsPerPage": "प्रति पृष्ठ पंक्ती:",
188
190
  "pageOf": "पृष्ठ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} पैकी {{count}} पंक्ती निवडली",
@@ -184,6 +184,8 @@
184
184
  "noData": "डेटा छैन",
185
185
  "noDataTitle": "अहिलेसम्म देखाउने केही छैन",
186
186
  "noDataDescription": "तपाईंको खातासँग जोडिएका कुनै सहमति, अनुरोध वा गतिविधि हामीले अहिलेसम्म फेला पारेनौं। उपलब्ध भएपछि तपाईंका गोपनीयता रेकर्डहरू यहाँ देखिनेछन्।",
187
+ "sessionExpiredTitle": "तपाईंको सत्र समाप्त भएको छ",
188
+ "sessionExpiredDescription": "जारी राख्न कृपया फेरि साइन इन गर्नुहोस्।",
187
189
  "rowsPerPage": "पृष्ठ प्रति पङ्क्तिहरू:",
188
190
  "pageOf": "पृष्ठ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} मध्ये {{count}} पङ्क्ति चयन गरियो",
@@ -184,6 +184,8 @@
184
184
  "noData": "ତଥ୍ୟ ନାହିଁ",
185
185
  "noDataTitle": "ଏପର୍ଯ୍ୟନ୍ତ ଦେଖାଇବାକୁ କିଛି ନାହିଁ",
186
186
  "noDataDescription": "ଆପଣଙ୍କ ଆକାଉଣ୍ଟ ସହିତ ସଂଯୁକ୍ତ କୌଣସି ସମ୍ମତି, ଅନୁରୋଧ କିମ୍ବା କାର୍ଯ୍ୟକଳାପ ଆମେ ଏପର୍ଯ୍ୟନ୍ତ ପାଇଲୁ ନାହିଁ। ଉପଲବ୍ଧ ହେବା ମାତ୍ରେ ଆପଣଙ୍କ ଗୋପନୀୟତା ରେକର୍ଡ ଏଠାରେ ଦେଖାଯିବ।",
187
+ "sessionExpiredTitle": "ଆପଣଙ୍କ ସେସନ୍ ସମାପ୍ତ ହୋଇଯାଇଛି",
188
+ "sessionExpiredDescription": "ଜାରି ରଖିବାକୁ ଦୟାକରି ପୁନର୍ବାର ସାଇନ୍ ଇନ୍ କରନ୍ତୁ।",
187
189
  "rowsPerPage": "ପୃଷ୍ଠାକୁ ଧାଡ଼ି:",
188
190
  "pageOf": "ପୃଷ୍ଠା {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} ରୁ {{count}} ଧାଡ଼ି ବାଛିଲେ",
@@ -184,6 +184,8 @@
184
184
  "noData": "ਕੋਈ ਡੇਟਾ ਨਹੀਂ",
185
185
  "noDataTitle": "ਅਜੇ ਦਿਖਾਉਣ ਲਈ ਕੁਝ ਨਹੀਂ",
186
186
  "noDataDescription": "ਅਸੀਂ ਤੁਹਾਡੇ ਖਾਤੇ ਨਾਲ ਜੁੜੀ ਕੋਈ ਸਹਿਮਤੀ, ਬੇਨਤੀ ਜਾਂ ਗਤੀਵਿਧੀ ਅਜੇ ਨਹੀਂ ਲੱਭ ਸਕੇ। ਤੁਹਾਡੇ ਗੋਪਨੀਯਤਾ ਰਿਕਾਰਡ ਉਪਲਬਧ ਹੁੰਦੇ ਹੀ ਇੱਥੇ ਦਿਖਾਈ ਦੇਣਗੇ।",
187
+ "sessionExpiredTitle": "ਤੁਹਾਡਾ ਸੈਸ਼ਨ ਸਮਾਪਤ ਹੋ ਗਿਆ ਹੈ",
188
+ "sessionExpiredDescription": "ਜਾਰੀ ਰੱਖਣ ਲਈ ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਸਾਈਨ ਇਨ ਕਰੋ।",
187
189
  "rowsPerPage": "ਪੰਨੇ ਪ੍ਰਤੀ ਕਤਾਰਾਂ:",
188
190
  "pageOf": "ਪੰਨਾ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} ਵਿੱਚੋਂ {{count}} ਕਤਾਰ ਚੁਣੀ ਗਈ",
@@ -184,6 +184,8 @@
184
184
  "noData": "डेटा नास्ति",
185
185
  "noDataTitle": "अद्यापि किमपि दर्शयितुं नास्ति",
186
186
  "noDataDescription": "भवतः खातया सह सम्बद्धाः काश्चन सम्मतयः, अनुरोधाः, कार्यकलापाः वा अद्यापि अस्माभिः न प्राप्ताः। उपलब्धाः सति भवतः गोपनीयता-अभिलेखाः अत्र दर्शयिष्यन्ते।",
187
+ "sessionExpiredTitle": "भवतः सत्रं समाप्तम्",
188
+ "sessionExpiredDescription": "अनुवर्तनार्थं कृपया पुनः प्रवेशं करोतु।",
187
189
  "rowsPerPage": "पृष्ठे पंक्तयः:",
188
190
  "pageOf": "पृष्ठम् {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} मध्ये {{count}} पंक्तिः चिता",
@@ -184,6 +184,8 @@
184
184
  "noData": "ᱰᱮᱴᱟ ᱵᱟᱹᱱᱩᱜᱼᱟ",
185
185
  "noDataTitle": "ᱱᱤᱛ ᱛᱟᱠ ᱡᱟᱦᱟᱸ ᱫᱮᱠᱷᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱵᱟᱝ",
186
186
  "noDataDescription": "ᱟᱞᱮ ᱱᱤᱛ ᱛᱟᱠ ᱟᱢᱟᱜ ᱮᱠᱟᱣᱩᱱᱴ ᱥᱟᱞᱟᱜ ᱡᱚᱲᱟᱣ ᱡᱟᱦᱟᱸ ᱥᱚᱦᱚᱢᱚᱛᱤ, ᱦᱟᱵᱟᱭ, ᱟᱨᱵᱟᱝ ᱠᱟᱹᱢᱤ ᱵᱟᱝ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱫᱼᱟ᱾ ᱟᱢᱟᱜ ᱯᱨᱟᱭᱣᱮᱥᱤ ᱨᱤᱠᱚᱨᱰ ᱠᱚ ᱧᱟᱢᱚᱜ ᱠᱷᱟᱱ ᱱᱚᱸᱰᱮ ᱫᱮᱠᱷᱟᱣᱚᱜᱼᱟ᱾",
187
+ "sessionExpiredTitle": "ᱟᱢᱟᱜ ᱥᱮᱥᱚᱱ ᱢᱩᱪᱟᱹᱫᱮᱱᱟ",
188
+ "sessionExpiredDescription": "ᱞᱟᱦᱟ ᱪᱟᱞᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱳᱦᱲᱟ ᱥᱟᱭᱱ ᱤᱱ ᱢᱮ᱾",
187
189
  "rowsPerPage": "ᱯᱮᱡᱽ ᱯᱤᱨᱤ ᱪᱷᱩᱴᱟᱹ:",
188
190
  "pageOf": "ᱯᱮᱡᱽ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} ᱠᱷᱚᱱ {{count}} ᱪᱷᱩᱴᱟᱹ ᱵᱟᱪᱷᱟᱣ",
@@ -184,6 +184,8 @@
184
184
  "noData": "ڊيٽا ڪانهي",
185
185
  "noDataTitle": "اڃا ڏيکارڻ لاءِ ڪجهه ناهي",
186
186
  "noDataDescription": "اسان کي اڃا تائين توهان جي اڪائونٽ سان لاڳاپيل ڪابه رضامندي، درخواست يا سرگرمي نه ملي. توهان جا رازداري رڪارڊ موجود ٿيندي ئي هتي ظاهر ٿيندا.",
187
+ "sessionExpiredTitle": "توهان جو سيشن ختم ٿي ويو آهي",
188
+ "sessionExpiredDescription": "جاري رکڻ لاءِ مهرباني ڪري ٻيهر سائن ان ڪريو.",
187
189
  "rowsPerPage": "صفحي في قطارون:",
188
190
  "pageOf": "صفحو {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}} مان {{count}} قطار چونڊيل",
@@ -184,6 +184,8 @@
184
184
  "noData": "தரவு இல்லை",
185
185
  "noDataTitle": "இன்னும் காட்ட எதுவும் இல்லை",
186
186
  "noDataDescription": "உங்கள் கணக்குடன் இணைக்கப்பட்ட ஒப்புதல்கள், கோரிக்கைகள் அல்லது செயல்பாடு எதையும் இன்னும் எங்களால் கண்டறிய முடியவில்லை. உங்கள் தனியுரிமை பதிவுகள் கிடைக்கும்போது அவை இங்கே தோன்றும்.",
187
+ "sessionExpiredTitle": "உங்கள் அமர்வு காலாவதியாகிவிட்டது",
188
+ "sessionExpiredDescription": "தொடர்வதற்கு தயவுசெய்து மீண்டும் உள்நுழையவும்.",
187
189
  "rowsPerPage": "பக்கத்திற்கு வரிசைகள்:",
188
190
  "pageOf": "பக்கம் {{current}} / {{total}}",
189
191
  "rowsSelected": "{{count}} / {{total}} வரிசைத் தேர்ந்தெடுக்கப்பட்டது",
@@ -184,6 +184,8 @@
184
184
  "noData": "డేటా లేదు",
185
185
  "noDataTitle": "చూపించడానికి ఇంకా ఏమీ లేదు",
186
186
  "noDataDescription": "మీ ఖాతాకు అనుసంధానించబడిన ఏ సమ్మతులు, అభ్యర్థనలు లేదా కార్యాచరణ ఇంకా మాకు కనుగొనబడలేదు. అందుబాటులోకి వచ్చిన తర్వాత మీ గోప్యతా రికార్డులు ఇక్కడ కనిపిస్తాయి.",
187
+ "sessionExpiredTitle": "మీ సెషన్ గడువు ముగిసింది",
188
+ "sessionExpiredDescription": "కొనసాగించడానికి దయచేసి మళ్లీ సైన్ ఇన్ చేయండి.",
187
189
  "rowsPerPage": "పేజీకి వరుసలు:",
188
190
  "pageOf": "పేజీ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{total}}లో {{count}} వరుస ఎంచుకోబడింది",
@@ -184,6 +184,8 @@
184
184
  "noData": "کوئی ڈیٹا نہیں",
185
185
  "noDataTitle": "ابھی دکھانے کے لیے کچھ نہیں",
186
186
  "noDataDescription": "ہمیں ابھی تک آپ کے اکاؤنٹ سے منسلک کوئی رضامندی، درخواست یا سرگرمی نہیں ملی۔ آپ کے پرائیویسی ریکارڈ دستیاب ہوتے ہی یہاں ظاہر ہو جائیں گے۔",
187
+ "sessionExpiredTitle": "آپ کا سیشن ختم ہو گیا ہے",
188
+ "sessionExpiredDescription": "جاری رکھنے کے لیے براہ کرم دوبارہ سائن اِن کریں۔",
187
189
  "rowsPerPage": "فی صفحہ قطاریں:",
188
190
  "pageOf": "صفحہ {{current}} / {{total}}",
189
191
  "rowsSelected": "{{count}} از {{total}} قطار منتخب",
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveContactFromJwt } from '../src/RedactoPrivacyCenter/lib/utils';
4
+
5
+ describe('resolveContactFromJwt', () => {
6
+ it('prefers the email when present in user_data', () => {
7
+ const decoded = {
8
+ user_data: {
9
+ primary_email: 'user@example.com',
10
+ primary_mobile: '+918955610253',
11
+ org_user_id: '+918955610253',
12
+ },
13
+ };
14
+ expect(resolveContactFromJwt(decoded)).toBe('user@example.com');
15
+ });
16
+
17
+ it('falls back to the mobile for a mobile-only login (no email)', () => {
18
+ const decoded = {
19
+ user_data: {
20
+ primary_email: null,
21
+ primary_mobile: '+918955610253',
22
+ org_user_id: '+918955610253',
23
+ },
24
+ };
25
+ expect(resolveContactFromJwt(decoded)).toBe('+918955610253');
26
+ });
27
+
28
+ it('falls back to org_user_id when neither email nor mobile is present', () => {
29
+ const decoded = { user_data: { org_user_id: 'ucid-123' } };
30
+ expect(resolveContactFromJwt(decoded)).toBe('ucid-123');
31
+ });
32
+
33
+ it('reads top-level contact / primary_email when not nested', () => {
34
+ expect(resolveContactFromJwt({ contact: 'a@b.com' })).toBe('a@b.com');
35
+ expect(resolveContactFromJwt({ primary_email: 'c@d.com' })).toBe('c@d.com');
36
+ });
37
+
38
+ it('returns null for missing, empty, or whitespace identifiers', () => {
39
+ expect(resolveContactFromJwt(null)).toBeNull();
40
+ expect(resolveContactFromJwt({})).toBeNull();
41
+ expect(resolveContactFromJwt({ user_data: {} })).toBeNull();
42
+ expect(resolveContactFromJwt({ user_data: { primary_mobile: ' ' } })).toBeNull();
43
+ });
44
+ });