@sanvika/auth 1.0.20 → 1.0.21

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 +144 -248
  2. package/package.json +1 -2
package/dist/index.js CHANGED
@@ -1,105 +1,23 @@
1
1
  "use client";
2
2
 
3
3
  // SanvikaAuthProvider.jsx
4
- import {
5
- createContext,
6
- useContext,
7
- useEffect,
8
- useState,
9
- useCallback,
10
- useRef
11
- } from "react";
4
+ import { createContext, useContext, useEffect, useState } from "react";
12
5
 
13
6
  // constants.js
14
7
  var STORAGE_KEYS = {
15
8
  ACCESS_TOKEN: "sanvika_access_token",
16
- USER: "sanvika_user",
17
- STATE: "sanvika_oauth_state",
18
- // CSRF state for OAuth flow
19
- RETURN_PATH: "sanvika_return_path"
20
- // Page to redirect back after login
9
+ USER: "sanvika_user"
21
10
  };
22
11
  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
12
 
24
- // tokenRefreshUtils.js
25
- function decodeToken(token) {
26
- try {
27
- const base64Url = token.split(".")[1];
28
- const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
29
- const jsonPayload = decodeURIComponent(
30
- atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
31
- );
32
- return JSON.parse(jsonPayload);
33
- } catch {
34
- return null;
35
- }
36
- }
37
- async function ensureTokenFresh(token, thresholdSeconds = 300) {
38
- if (!token) return null;
39
- const decoded = decodeToken(token);
40
- if (!decoded || !decoded.exp) return token;
41
- const nowSeconds = Math.floor(Date.now() / 1e3);
42
- const secondsUntilExpiry = decoded.exp - nowSeconds;
43
- if (secondsUntilExpiry > thresholdSeconds) {
44
- return token;
45
- }
46
- try {
47
- const refreshRes = await fetch("/api/auth/refresh", {
48
- method: "POST",
49
- headers: { "Content-Type": "application/json" },
50
- credentials: "include"
51
- // Send httpOnly refresh token cookie (same domain)
52
- });
53
- if (!refreshRes.ok) {
54
- console.warn("[TokenRefresh] Refresh failed:", refreshRes.status);
55
- return token;
56
- }
57
- const data = await refreshRes.json();
58
- if (data.success && data.accessToken) {
59
- localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, data.accessToken);
60
- return data.accessToken;
61
- }
62
- } catch (err) {
63
- console.warn("[TokenRefresh] Error refreshing token:", err.message);
64
- }
65
- return token;
66
- }
67
- function scheduleTokenRefresh(token, onRefresh) {
68
- if (!token) return () => {
69
- };
70
- const decoded = decodeToken(token);
71
- if (!decoded || !decoded.exp) return () => {
72
- };
73
- const nowSeconds = Math.floor(Date.now() / 1e3);
74
- const secondsUntilExpiry = decoded.exp - nowSeconds;
75
- const refreshAt = Math.max(60, Math.floor(secondsUntilExpiry * 0.75));
76
- const timeoutId = setTimeout(async () => {
77
- try {
78
- const freshToken = await ensureTokenFresh(token, 0);
79
- if (freshToken !== token && onRefresh) {
80
- onRefresh(freshToken);
81
- }
82
- } catch (err) {
83
- console.warn("[TokenRefresh] Auto-refresh failed:", err.message);
84
- }
85
- }, refreshAt * 1e3);
86
- return () => clearTimeout(timeoutId);
87
- }
88
-
89
13
  // SanvikaAuthProvider.jsx
90
14
  import { jsx } from "react/jsx-runtime";
91
15
  var SA_URL = "https://accounts.sanvikaproduction.com";
92
16
  var SanvikaAuthContext = createContext(null);
