tycho-components 0.36.8 → 0.36.10

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.
@@ -8,9 +8,10 @@ declare global {
8
8
  * the vendor tag on mount from a Vite env var). Set `VITE_APP_CLARITY_ID`.
9
9
  *
10
10
  * On top of the plain snippet it stitches every session of the logged-in Tycho
11
- * user under their stable `uid` (decoded client-side from the `jwt_token_tycho`
12
- * cookie) via `clarity('identify', …)`, so cross-module full-page navigations
13
- * count as ONE identified user instead of N anonymous ones. The friendly name is
14
- * PII drop the last `identify` argument for a stricter privacy posture.
11
+ * user under their stable `uid` (from `CookieStorage` / `SecurityUtils.parseJwt`)
12
+ * via `clarity('identify', …)`, so cross-module full-page navigations count as
13
+ * ONE identified user instead of N anonymous ones. Re-runs identify when the JWT
14
+ * cookie changes (login/logout) or the tab becomes visible again. The friendly
15
+ * name is PII — drop the last `identify` argument for a stricter privacy posture.
15
16
  */
16
17
  export default function AppClarity(): null;
@@ -1,48 +1,67 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
+ import CookieStorage, { JWT_CHANGED_EVENT } from '../../configs/CookieStorage';
3
+ import SecurityUtils from '../../functions/SecurityUtils';
4
+ const SCRIPT_ATTR = 'data-tycho-clarity';
2
5
  /**
3
6
  * Microsoft Clarity loader — mirrors {@link AppAnalytics} (null render, injects
4
7
  * the vendor tag on mount from a Vite env var). Set `VITE_APP_CLARITY_ID`.
5
8
  *
6
9
  * On top of the plain snippet it stitches every session of the logged-in Tycho
7
- * user under their stable `uid` (decoded client-side from the `jwt_token_tycho`
8
- * cookie) via `clarity('identify', …)`, so cross-module full-page navigations
9
- * count as ONE identified user instead of N anonymous ones. The friendly name is
10
- * PII drop the last `identify` argument for a stricter privacy posture.
10
+ * user under their stable `uid` (from `CookieStorage` / `SecurityUtils.parseJwt`)
11
+ * via `clarity('identify', …)`, so cross-module full-page navigations count as
12
+ * ONE identified user instead of N anonymous ones. Re-runs identify when the JWT
13
+ * cookie changes (login/logout) or the tab becomes visible again. The friendly
14
+ * name is PII — drop the last `identify` argument for a stricter privacy posture.
11
15
  */
12
16
  export default function AppClarity() {
13
17
  const CLARITY_ID = import.meta.env.VITE_APP_CLARITY_ID;
18
+ const lastUidRef = useRef(undefined);
14
19
  useEffect(() => {
15
20
  if (!CLARITY_ID) {
16
21
  console.warn('Microsoft Clarity project ID not set.');
17
22
  return;
18
23
  }
19
- // Inject the official Clarity loader (same shape as Clarity's install snippet).
20
- const loader = document.createElement('script');
21
- loader.innerHTML =
22
- `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};` +
23
- `t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;` +
24
- `y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})` +
25
- `(window,document,"clarity","script","${CLARITY_ID}");`;
26
- document.head.appendChild(loader);
27
- // Identify the logged-in Tycho user so sessions stitch under one real id.
28
- try {
29
- const match = document.cookie.match(/(?:^|;\s*)jwt_token_tycho=([^;]+)/);
30
- if (match) {
31
- let b64 = match[1].split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
32
- b64 += '==='.slice((b64.length + 3) % 4);
33
- const payload = JSON.parse(decodeURIComponent(atob(b64)
34
- .split('')
35
- .map((ch) => '%' + ('00' + ch.charCodeAt(0).toString(16)).slice(-2))
36
- .join('')));
37
- if (payload?.uid) {
38
- window.clarity?.('identify', payload.uid, undefined, undefined, payload.name || undefined);
39
- }
40
- }
24
+ if (!document.querySelector(`script[${SCRIPT_ATTR}]`)) {
25
+ const loader = document.createElement('script');
26
+ loader.setAttribute(SCRIPT_ATTR, CLARITY_ID);
27
+ loader.innerHTML =
28
+ `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};` +
29
+ `t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;` +
30
+ `y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})` +
31
+ `(window,document,"clarity","script","${CLARITY_ID}");`;
32
+ document.head.appendChild(loader);
41
33
  window.clarity?.('consent');
42
34
  }
43
- catch {
44
- /* not logged in / unparseable — track anonymously */
45
- }
35
+ const identifyUser = () => {
36
+ try {
37
+ const token = CookieStorage.getJwtToken();
38
+ if (!token) {
39
+ lastUidRef.current = undefined;
40
+ return;
41
+ }
42
+ const payload = SecurityUtils.parseJwt(token);
43
+ if (!payload?.uid || payload.uid === lastUidRef.current)
44
+ return;
45
+ window.clarity?.('identify', payload.uid, undefined, undefined, payload.name || undefined);
46
+ lastUidRef.current = payload.uid;
47
+ }
48
+ catch {
49
+ /* not logged in / unparseable — track anonymously */
50
+ }
51
+ };
52
+ const onVisible = () => {
53
+ if (document.visibilityState === 'visible')
54
+ identifyUser();
55
+ };
56
+ identifyUser();
57
+ window.addEventListener(JWT_CHANGED_EVENT, identifyUser);
58
+ window.addEventListener('focus', identifyUser);
59
+ document.addEventListener('visibilitychange', onVisible);
60
+ return () => {
61
+ window.removeEventListener(JWT_CHANGED_EVENT, identifyUser);
62
+ window.removeEventListener('focus', identifyUser);
63
+ document.removeEventListener('visibilitychange', onVisible);
64
+ };
46
65
  }, [CLARITY_ID]);
47
66
  return null;
48
67
  }
@@ -30,7 +30,7 @@ function getFieldDisplayContent(field, raw, t) {
30
30
  notAvailable: t('label.notavailable'),
31
31
  });
