@sanvika/auth 1.0.10 → 1.0.12

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 +118 -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";
@@ -288,12 +382,13 @@ function SanvikaAccountButton({
288
382
  logout,
289
383
  redirectToLogin,
290
384
  dashboardPath,
385
+ accessToken,
291
386
  updateUser
292
387
  } = useSanvikaAuth();
293
388
  const [dropdownOpen, setDropdownOpen] = useState2(false);
294
389
  const [imgError, setImgError] = useState2(false);
295
390
  const [prevImage, setPrevImage] = useState2(user == null ? void 0 : user.image);
296
- const wrapperRef = useRef(null);
391
+ const wrapperRef = useRef2(null);
297
392
  if ((user == null ? void 0 : user.image) !== prevImage) {
298
393
  setPrevImage(user == null ? void 0 : user.image);
299
394
  setImgError(false);
@@ -417,7 +512,20 @@ function SanvikaAccountButton({
417
512
  /* @__PURE__ */ jsxs(
418
513
  "a",
419
514
  {
420
- href: onboardingPath && user.status === "onboarding" ? onboardingPath : dashboardPath,
515
+ href: "https://accounts.sanvikaproduction.com/dashboard/user-profile",
516
+ className: "snvk-dropdownItem",
517
+ role: "menuitem",
518
+ onClick: () => setDropdownOpen(false),
519
+ children: [
520
+ /* @__PURE__ */ jsx2(UserIcon, { size: 15 }),
521
+ /* @__PURE__ */ jsx2("span", { children: "View Profile" })
522
+ ]
523
+ }
524
+ ),
525
+ /* @__PURE__ */ jsxs(
526
+ "a",
527
+ {
528
+ href: onboardingPath && user.status === "onboarding" ? onboardingPath : "https://accounts.sanvikaproduction.com/dashboard",
421
529
  className: "snvk-dropdownItem",
422
530
  role: "menuitem",
423
531
  onClick: () => setDropdownOpen(false),
@@ -430,7 +538,9 @@ function SanvikaAccountButton({
430
538
  /* @__PURE__ */ jsxs(
431
539
  "a",
432
540
  {
433
- href: "https://accounts.sanvikaproduction.com/account/delete",
541
+ href: `https://accounts.sanvikaproduction.com/account/delete?token=${encodeURIComponent(
542
+ accessToken || ""
543
+ )}`,
434
544
  className: "snvk-dropdownItem snvk-deleteItem",
435
545
  role: "menuitem",
436
546
  onClick: () => setDropdownOpen(false),
@@ -466,5 +576,7 @@ export {
466
576
  SanvikaAccountButton,
467
577
  SanvikaAuthContext,
468
578
  SanvikaAuthProvider,
579
+ decodeToken,
580
+ ensureTokenFresh,
469
581
  useSanvikaAuth
470
582
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanvika/auth",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Sanvika Auth SDK — React components and hooks for Sanvika SSO integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",