@redacto.io/consent-sdk-react 9.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @redacto.io/consent-sdk-react
2
2
 
3
+ ## 9.5.0-beta.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fix(privacy-center): resolve mobile/org_user_id as contact for mobile-only logins
8
+
3
9
  ## 9.4.0
4
10
 
5
11
  ### Minor Changes
@@ -9271,6 +9271,14 @@ var decodeJwtToken = (token) => {
9271
9271
  return null;
9272
9272
  }
9273
9273
  };
9274
+ var resolveContactFromJwt = (decoded) => {
9275
+ if (!decoded) {
9276
+ return null;
9277
+ }
9278
+ const userData = decoded.user_data || {};
9279
+ const candidate = decoded.contact || decoded.primary_email || userData.primary_email || userData.primary_mobile || userData.org_user_id;
9280
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
9281
+ };
9274
9282
  var isJwtExpired = (token, bufferSeconds = 5 * 60) => {
9275
9283
  if (!token || typeof token !== "string") {
9276
9284
  return true;
@@ -9998,9 +10006,9 @@ var syncJwtDataToSessionStorage = (accessToken) => {
9998
10006
  if (!decoded) {
9999
10007
  return;
10000
10008
  }
10001
- const email = decoded.contact || decoded.primary_email || sessionStorage.getItem("email") || (typeof localStorage !== "undefined" ? localStorage.getItem("userEmail") : null);
10002
- if (email && typeof email === "string") {
10003
- sessionStorage.setItem("email", email);
10009
+ const contact = resolveContactFromJwt(decoded) || sessionStorage.getItem("email") || (typeof localStorage !== "undefined" ? localStorage.getItem("userEmail") : null);
10010
+ if (contact && typeof contact === "string") {
10011
+ sessionStorage.setItem("email", contact);
10004
10012
  }
10005
10013
  const orgUuid = decoded.organisation_uuid;
10006
10014
  if (orgUuid && typeof orgUuid === "string") {
@@ -15145,9 +15153,9 @@ function PrivacyCenterLayout({ children, slug, onBack }) {
15145
15153
  if (orgId) sessionStorage.setItem("pc_org_id", orgId);
15146
15154
  if (workspaceId) sessionStorage.setItem("pc_workspace_id", workspaceId);
15147
15155
  if (orgSlug) sessionStorage.setItem("pc_org_slug", orgSlug);
15148
- const email = decoded.contact || decoded.primary_email;
15149
- if (email && typeof email === "string") {
15150
- sessionStorage.setItem("email", email);
15156
+ const contact = resolveContactFromJwt(decoded);
15157
+ if (contact) {
15158
+ sessionStorage.setItem("email", contact);
15151
15159
  }
15152
15160
  }
15153
15161
  }
@@ -9205,6 +9205,14 @@ var decodeJwtToken = (token) => {
9205
9205
  return null;
9206
9206
  }
9207
9207
  };
9208
+ var resolveContactFromJwt = (decoded) => {
9209
+ if (!decoded) {
9210
+ return null;
9211
+ }
9212
+ const userData = decoded.user_data || {};
9213
+ const candidate = decoded.contact || decoded.primary_email || userData.primary_email || userData.primary_mobile || userData.org_user_id;
9214
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
9215
+ };
9208
9216
  var isJwtExpired = (token, bufferSeconds = 5 * 60) => {
9209
9217
  if (!token || typeof token !== "string") {
9210
9218
  return true;
@@ -9932,9 +9940,9 @@ var syncJwtDataToSessionStorage = (accessToken) => {
9932
9940
  if (!decoded) {
9933
9941
  return;
9934
9942
  }
9935
- const email = decoded.contact || decoded.primary_email || sessionStorage.getItem("email") || (typeof localStorage !== "undefined" ? localStorage.getItem("userEmail") : null);
9936
- if (email && typeof email === "string") {
9937
- sessionStorage.setItem("email", email);
9943
+ const contact = resolveContactFromJwt(decoded) || sessionStorage.getItem("email") || (typeof localStorage !== "undefined" ? localStorage.getItem("userEmail") : null);
9944
+ if (contact && typeof contact === "string") {
9945
+ sessionStorage.setItem("email", contact);
9938
9946
  }
9939
9947
  const orgUuid = decoded.organisation_uuid;
9940
9948
  if (orgUuid && typeof orgUuid === "string") {
@@ -15085,9 +15093,9 @@ function PrivacyCenterLayout({ children, slug, onBack }) {
15085
15093
  if (orgId) sessionStorage.setItem("pc_org_id", orgId);
15086
15094
  if (workspaceId) sessionStorage.setItem("pc_workspace_id", workspaceId);
15087
15095
  if (orgSlug) sessionStorage.setItem("pc_org_slug", orgSlug);
15088
- const email = decoded.contact || decoded.primary_email;
15089
- if (email && typeof email === "string") {
15090
- sessionStorage.setItem("email", email);
15096
+ const contact = resolveContactFromJwt(decoded);
15097
+ if (contact) {
15098
+ sessionStorage.setItem("email", contact);
15091
15099
  }
15092
15100
  }
15093
15101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-react",
3
- "version": "9.4.0",
3
+ "version": "9.5.0-beta.0",
4
4
  "main": "dist/index.cjs",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -5,6 +5,7 @@ import type { OrganizationDetail } from './lib/types';
5
5
  import {
6
6
  checkAndRefreshAuth,
7
7
  decodeJwtToken,
8
+ resolveContactFromJwt,
8
9
  } from './lib/utils';
9
10
  import { useBaseUrl } from './context/BaseUrlContext';
10
11
  import { useAuth } from './context/AuthContext';
@@ -56,10 +57,11 @@ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLay
56
57
  if (workspaceId) sessionStorage.setItem('pc_workspace_id', workspaceId);
57
58
  if (orgSlug) sessionStorage.setItem('pc_org_slug', orgSlug);
58
59
 
59
- // Extract and set email from JWT for sidebar and consent manager
60
- const email = decoded.contact || decoded.primary_email;
61
- if (email && typeof email === 'string') {
62
- sessionStorage.setItem('email', email);
60
+ // Resolve the contact identifier (email, or mobile/org_user_id for
61
+ // mobile-only logins) for the sidebar and consent manager.
62
+ const contact = resolveContactFromJwt(decoded);
63
+ if (contact) {
64
+ sessionStorage.setItem('email', contact);
63
65
  }
64
66
  }
65
67
  }
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from 'react';
2
- import { setRefreshTokenCookie, decodeJwtToken, setSessionExpiredListener } from '../lib/utils';
2
+ import { setRefreshTokenCookie, decodeJwtToken, resolveContactFromJwt, setSessionExpiredListener } from '../lib/utils';
3
3
  import type { AuthContextValue } from './types';
4
4
 
5
5
  export type { AuthContextValue } from './types';
@@ -25,12 +25,14 @@ const syncJwtDataToSessionStorage = (accessToken: string): void => {
25
25
  return;
26
26
  }
27
27
 
28
- // Extract email from JWT (contact or primary_email), fall back to existing sources
29
- const email = decoded.contact || decoded.primary_email
28
+ // Resolve the user's contact identifier (email, or mobile/org_user_id for
29
+ // mobile-only logins) so case + consent-management actions have a subject.
30
+ // Falls back to previously stored values when the token omits identifiers.
31
+ const contact = resolveContactFromJwt(decoded)
30
32
  || sessionStorage.getItem('email')
31
33
  || (typeof localStorage !== 'undefined' ? localStorage.getItem('userEmail') : null);
32
- if (email && typeof email === 'string') {
33
- sessionStorage.setItem('email', email);
34
+ if (contact && typeof contact === 'string') {
35
+ sessionStorage.setItem('email', contact);
34
36
  }
35
37
 
36
38
  // Extract organization UUID
@@ -244,6 +244,34 @@ export const decodeJwtToken = (token: string): { organisation_uuid?: string; org
244
244
  }
245
245
  };
246
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
+
247
275
  /**
248
276
  * Decode JWT payload and check if token is expired or will expire soon
249
277
  * @param token - JWT token string
@@ -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
+ });