@sanvika/auth 1.0.10 → 1.0.11

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 (2) hide show
  1. package/dist/index.js +117 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ import {
6
6
  useContext,
7
7
  useEffect,
8
8
  useState,
9
- useCallback
9
+ useCallback,
10
+ useRef
10
11
  } from "react";
11
12
 
12
13
  // constants.js
@@ -21,6 +22,74 @@ var STORAGE_KEYS = {
21
22
  var DEFAULT_IAM_URL = "https://accounts.sanvikaproduction.com";
22
23
  var DEFAULT_AVATAR_SVG = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23e5e7eb'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23adb5bd'/%3E%3Cellipse cx='20' cy='35' rx='12' ry='8' fill='%23adb5bd'/%3E%3C/svg%3E`;
23
24
 
25
+ // tokenRefreshUtils.js
26
+ function decodeToken(token) {
27
+ try {
28
+ const base64Url = token.split(".")[1];
29
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
30
+ const jsonPayload = decodeURIComponent(
31
+ atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
32
+ );
33
+ return JSON.parse(jsonPayload);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ async function ensureTokenFresh(token, thresholdSeconds = 300) {
39
+ if (!token) return null;
40
+ const decoded = decodeToken(token);
41
+ if (!decoded || !decoded.exp) return token;
42
+ const nowSeconds = Math.floor(Date.now() / 1e3);
43
+ const secondsUntilExpiry = decoded.exp - nowSeconds;
44
+ if (secondsUntilExpiry > thresholdSeconds) {
45
+ return token;
46
+ }
47
+ try {
48
+ const refreshRes = await fetch(
49
+ `${process.env.NEXT_PUBLIC_APP_BASE_URL || "https://accounts.sanvikaproduction.com"}/api/auth/refresh`,
50
+ {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ credentials: "include"
54
+ // Send refresh token cookie
55
+ }
56
+ );
57
+ if (!refreshRes.ok) {
58
+ console.warn("[TokenRefresh] Refresh failed:", refreshRes.status);
59
+ return token;
60
+ }
61
+ const data = await refreshRes.json();
62
+ if (data.success && data.accessToken) {
63
+ localStorage.setItem("sanvika_accessToken", data.accessToken);
64
+ return data.accessToken;
65
+ }
66
+ } catch (err) {
67
+ console.warn("[TokenRefresh] Error refreshing token:", err.message);
68
+ }
69
+ return token;
70
+ }
71
+ function scheduleTokenRefresh(token, onRefresh) {
72
+ if (!token) return () => {
73
+ };
74
+ const decoded = decodeToken(token);
75
+ if (!decoded || !decoded.exp) return () => {
76
+ };
77
+ const nowSeconds = Math.floor(Date.now() / 1e3);
78
+ const secondsUntilExpiry = decoded.exp - nowSeconds;
79
+ const refreshAt = Math.max(60, Math.floor(secondsUntilExpiry * 0.75));
80
+ const timeoutId = setTimeout(async () => {
81
+ try {
82
+ const freshToken = await ensureTokenFresh(token, 0);
83
+ if (freshToken !== token && onRefresh) {
84
+ onRefresh(freshToken);
85
+ }
86
+ } catch (err) {
87
+ console.warn("[TokenRefresh] Auto-refresh failed:", err.message);
88
+ }
89
+ }, refreshAt * 1e3);
90
+ return () => clearTimeout(timeoutId);
91
+ }
92
+
24
93
  // SanvikaAuthProvider.jsx
25
94
  import { jsx } from "react/jsx-runtime";
26
95
  var SanvikaAuthContext = createContext(null);
@@ -34,6 +103,7 @@ function SanvikaAuthProvider({
34
103
  const [user, setUser] = useState(null);
35
104
  const [accessToken, setToken] = useState(null);
36
105
  const [loading, setLoading] = useState(true);
106
+ const refreshCleanupRef = useRef(null);
37
107
  useEffect(() => {
38
108
  try {
39
109
  const storedToken = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
@@ -67,6 +137,30 @@ function SanvikaAuthProvider({
67
137
  window.addEventListener("storage", onStorage);
68
138
  return () => window.removeEventListener("storage", onStorage);
69
139
  }, []);
140
+ useEffect(() => {
141
+ if (!accessToken) {
142
+ if (refreshCleanupRef.current) {
143
+ refreshCleanupRef.current();
144
+ refreshCleanupRef.current = null;
145
+ }
146
+ return;
147
+ }
148
+ const cleanup = scheduleTokenRefresh(accessToken, (freshToken) => {
149
+ localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, freshToken);
150
+ setToken(freshToken);
151
+ window.dispatchEvent(
152
+ new StorageEvent("storage", {
153
+ key: STORAGE_KEYS.ACCESS_TOKEN,
154
+ newValue: freshToken,
155
+ oldValue: accessToken
156
+ })
157
+ );
158
+ });
159
+ refreshCleanupRef.current = cleanup;
160
+ return () => {
161
+ if (cleanup) cleanup();
162
+ };
163
+ }, [accessToken]);
70
164
  const login = useCallback((token, userData) => {
71
165
  localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
72
166
  localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
@@ -146,7 +240,7 @@ function useSanvikaAuth() {
146
240
  }
147
241
 
148
242
  // SanvikaAccountButton.jsx
149
- import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
243
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
150
244
 
151
245
  // #style-inject:#style-inject
152
246
  function styleInject(css, { insertAt } = {}) {
@@ -171,7 +265,7 @@ function styleInject(css, { insertAt } = {}) {
171
265
  }
172
266
 
173
267
  // SanvikaAccountButton.css
174
- styleInject("@keyframes snvk-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n.snvk-skeleton {\n width: clamp(72px, 20vw, 96px);\n height: clamp(34px, 8vw, 40px);\n border-radius: 8px;\n background:\n linear-gradient(\n 90deg,\n var(--skeleton-base-color, #d0d0d0) 25%,\n var(--skeleton-highlight-color, #f0f0f0) 50%,\n var(--skeleton-base-color, #d0d0d0) 75%);\n background-size: 200% 100%;\n animation: snvk-shimmer 1.4s infinite;\n display: inline-block;\n}\n.snvk-wrapper {\n position: relative;\n display: inline-block;\n}\n.snvk-guestBtn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: clamp(6px, 2vw, 8px) clamp(10px, 3vw, 16px);\n min-height: 44px;\n min-width: 44px;\n background: var(--sanvika-brand-color, #4f46e5);\n color: #ffffff;\n border: none;\n border-radius: 8px;\n font-size: clamp(13px, 3vw, 14px);\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.3s ease, color 0.3s ease;\n white-space: nowrap;\n}\n.snvk-guestBtn:hover {\n background: var(--sanvika-brand-hover, #4338ca);\n}\n.snvk-iconWrap {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n.snvk-profileBtn {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n padding: 5px 10px 5px 5px;\n min-height: 44px;\n background: var(--muted-bg, #f5f5f5);\n border: 1px solid var(--border-color-light, #e5e7eb);\n border-radius: 99px;\n font-size: clamp(13px, 3vw, 14px);\n font-weight: 600;\n cursor: pointer;\n color: var(--text-color, #1a1a1a);\n transition:\n background-color 0.3s ease,\n border-color 0.3s ease,\n color 0.3s ease;\n white-space: nowrap;\n outline: none;\n}\n.snvk-profileBtn:hover {\n background: var(--hover-color, #ebebeb);\n border-color: var(--border-color-dark, #d1d5db);\n}\n.snvk-profileBtn:focus-visible {\n outline: 2px solid var(--sanvika-brand-color, #4f46e5);\n outline-offset: 2px;\n}\n.snvk-avatar {\n width: 28px;\n height: 28px;\n border-radius: 50%;\n object-fit: cover;\n border: 2px solid var(--sanvika-brand-color, #4f46e5);\n flex-shrink: 0;\n}\n.snvk-textWrap {\n display: inline;\n}\n.snvk-hideTextOnMobile {\n display: none;\n}\n@media (min-width: 500px) {\n .snvk-hideTextOnMobile {\n display: inline;\n }\n}\n.snvk-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n min-width: clamp(200px, 60vw, 240px);\n background: var(--card-bg, #f8f8f8);\n border: 1px solid var(--border-color-light, #d0d0d0);\n border-radius: 12px;\n box-shadow: 0 8px 24px var(--shadow-color, rgba(0, 0, 0, 0.15));\n z-index: 9999;\n overflow: hidden;\n transition: background-color 0.3s ease, border-color 0.3s ease;\n}\n.snvk-dropdownHeader {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 14px 16px;\n background: var(--section-bg, #fafafa);\n transition: background-color 0.3s ease;\n}\n.snvk-dropdownAvatar {\n width: 38px;\n height: 38px;\n border-radius: 50%;\n object-fit: cover;\n border: 2px solid var(--sanvika-brand-color, #4f46e5);\n flex-shrink: 0;\n}\n.snvk-dropdownName {\n font-size: clamp(13px, 3vw, 14px);\n font-weight: 700;\n color: var(--text-color, #1a1a1a);\n line-height: 1.3;\n transition: color 0.3s ease;\n}\n.snvk-dropdownMobile {\n font-size: clamp(11px, 2.5vw, 12px);\n color: var(--secondary-text-color, #444444);\n margin-top: 2px;\n transition: color 0.3s ease;\n font-weight: 600;\n}\n.snvk-divider {\n height: 1px;\n background: var(--muted-border, #f0f0f0);\n transition: background-color 0.3s ease;\n}\n.snvk-dropdownItem {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 11px 16px;\n min-height: 44px;\n background: none;\n border: none;\n font-size: clamp(13px, 3vw, 14px);\n color: var(--text-color, #222222);\n font-weight: 500;\n cursor: pointer;\n text-decoration: none;\n text-align: left;\n transition: background-color 0.3s ease, color 0.3s ease;\n}\n.snvk-dropdownItem:hover {\n background: var(--hover-color, #f7f7f7);\n}\n.snvk-logoutItem {\n color: var(--error-color, #c0392b);\n}\n.snvk-logoutItem:hover {\n background: var(--error-bg, #fff5f5);\n}\n.snvk-deleteItem {\n color: var(--warning-color, #d97706);\n}\n.snvk-deleteItem:hover {\n background: var(--warning-bg, #fffbf0);\n}\n");
268
+ styleInject("@keyframes snvk-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n.snvk-skeleton {\n width: clamp(72px, 20vw, 96px);\n height: clamp(34px, 8vw, 40px);\n border-radius: 8px;\n background:\n linear-gradient(\n 90deg,\n var(--skeleton-base-color, #d0d0d0) 25%,\n var(--skeleton-highlight-color, #f0f0f0) 50%,\n var(--skeleton-base-color, #d0d0d0) 75%);\n background-size: 200% 100%;\n animation: snvk-shimmer 1.4s infinite;\n display: inline-block;\n}\n.snvk-wrapper {\n position: relative;\n display: inline-block;\n}\n.snvk-guestBtn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: clamp(6px, 2vw, 8px) clamp(10px, 3vw, 16px);\n min-height: 44px;\n min-width: 44px;\n background: var(--sanvika-brand-color, #4f46e5);\n color: #ffffff;\n border: none;\n border-radius: 8px;\n font-size: clamp(13px, 3vw, 14px);\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.3s ease, color 0.3s ease;\n white-space: nowrap;\n}\n.snvk-guestBtn:hover {\n background: var(--sanvika-brand-hover, #4338ca);\n}\n.snvk-iconWrap {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n.snvk-profileBtn {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n padding: 5px 10px 5px 5px;\n min-height: 44px;\n background: var(--muted-bg, #f5f5f5);\n border: 1px solid var(--border-color-light, #e5e7eb);\n border-radius: 99px;\n font-size: clamp(13px, 3vw, 14px);\n font-weight: 600;\n cursor: pointer;\n color: var(--text-color, #1a1a1a);\n transition:\n background-color 0.3s ease,\n border-color 0.3s ease,\n color 0.3s ease;\n white-space: nowrap;\n outline: none;\n}\n.snvk-profileBtn:hover {\n background: var(--hover-color, #ebebeb);\n border-color: var(--border-color-dark, #d1d5db);\n}\n.snvk-profileBtn:focus-visible {\n outline: 2px solid var(--sanvika-brand-color, #4f46e5);\n outline-offset: 2px;\n}\n.snvk-avatar {\n width: 28px;\n height: 28px;\n border-radius: 50%;\n object-fit: cover;\n border: 2px solid var(--sanvika-brand-color, #4f46e5);\n flex-shrink: 0;\n}\n.snvk-textWrap {\n display: inline;\n}\n.snvk-hideTextOnMobile {\n display: none;\n}\n@media (min-width: 500px) {\n .snvk-hideTextOnMobile {\n display: inline;\n }\n}\n.snvk-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n min-width: clamp(200px, 60vw, 240px);\n background: var(--card-bg, #f3f3f3);\n border: 1px solid var(--border-color-light, #cccccc);\n border-radius: 12px;\n box-shadow: 0 8px 24px var(--shadow-color, rgba(0, 0, 0, 0.2));\n z-index: 9999;\n overflow: hidden;\n transition: background-color 0.3s ease, border-color 0.3s ease;\n}\n.snvk-dropdownHeader {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 14px 16px;\n background: var(--section-bg, #ebebeb);\n transition: background-color 0.3s ease;\n}\n.snvk-dropdownAvatar {\n width: 38px;\n height: 38px;\n border-radius: 50%;\n object-fit: cover;\n border: 2px solid var(--sanvika-brand-color, #4f46e5);\n flex-shrink: 0;\n}\n.snvk-dropdownName {\n font-size: clamp(13px, 3vw, 14px);\n font-weight: 800;\n color: var(--text-color, #000000);\n line-height: 1.3;\n transition: color 0.3s ease;\n}\n.snvk-dropdownMobile {\n font-size: clamp(11px, 2.5vw, 12px);\n color: var(--secondary-text-color, #333333);\n margin-top: 2px;\n transition: color 0.3s ease;\n font-weight: 700;\n}\n.snvk-divider {\n height: 1px;\n background: var(--muted-border, #d8d8d8);\n transition: background-color 0.3s ease;\n}\n.snvk-dropdownItem {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 11px 16px;\n min-height: 44px;\n background: none;\n border: none;\n font-size: clamp(13px, 3vw, 14px);\n color: var(--text-color, #000000);\n font-weight: 600;\n cursor: pointer;\n text-decoration: none;\n text-align: left;\n transition: background-color 0.3s ease, color 0.3s ease;\n}\n.snvk-dropdownItem:hover {\n background: var(--hover-color, #e8e8e8);\n}\n.snvk-logoutItem {\n color: var(--error-color, #c0392b);\n}\n.snvk-logoutItem:hover {\n background: var(--error-bg, #fff5f5);\n}\n.snvk-deleteItem {\n color: var(--warning-color, #d97706);\n}\n.snvk-deleteItem:hover {\n background: var(--warning-bg, #fffbf0);\n}\n");
175
269
 
176
270
  // SanvikaAccountButton.jsx
177
271
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
@@ -293,7 +387,7 @@ function SanvikaAccountButton({
293
387
  const [dropdownOpen, setDropdownOpen] = useState2(false);
294
388
  const [imgError, setImgError] = useState2(false);
295
389
  const [prevImage, setPrevImage] = useState2(user == null ? void 0 : user.image);
296
- const wrapperRef = useRef(null);
390
+ const wrapperRef = useRef2(null);
297
391
  if ((user == null ? void 0 : user.image) !== prevImage) {
298
392
  setPrevImage(user == null ? void 0 : user.image);
299
393
  setImgError(false);
@@ -417,7 +511,20 @@ function SanvikaAccountButton({
417
511
  /* @__PURE__ */ jsxs(
418
512
  "a",
419
513
  {
420
- href: onboardingPath && user.status === "onboarding" ? onboardingPath : dashboardPath,
514
+ href: "https://accounts.sanvikaproduction.com/dashboard/user-profile",
515
+ className: "snvk-dropdownItem",
516
+ role: "menuitem",
517
+ onClick: () => setDropdownOpen(false),
518
+ children: [
519
+ /* @__PURE__ */ jsx2(UserIcon, { size: 15 }),
520
+ /* @__PURE__ */ jsx2("span", { children: "View Profile" })
521
+ ]
522
+ }
523
+ ),
524
+ /* @__PURE__ */ jsxs(
525
+ "a",
526
+ {
527
+ href: onboardingPath && user.status === "onboarding" ? onboardingPath : dashboardPath || "https://accounts.sanvikaproduction.com/dashboard",
421
528
  className: "snvk-dropdownItem",
422
529
  role: "menuitem",
423
530
  onClick: () => setDropdownOpen(false),
@@ -430,7 +537,9 @@ function SanvikaAccountButton({
430
537
  /* @__PURE__ */ jsxs(
431
538
  "a",
432
539
  {
433
- href: "https://accounts.sanvikaproduction.com/account/delete",
540
+ href: `https://accounts.sanvikaproduction.com/account/delete?token=${encodeURIComponent(
541
+ (user == null ? void 0 : user.accessToken) || ""
542
+ )}`,
434
543
  className: "snvk-dropdownItem snvk-deleteItem",
435
544
  role: "menuitem",
436
545
  onClick: () => setDropdownOpen(false),
@@ -466,5 +575,7 @@ export {
466
575
  SanvikaAccountButton,
467
576
  SanvikaAuthContext,
468
577
  SanvikaAuthProvider,
578
+ decodeToken,
579
+ ensureTokenFresh,
469
580
  useSanvikaAuth
470
581
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanvika/auth",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Sanvika Auth SDK — React components and hooks for Sanvika SSO integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",