@sanvika/auth 1.0.3 → 1.0.5

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/dist/index.js ADDED
@@ -0,0 +1,435 @@
1
+ // SanvikaAuthProvider.jsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useState,
7
+ useCallback
8
+ } from "react";
9
+
10
+ // constants.js
11
+ var STORAGE_KEYS = {
12
+ ACCESS_TOKEN: "sanvika_access_token",
13
+ USER: "sanvika_user",
14
+ STATE: "sanvika_oauth_state",
15
+ // CSRF state for OAuth flow
16
+ RETURN_PATH: "sanvika_return_path"
17
+ // Page to redirect back after login
18
+ };
19
+ var DEFAULT_IAM_URL = "https://accounts.sanvikaproduction.com";
20
+ 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`;
21
+
22
+ // SanvikaAuthProvider.jsx
23
+ import { jsx } from "react/jsx-runtime";
24
+ var SanvikaAuthContext = createContext(null);
25
+ function SanvikaAuthProvider({
26
+ children,
27
+ clientId,
28
+ redirectUri,
29
+ iamUrl = DEFAULT_IAM_URL,
30
+ dashboardPath = "/dashboard"
31
+ }) {
32
+ const [user, setUser] = useState(null);
33
+ const [accessToken, setToken] = useState(null);
34
+ const [loading, setLoading] = useState(true);
35
+ useEffect(() => {
36
+ try {
37
+ const storedToken = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
38
+ const storedUser = localStorage.getItem(STORAGE_KEYS.USER);
39
+ if (storedToken && storedUser) {
40
+ setToken(storedToken);
41
+ setUser(JSON.parse(storedUser));
42
+ }
43
+ } catch {
44
+ localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
45
+ localStorage.removeItem(STORAGE_KEYS.USER);
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ }, []);
50
+ useEffect(() => {
51
+ function onStorage(e) {
52
+ if (e.key === STORAGE_KEYS.ACCESS_TOKEN && !e.newValue) {
53
+ setUser(null);
54
+ setToken(null);
55
+ }
56
+ if (e.key === STORAGE_KEYS.ACCESS_TOKEN && e.newValue) {
57
+ try {
58
+ const u = localStorage.getItem(STORAGE_KEYS.USER);
59
+ if (u) setUser(JSON.parse(u));
60
+ setToken(e.newValue);
61
+ } catch {
62
+ }
63
+ }
64
+ }
65
+ window.addEventListener("storage", onStorage);
66
+ return () => window.removeEventListener("storage", onStorage);
67
+ }, []);
68
+ const login = useCallback((token, userData) => {
69
+ localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
70
+ localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
71
+ setToken(token);
72
+ setUser(userData);
73
+ }, []);
74
+ const logout = useCallback(async () => {
75
+ const token = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
76
+ if (token) {
77
+ fetch(`${iamUrl}/api/auth/logout`, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify({ logout_all: false }),
81
+ credentials: "include"
82
+ }).catch(() => {
83
+ });
84
+ }
85
+ localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
86
+ localStorage.removeItem(STORAGE_KEYS.USER);
87
+ setUser(null);
88
+ setToken(null);
89
+ }, [iamUrl]);
90
+ const updateUser = useCallback((partial) => {
91
+ setUser((prev) => {
92
+ if (!prev) return prev;
93
+ const merged = { ...prev, ...partial };
94
+ localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(merged));
95
+ return merged;
96
+ });
97
+ }, []);
98
+ const redirectToLogin = useCallback(
99
+ (returnPath) => {
100
+ if (returnPath) {
101
+ localStorage.setItem(STORAGE_KEYS.RETURN_PATH, returnPath);
102
+ }
103
+ const state = Math.random().toString(36).slice(2);
104
+ localStorage.setItem(STORAGE_KEYS.STATE, state);
105
+ const url = new URL(`${iamUrl}/authorize`);
106
+ url.searchParams.set("client_id", clientId);
107
+ url.searchParams.set("redirect_uri", redirectUri);
108
+ url.searchParams.set("response_type", "code");
109
+ url.searchParams.set("state", state);
110
+ const appTheme = localStorage.getItem("theme");
111
+ if (appTheme === "light" || appTheme === "dark") {
112
+ url.searchParams.set("theme", appTheme);
113
+ }
114
+ window.location.href = url.toString();
115
+ },
116
+ [iamUrl, clientId, redirectUri]
117
+ );
118
+ return /* @__PURE__ */ jsx(
119
+ SanvikaAuthContext.Provider,
120
+ {
121
+ value: {
122
+ user,
123
+ accessToken,
124
+ loading,
125
+ isLoggedIn: !!user,
126
+ login,
127
+ logout,
128
+ updateUser,
129
+ redirectToLogin,
130
+ clientId,
131
+ redirectUri,
132
+ iamUrl,
133
+ dashboardPath
134
+ },
135
+ children
136
+ }
137
+ );
138
+ }
139
+ function useSanvikaAuth() {
140
+ const ctx = useContext(SanvikaAuthContext);
141
+ if (!ctx)
142
+ throw new Error("useSanvikaAuth must be used inside <SanvikaAuthProvider>");
143
+ return ctx;
144
+ }
145
+
146
+ // SanvikaAccountButton.jsx
147
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
148
+
149
+ // #style-inject:#style-inject
150
+ function styleInject(css, { insertAt } = {}) {
151
+ if (!css || typeof document === "undefined") return;
152
+ const head = document.head || document.getElementsByTagName("head")[0];
153
+ const style = document.createElement("style");
154
+ style.type = "text/css";
155
+ if (insertAt === "top") {
156
+ if (head.firstChild) {
157
+ head.insertBefore(style, head.firstChild);
158
+ } else {
159
+ head.appendChild(style);
160
+ }
161
+ } else {
162
+ head.appendChild(style);
163
+ }
164
+ if (style.styleSheet) {
165
+ style.styleSheet.cssText = css;
166
+ } else {
167
+ style.appendChild(document.createTextNode(css));
168
+ }
169
+ }
170
+
171
+ // SanvikaAccountButton.css
172
+ 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}\n.snvk-profileBtn:hover {\n background: var(--hover-color, #ebebeb);\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, #ffffff);\n border: 1px solid var(--border-color-light, #e5e7eb);\n border-radius: 12px;\n box-shadow: 0 8px 24px var(--shadow-color, rgba(0, 0, 0, 0.12));\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, #888888);\n margin-top: 2px;\n transition: color 0.3s ease;\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, #333333);\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");
173
+
174
+ // SanvikaAccountButton.jsx
175
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
176
+ function UserIcon({ size = 18 }) {
177
+ return /* @__PURE__ */ jsxs(
178
+ "svg",
179
+ {
180
+ width: size,
181
+ height: size,
182
+ viewBox: "0 0 24 24",
183
+ fill: "none",
184
+ stroke: "currentColor",
185
+ strokeWidth: "2",
186
+ strokeLinecap: "round",
187
+ strokeLinejoin: "round",
188
+ children: [
189
+ /* @__PURE__ */ jsx2("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }),
190
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "7", r: "4" })
191
+ ]
192
+ }
193
+ );
194
+ }
195
+ function ChevronIcon({ open }) {
196
+ return /* @__PURE__ */ jsx2(
197
+ "svg",
198
+ {
199
+ width: "12",
200
+ height: "12",
201
+ viewBox: "0 0 24 24",
202
+ fill: "none",
203
+ stroke: "currentColor",
204
+ strokeWidth: "2.5",
205
+ strokeLinecap: "round",
206
+ style: {
207
+ transform: open ? "rotate(180deg)" : "rotate(0deg)",
208
+ transition: "transform 0.2s"
209
+ },
210
+ children: /* @__PURE__ */ jsx2("polyline", { points: "6 9 12 15 18 9" })
211
+ }
212
+ );
213
+ }
214
+ function DashboardIcon() {
215
+ return /* @__PURE__ */ jsxs(
216
+ "svg",
217
+ {
218
+ width: "15",
219
+ height: "15",
220
+ viewBox: "0 0 24 24",
221
+ fill: "none",
222
+ stroke: "currentColor",
223
+ strokeWidth: "2",
224
+ strokeLinecap: "round",
225
+ children: [
226
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "3", width: "7", height: "7" }),
227
+ /* @__PURE__ */ jsx2("rect", { x: "14", y: "3", width: "7", height: "7" }),
228
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "14", width: "7", height: "7" }),
229
+ /* @__PURE__ */ jsx2("rect", { x: "14", y: "14", width: "7", height: "7" })
230
+ ]
231
+ }
232
+ );
233
+ }
234
+ function LogoutIcon() {
235
+ return /* @__PURE__ */ jsxs(
236
+ "svg",
237
+ {
238
+ width: "15",
239
+ height: "15",
240
+ viewBox: "0 0 24 24",
241
+ fill: "none",
242
+ stroke: "currentColor",
243
+ strokeWidth: "2",
244
+ strokeLinecap: "round",
245
+ children: [
246
+ /* @__PURE__ */ jsx2("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
247
+ /* @__PURE__ */ jsx2("polyline", { points: "16 17 21 12 16 7" }),
248
+ /* @__PURE__ */ jsx2("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
249
+ ]
250
+ }
251
+ );
252
+ }
253
+ function SanvikaAccountButton({
254
+ text = "Account",
255
+ hideTextOnMobile = false,
256
+ onLoginClick,
257
+ onProfileClick,
258
+ onboardingPath = null,
259
+ className = ""
260
+ }) {
261
+ var _a;
262
+ const {
263
+ user,
264
+ loading,
265
+ isLoggedIn,
266
+ logout,
267
+ redirectToLogin,
268
+ dashboardPath,
269
+ updateUser
270
+ } = useSanvikaAuth();
271
+ const [dropdownOpen, setDropdownOpen] = useState2(false);
272
+ const [imgError, setImgError] = useState2(false);
273
+ const [prevImage, setPrevImage] = useState2(user == null ? void 0 : user.image);
274
+ const wrapperRef = useRef(null);
275
+ if ((user == null ? void 0 : user.image) !== prevImage) {
276
+ setPrevImage(user == null ? void 0 : user.image);
277
+ setImgError(false);
278
+ }
279
+ useEffect2(() => {
280
+ if (!user) return;
281
+ let disposed = false;
282
+ const handleProfileUpdated = async () => {
283
+ if (disposed) return;
284
+ try {
285
+ const stored = localStorage.getItem("sanvika_user");
286
+ if (stored) updateUser(JSON.parse(stored));
287
+ } catch {
288
+ }
289
+ };
290
+ window.addEventListener("profileUpdated", handleProfileUpdated);
291
+ return () => {
292
+ disposed = true;
293
+ window.removeEventListener("profileUpdated", handleProfileUpdated);
294
+ };
295
+ }, [user, updateUser]);
296
+ useEffect2(() => {
297
+ if (!dropdownOpen) return;
298
+ function handleOutside(e) {
299
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
300
+ setDropdownOpen(false);
301
+ }
302
+ }
303
+ document.addEventListener("mousedown", handleOutside);
304
+ return () => document.removeEventListener("mousedown", handleOutside);
305
+ }, [dropdownOpen]);
306
+ if (loading) {
307
+ return /* @__PURE__ */ jsx2("div", { className: "snvk-skeleton", "aria-hidden": "true" });
308
+ }
309
+ if (!isLoggedIn) {
310
+ return /* @__PURE__ */ jsxs(
311
+ "button",
312
+ {
313
+ className: `snvk-guestBtn ${className}`,
314
+ onClick: () => {
315
+ if (onLoginClick) {
316
+ onLoginClick();
317
+ } else {
318
+ redirectToLogin(window.location.pathname);
319
+ }
320
+ },
321
+ "aria-label": "Login or Sign Up",
322
+ children: [
323
+ /* @__PURE__ */ jsx2("span", { className: "snvk-iconWrap", children: /* @__PURE__ */ jsx2(UserIcon, { size: 18 }) }),
324
+ /* @__PURE__ */ jsx2(
325
+ "span",
326
+ {
327
+ className: hideTextOnMobile ? "snvk-hideTextOnMobile" : "snvk-textWrap",
328
+ children: text
329
+ }
330
+ )
331
+ ]
332
+ }
333
+ );
334
+ }
335
+ const displayName = user.firstName || ((_a = user.mobile) == null ? void 0 : _a.slice(-4)) || "Me";
336
+ const imageSrc = !imgError && user.image ? user.image : DEFAULT_AVATAR_SVG;
337
+ const handleProfileClick = () => {
338
+ if (onProfileClick) return onProfileClick();
339
+ if (onboardingPath && user.status === "onboarding") {
340
+ window.location.href = onboardingPath;
341
+ } else {
342
+ window.location.href = dashboardPath;
343
+ }
344
+ };
345
+ return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: `snvk-wrapper ${className}`, children: [
346
+ /* @__PURE__ */ jsxs(
347
+ "button",
348
+ {
349
+ className: "snvk-profileBtn",
350
+ onClick: () => setDropdownOpen((o) => !o),
351
+ onDoubleClick: handleProfileClick,
352
+ "aria-label": "Account menu",
353
+ "aria-expanded": dropdownOpen,
354
+ children: [
355
+ /* @__PURE__ */ jsx2(
356
+ "img",
357
+ {
358
+ src: imageSrc,
359
+ alt: displayName,
360
+ className: "snvk-avatar",
361
+ onError: () => setImgError(true)
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsx2(
365
+ "span",
366
+ {
367
+ className: hideTextOnMobile ? "snvk-hideTextOnMobile" : "snvk-textWrap",
368
+ children: displayName
369
+ }
370
+ ),
371
+ /* @__PURE__ */ jsx2(ChevronIcon, { open: dropdownOpen })
372
+ ]
373
+ }
374
+ ),
375
+ dropdownOpen && /* @__PURE__ */ jsxs("div", { className: "snvk-dropdown", role: "menu", children: [
376
+ /* @__PURE__ */ jsxs("div", { className: "snvk-dropdownHeader", children: [
377
+ /* @__PURE__ */ jsx2(
378
+ "img",
379
+ {
380
+ src: imageSrc,
381
+ alt: displayName,
382
+ className: "snvk-dropdownAvatar",
383
+ onError: () => setImgError(true)
384
+ }
385
+ ),
386
+ /* @__PURE__ */ jsxs("div", { children: [
387
+ /* @__PURE__ */ jsx2("div", { className: "snvk-dropdownName", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
388
+ /* @__PURE__ */ jsxs("div", { className: "snvk-dropdownMobile", children: [
389
+ "+91 ",
390
+ user.mobile
391
+ ] })
392
+ ] })
393
+ ] }),
394
+ /* @__PURE__ */ jsx2("div", { className: "snvk-divider" }),
395
+ /* @__PURE__ */ jsxs(
396
+ "a",
397
+ {
398
+ href: onboardingPath && user.status === "onboarding" ? onboardingPath : dashboardPath,
399
+ className: "snvk-dropdownItem",
400
+ role: "menuitem",
401
+ onClick: () => setDropdownOpen(false),
402
+ children: [
403
+ /* @__PURE__ */ jsx2(DashboardIcon, {}),
404
+ /* @__PURE__ */ jsx2("span", { children: "Dashboard" })
405
+ ]
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsxs(
409
+ "button",
410
+ {
411
+ className: "snvk-dropdownItem snvk-logoutItem",
412
+ role: "menuitem",
413
+ onClick: async () => {
414
+ setDropdownOpen(false);
415
+ await logout();
416
+ window.location.href = "/";
417
+ },
418
+ children: [
419
+ /* @__PURE__ */ jsx2(LogoutIcon, {}),
420
+ /* @__PURE__ */ jsx2("span", { children: "Logout" })
421
+ ]
422
+ }
423
+ )
424
+ ] })
425
+ ] });
426
+ }
427
+ export {
428
+ DEFAULT_AVATAR_SVG,
429
+ DEFAULT_IAM_URL,
430
+ STORAGE_KEYS,
431
+ SanvikaAccountButton,
432
+ SanvikaAuthContext,
433
+ SanvikaAuthProvider,
434
+ useSanvikaAuth
435
+ };
package/package.json CHANGED
@@ -1,22 +1,29 @@
1
1
  {
2
2
  "name": "@sanvika/auth",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Sanvika Auth SDK — React components and hooks for Sanvika SSO integration",
5
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
7
11
  "files": [
8
- "index.js",
9
- "SanvikaAuthProvider.js",
10
- "SanvikaAccountButton.js",
11
- "SanvikaAccountButton.module.css",
12
- "constants.js"
12
+ "dist"
13
13
  ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "prepublishOnly": "npm run build"
17
+ },
14
18
  "publishConfig": {
15
19
  "access": "public"
16
20
  },
17
21
  "peerDependencies": {
18
- "react": ">=18",
19
- "next": ">=14"
22
+ "next": ">=14",
23
+ "react": ">=18"
24
+ },
25
+ "devDependencies": {
26
+ "tsup": "^8.5.1"
20
27
  },
21
28
  "keywords": [
22
29
  "sanvika",