32
32
  }
33
- if (raw) {
33
+ if (raw != null && raw !== '') {
34
34
  return raw;
35
35
  }
36
36
  return t('label.notavailable');
@@ -1,3 +1,5 @@
1
+ /** Dispatched on `window` whenever the Tycho JWT cookie is set or cleared. */
2
+ export declare const JWT_CHANGED_EVENT = "tycho:jwt-changed";
1
3
  declare function getRedirectUri(): string | undefined;
2
4
  declare function setRedirectUri(uri: string): void;
3
5
  declare function removeRedirectUri(): void;
@@ -1,7 +1,12 @@
1
1
  import Cookies from 'js-cookie';
2
2
  const REDIRECT_URI = 'redirect_uri_tycho';
3
3
  const JWT_TOKEN = 'jwt_token_tycho';
4
+ /** Dispatched on `window` whenever the Tycho JWT cookie is set or cleared. */
5
+ export const JWT_CHANGED_EVENT = 'tycho:jwt-changed';
4
6
  const expireDays = 7;
7
+ const notifyJwtChanged = () => {
8
+ window.dispatchEvent(new Event(JWT_CHANGED_EVENT));
9
+ };
5
10
  const set = (key, value) => {
6
11
  Cookies.set(key, value, { expires: expireDays });
7
12
  };
@@ -14,13 +19,16 @@ const remove = (key) => {
14
19
  };
15
20
  const setJwtToken = (jwtToken) => {
16
21
  Cookies.set(JWT_TOKEN, jwtToken, { expires: expireDays });
22
+ notifyJwtChanged();
17
23
  };
18
24
  const getJwtToken = () => {
19
25
  const cookie = Cookies.get(JWT_TOKEN);
20
26
  return cookie === 'undefined' ? '' : cookie;
21
27
  };
22
28
  const removeJwtToken = () => {
23
- return Cookies.remove(JWT_TOKEN);
29
+ const removed = Cookies.remove(JWT_TOKEN);
30
+ notifyJwtChanged();
31
+ return removed;
24
32
  };
25
33
  function getRedirectUri() {
26
34
  return Cookies.get(REDIRECT_URI);
@@ -11,7 +11,8 @@ const hasAccess = (uid, roles, mode) => {
11
11
  };
12
12
  const parseJwt = (token) => {
13
13
  const base64Url = token.split('.')[1];
14
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
14
+ let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
15
+ base64 += '==='.slice((base64.length + 3) % 4);
15
16
  const payload = decodeURIComponent(window
16
17
  .atob(base64)
17
18
  .split('')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.36.8",
4
+ "version": "0.36.10",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -82,7 +82,7 @@
82
82
  "react-i18next": "^13.0.2",
83
83
  "react-router-dom": "^6.14.2",
84
84
  "react-toastify": "^9.1.3",
85
- "tycho-storybook": "0.10.12",
85
+ "tycho-storybook": "0.10.13",
86
86
  "wavesurfer-react": "^2.2.2",
87
87
  "wavesurfer.js": "^6.6.3"
88
88
  },
@@ -111,7 +111,7 @@
111
111
  "react-toastify": "^9.1.3",
112
112
  "sass-embedded": "^1.97.2",
113
113
  "storybook": "^10.1.11",
114
- "tycho-storybook": "^0.10.12",
114
+ "tycho-storybook": "^0.10.13",
115
115
  "typescript": "^5.7.3",
116
116
  "vite": "^7.0.0",
117
117
  "vitest": "^3.2.6",