93
- function SanvikaAuthProvider({
94
- children,
95
- clientId,
96
- redirectUri,
97
- dashboardPath = "/dashboard"
98
- }) {
17
+ function SanvikaAuthProvider({ children }) {
99
18
  const [user, setUser] = useState(null);
100
19
  const [accessToken, setToken] = useState(null);
101
20
  const [loading, setLoading] = useState(true);
102
- const refreshCleanupRef = useRef(null);
103
21
  useEffect(() => {
104
22
  try {
105
23
  const storedToken = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
@@ -108,136 +26,69 @@ function SanvikaAuthProvider({
108
26
  setToken(storedToken);
109
27
  setUser(JSON.parse(storedUser));
110
28
  }
111
- } catch {
112
- localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
113
- localStorage.removeItem(STORAGE_KEYS.USER);
29
+ } catch (e) {
30
+ console.error("[SanvikaAuth] Failed to load from localStorage:", e);
114
31
  } finally {
115
32
  setLoading(false);
116
33
  }
117
34
  }, []);
118
- useEffect(() => {
119
- function onStorage(e) {
120
- if (e.key === STORAGE_KEYS.ACCESS_TOKEN && !e.newValue) {
121
- setUser(null);
122
- setToken(null);
123
- }
124
- if (e.key === STORAGE_KEYS.ACCESS_TOKEN && e.newValue) {
125
- try {
126
- const u = localStorage.getItem(STORAGE_KEYS.USER);
127
- if (u) setUser(JSON.parse(u));
128
- setToken(e.newValue);
129
- } catch {
130
- }
131
- }
132
- }
133
- window.addEventListener("storage", onStorage);
134
- return () => window.removeEventListener("storage", onStorage);
135
- }, []);
136
- useEffect(() => {
137
- if (!accessToken) {
138
- if (refreshCleanupRef.current) {
139
- refreshCleanupRef.current();
140
- refreshCleanupRef.current = null;
141
- }
142
- return;
143
- }
144
- const cleanup = scheduleTokenRefresh(accessToken, (freshToken) => {
145
- localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, freshToken);
146
- setToken(freshToken);
147
- window.dispatchEvent(
148
- new StorageEvent("storage", {
149
- key: STORAGE_KEYS.ACCESS_TOKEN,
150
- newValue: freshToken,
151
- oldValue: accessToken
152
- })
153
- );
35
+ const login = async ({ mobile, password, deviceId, deviceName }) => {
36
+ const response = await fetch(`${SA_URL}/api/auth/login`, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ credentials: "include",
40
+ body: JSON.stringify({
41
+ mobile,
42
+ password,
43
+ deviceId: deviceId || crypto.randomUUID(),
44
+ deviceName: deviceName || "Browser"
45
+ })
154
46
  });
155
- refreshCleanupRef.current = cleanup;
156
- return () => {
157
- if (cleanup) cleanup();
158
- };
159
- }, [accessToken]);
160
- const login = useCallback((token, userData) => {
161
- localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
162
- localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
163
- setToken(token);
164
- setUser(userData);
165
- }, []);
166
- const logout = useCallback(async () => {
167
- const token = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
168
- if (token) {
169
- fetch(`${SA_URL}/api/auth/logout`, {
47
+ const data = await response.json();
48
+ if (!data.success) throw new Error(data.error);
49
+ localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, data.accessToken);
50
+ localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(data.user));
51
+ setToken(data.accessToken);
52
+ setUser(data.user);
53
+ return data;
54
+ };
55
+ const logout = async (logoutAll = false) => {
56
+ try {
57
+ await fetch(`${SA_URL}/api/auth/logout`, {
170
58
  method: "POST",
171
59
  headers: {
172
60
  "Content-Type": "application/json",
173
- Authorization: `Bearer ${token}`
61
+ Authorization: `Bearer ${accessToken}`
174
62
  },
175
- body: JSON.stringify({ logout_all: false })
176
- }).catch(() => {
63
+ credentials: "include",
64
+ body: JSON.stringify({ logout_all: logoutAll })
177
65
  });
66
+ } catch (e) {
67
+ console.error("[SanvikaAuth] Logout API error:", e);
68
+ } finally {
69
+ localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
70
+ localStorage.removeItem(STORAGE_KEYS.USER);
71
+ setToken(null);
72
+ setUser(null);
178
73
  }
179
- localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
180
- localStorage.removeItem(STORAGE_KEYS.USER);
181
- setUser(null);
182
- setToken(null);
183
- }, []);
184
- const updateUser = useCallback((partial) => {
185
- setUser((prev) => {
186
- if (!prev) return prev;
187
- const merged = { ...prev, ...partial };
188
- localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(merged));
189
- return merged;
190
- });
191
- }, []);
192
- const redirectToLogin = useCallback(
193
- (returnPath) => {
194
- if (returnPath) {
195
- localStorage.setItem(STORAGE_KEYS.RETURN_PATH, returnPath);
196
- }
197
- const state = Math.random().toString(36).slice(2);
198
- localStorage.setItem(STORAGE_KEYS.STATE, state);
199
- const url = new URL(`${SA_URL}/authorize`);
200
- url.searchParams.set("client_id", clientId);
201
- url.searchParams.set("redirect_uri", redirectUri);
202
- url.searchParams.set("response_type", "code");
203
- url.searchParams.set("state", state);
204
- const appTheme = localStorage.getItem("theme");
205
- if (appTheme === "light" || appTheme === "dark") {
206
- url.searchParams.set("theme", appTheme);
207
- }
208
- window.location.href = url.toString();
209
- },
210
- [clientId, redirectUri]
211
- );
212
- return /* @__PURE__ */ jsx(
213
- SanvikaAuthContext.Provider,
214
- {
215
- value: {
216
- user,
217
- accessToken,
218
- loading,
219
- isLoggedIn: !!user,
220
- login,
221
- logout,
222
- updateUser,
223
- redirectToLogin,
224
- clientId,
225
- redirectUri,
226
- dashboardPath
227
- },
228
- children
229
- }
230
- );
74
+ };
75
+ const value = {
76
+ user,
77
+ accessToken,
78
+ loading,
79
+ isAuthenticated: !!user,
80
+ login,
81
+ logout
82
+ };
83
+ return /* @__PURE__ */ jsx(SanvikaAuthContext.Provider, { value, children });
231
84
  }
232
85
  function useSanvikaAuth() {
233
86
  const ctx = useContext(SanvikaAuthContext);
234
- if (!ctx)
235
- throw new Error("useSanvikaAuth must be used inside <SanvikaAuthProvider>");
236
87
  return ctx;
237
88
  }
238
89
 
239
90
  // SanvikaAccountButton.jsx
240
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
91
+ import { useEffect as useEffect2, useRef, useState as useState2, Component } from "react";
241
92
 
242
93
  // #style-inject:#style-inject
243
94
  function styleInject(css, { insertAt } = {}) {
@@ -266,6 +117,22 @@ styleInject("@keyframes snvk-shimmer {\n 0% {\n background-position: 200% 0;
266
117
 
267
118
  // SanvikaAccountButton.jsx
268
119
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
120
+ var SanvikaAccountButtonErrorBoundary = class extends Component {
121
+ constructor(props) {
122
+ super(props);
123
+ this.state = { hasError: false };
124
+ }
125
+ static getDerivedStateFromError(error) {
126
+ return { hasError: true };
127
+ }
128
+ static displayName = "SanvikaAccountButtonErrorBoundary";
129
+ render() {
130
+ if (this.state.hasError) {
131
+ return /* @__PURE__ */ jsx2("div", { className: "snvk-skeleton", "aria-hidden": "true" });
132
+ }
133
+ return this.props.children;
134
+ }
135
+ };
269
136
  function UserIcon({ size = 18 }) {
270
137
  return /* @__PURE__ */ jsxs(
271
138
  "svg",
@@ -324,48 +191,20 @@ function DashboardIcon() {
324
191
  }
325
192
  );
326
193
  }
327
- function SanvikaAccountButton({
328
- text = "Account",
329
- hideTextOnMobile = false,
194
+ function SanvikaAccountButtonContent({
195
+ text,
196
+ hideTextOnMobile,
330
197
  onLoginClick,
331
198
  onProfileClick,
332
- onboardingPath = null,
333
- className = ""
199
+ onboardingPath,
200
+ className
334
201
  }) {
335
- var _a;
336
- const {
337
- user,
338
- loading,
339
- isLoggedIn,
340
- redirectToLogin,
341
- dashboardPath,
342
- updateUser
343
- } = useSanvikaAuth();
202
+ var _a, _b;
203
+ const auth = useSanvikaAuth();
344
204
  const [dropdownOpen, setDropdownOpen] = useState2(false);
345
205
  const [imgError, setImgError] = useState2(false);
346
- const [prevImage, setPrevImage] = useState2(user == null ? void 0 : user.image);
347
- const wrapperRef = useRef2(null);
348
- if ((user == null ? void 0 : user.image) !== prevImage) {
349
- setPrevImage(user == null ? void 0 : user.image);
350
- setImgError(false);
351
- }
352
- useEffect2(() => {
353
- if (!user) return;
354
- let disposed = false;
355
- const handleProfileUpdated = async () => {
356
- if (disposed) return;
357
- try {
358
- const stored = localStorage.getItem("sanvika_user");
359
- if (stored) updateUser(JSON.parse(stored));
360
- } catch {
361
- }
362
- };
363
- window.addEventListener("profileUpdated", handleProfileUpdated);
364
- return () => {
365
- disposed = true;
366
- window.removeEventListener("profileUpdated", handleProfileUpdated);
367
- };
368
- }, [user, updateUser]);
206
+ const [prevImage, setPrevImage] = useState2((_a = auth == null ? void 0 : auth.user) == null ? void 0 : _a.image);
207
+ const wrapperRef = useRef(null);
369
208
  useEffect2(() => {
370
209
  if (!dropdownOpen) return;
371
210
  function handleOutside(e) {
@@ -376,10 +215,38 @@ function SanvikaAccountButton({
376
215
  document.addEventListener("mousedown", handleOutside);
377
216
  return () => document.removeEventListener("mousedown", handleOutside);
378
217
  }, [dropdownOpen]);
379
- if (loading) {
380
- return /* @__PURE__ */ jsx2("div", { className: "snvk-skeleton", "aria-hidden": "true" });
218
+ if (!auth) {
219
+ return /* @__PURE__ */ jsxs(
220
+ "button",
221
+ {
222
+ className: `snvk-guestBtn ${className}`,
223
+ onClick: () => {
224
+ if (onLoginClick) {
225
+ onLoginClick();
226
+ } else {
227
+ window.location.href = "https://accounts.sanvikaproduction.com";
228
+ }
229
+ },
230
+ "aria-label": "Login or Sign Up",
231
+ children: [
232
+ /* @__PURE__ */ jsx2("span", { className: "snvk-iconWrap", children: /* @__PURE__ */ jsx2(UserIcon, { size: 18 }) }),
233
+ /* @__PURE__ */ jsx2(
234
+ "span",
235
+ {
236
+ className: hideTextOnMobile ? "snvk-hideTextOnMobile" : "snvk-textWrap",
237
+ children: text
238
+ }
239
+ )
240
+ ]
241
+ }
242
+ );
243
+ }
244
+ const { user, loading, isAuthenticated, logout } = auth;
245
+ if ((user == null ? void 0 : user.image) !== prevImage) {
246
+ setPrevImage(user == null ? void 0 : user.image);
247
+ setImgError(false);
381
248
  }
382
- if (!isLoggedIn) {
249
+ if (!isAuthenticated || loading) {
383
250
  return /* @__PURE__ */ jsxs(
384
251
  "button",
385
252
  {
@@ -388,7 +255,7 @@ function SanvikaAccountButton({
388
255
  if (onLoginClick) {
389
256
  onLoginClick();
390
257
  } else {
391
- redirectToLogin(window.location.pathname);
258
+ window.location.href = "https://accounts.sanvikaproduction.com";
392
259
  }
393
260
  },
394
261
  "aria-label": "Login or Sign Up",
@@ -405,15 +272,15 @@ function SanvikaAccountButton({
405
272
  }
406
273
  );
407
274
  }
408
- const displayName = user.firstName || ((_a = user.mobile) == null ? void 0 : _a.slice(-4)) || "Me";
275
+ const displayName = user.firstName || ((_b = user.mobile) == null ? void 0 : _b.slice(-4)) || "Me";
409
276
  const imageSrc = !imgError && user.image ? user.image : DEFAULT_AVATAR_SVG;
410
277
  const handleProfileClick = () => {
411
278
  if (onProfileClick) return onProfileClick();
412
- if (onboardingPath && user.status === "onboarding") {
413
- window.location.href = onboardingPath;
414
- } else {
415
- window.location.href = dashboardPath;
416
- }
279
+ window.location.href = "https://accounts.sanvikaproduction.com/dashboard";
280
+ };
281
+ const handleLogout = async () => {
282
+ await logout(false);
283
+ setDropdownOpen(false);
417
284
  };
418
285
  return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: `snvk-wrapper ${className}`, children: [
419
286
  /* @__PURE__ */ jsxs(
@@ -468,7 +335,7 @@ function SanvikaAccountButton({
468
335
  /* @__PURE__ */ jsxs(
469
336
  "a",
470
337
  {
471
- href: onboardingPath && user.status === "onboarding" ? onboardingPath : dashboardPath,
338
+ href: "https://accounts.sanvikaproduction.com/dashboard",
472
339
  className: "snvk-dropdownItem",
473
340
  role: "menuitem",
474
341
  onClick: () => setDropdownOpen(false),
@@ -477,18 +344,47 @@ function SanvikaAccountButton({
477
344
  /* @__PURE__ */ jsx2("span", { children: "Dashboard" })
478
345
  ]
479
346
  }
347
+ ),
348
+ /* @__PURE__ */ jsx2("div", { className: "snvk-divider" }),
349
+ /* @__PURE__ */ jsxs(
350
+ "button",
351
+ {
352
+ className: "snvk-dropdownItem",
353
+ role: "menuitem",
354
+ onClick: handleLogout,
355
+ children: [
356
+ /* @__PURE__ */ jsxs(
357
+ "svg",
358
+ {
359
+ width: "15",
360
+ height: "15",
361
+ viewBox: "0 0 24 24",
362
+ fill: "none",
363
+ stroke: "currentColor",
364
+ strokeWidth: "2",
365
+ strokeLinecap: "round",
366
+ children: [
367
+ /* @__PURE__ */ jsx2("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
368
+ /* @__PURE__ */ jsx2("polyline", { points: "16 17 21 12 16 7" }),
369
+ /* @__PURE__ */ jsx2("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
370
+ ]
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsx2("span", { children: "Logout" })
374
+ ]
375
+ }
480
376
  )
481
377
  ] })
482
378
  ] });
483
379
  }
380
+ function SanvikaAccountButton(props) {
381
+ return /* @__PURE__ */ jsx2(SanvikaAccountButtonErrorBoundary, { children: /* @__PURE__ */ jsx2(SanvikaAccountButtonContent, { ...props }) });
382
+ }
484
383
  export {
485
384
  DEFAULT_AVATAR_SVG,
486
385
  STORAGE_KEYS,
487
386
  SanvikaAccountButton,
488
387
  SanvikaAuthContext,
489
388
  SanvikaAuthProvider,
490
- decodeToken,
491
- ensureTokenFresh,
492
- scheduleTokenRefresh,
493
389
  useSanvikaAuth
494
390
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanvika/auth",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Sanvika Auth SDK — React components and hooks for Sanvika SSO integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,6 @@
29
29
  "sanvika",
30
30
  "auth",
31
31
  "sso",
32
- "oauth",
33
32
  "react",
34
33
  "nextjs"
35
34
  ],