@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.
@@ -1,295 +0,0 @@
1
- "use client";
2
- // ============================================================
3
- // SanvikaAccountButton — Drop-in navbar button for all Sanvika apps
4
- //
5
- // States:
6
- // loading → skeleton placeholder (no flash)
7
- // guest → "Account" button → redirects to Sanvika SSO
8
- // logged-in → user image + firstName → dropdown (Dashboard + Logout)
9
- //
10
- // Requirements:
11
- // 1. Wrap your root layout with <SanvikaAuthProvider ...>
12
- // 2. Drop <SanvikaAccountButton /> anywhere in your navbar
13
- // No other setup needed.
14
- //
15
- // Props:
16
- // text — button label when guest (default: "Account")
17
- // hideTextOnMobile — hides text on small screens (default: false)
18
- // onLoginClick — optional override for login click (default: redirectToSSO)
19
- // onProfileClick — optional override for profile click (default: dashboardPath)
20
- // onboardingPath — where to redirect if user.status === "onboarding" (default: null)
21
- // className — extra CSS class for outer wrapper
22
- // ============================================================
23
-
24
- import { useEffect, useRef, useState } from "react";
25
- import { useSanvikaAuth } from "./SanvikaAuthProvider.js";
26
- import { DEFAULT_AVATAR_SVG } from "./constants.js";
27
- import styles from "./SanvikaAccountButton.module.css";
28
-
29
- // ─── SVG Icons ─────────────────────────────────────────────────────────────
30
-
31
- function UserIcon({ size = 18 }) {
32
- return (
33
- <svg
34
- width={size}
35
- height={size}
36
- viewBox="0 0 24 24"
37
- fill="none"
38
- stroke="currentColor"
39
- strokeWidth="2"
40
- strokeLinecap="round"
41
- strokeLinejoin="round"
42
- >
43
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
44
- <circle cx="12" cy="7" r="4" />
45
- </svg>
46
- );
47
- }
48
-
49
- function ChevronIcon({ open }) {
50
- return (
51
- <svg
52
- width="12"
53
- height="12"
54
- viewBox="0 0 24 24"
55
- fill="none"
56
- stroke="currentColor"
57
- strokeWidth="2.5"
58
- strokeLinecap="round"
59
- style={{
60
- transform: open ? "rotate(180deg)" : "rotate(0deg)",
61
- transition: "transform 0.2s",
62
- }}
63
- >
64
- <polyline points="6 9 12 15 18 9" />
65
- </svg>
66
- );
67
- }
68
-
69
- function DashboardIcon() {
70
- return (
71
- <svg
72
- width="15"
73
- height="15"
74
- viewBox="0 0 24 24"
75
- fill="none"
76
- stroke="currentColor"
77
- strokeWidth="2"
78
- strokeLinecap="round"
79
- >
80
- <rect x="3" y="3" width="7" height="7" />
81
- <rect x="14" y="3" width="7" height="7" />
82
- <rect x="3" y="14" width="7" height="7" />
83
- <rect x="14" y="14" width="7" height="7" />
84
- </svg>
85
- );
86
- }
87
-
88
- function LogoutIcon() {
89
- return (
90
- <svg
91
- width="15"
92
- height="15"
93
- viewBox="0 0 24 24"
94
- fill="none"
95
- stroke="currentColor"
96
- strokeWidth="2"
97
- strokeLinecap="round"
98
- >
99
- <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
100
- <polyline points="16 17 21 12 16 7" />
101
- <line x1="21" y1="12" x2="9" y2="12" />
102
- </svg>
103
- );
104
- }
105
-
106
- // ─── Main Component ────────────────────────────────────────────────────────
107
-
108
- export default function SanvikaAccountButton({
109
- text = "Account",
110
- hideTextOnMobile = false,
111
- onLoginClick,
112
- onProfileClick,
113
- onboardingPath = null,
114
- className = "",
115
- }) {
116
- const {
117
- user,
118
- loading,
119
- isLoggedIn,
120
- logout,
121
- redirectToLogin,
122
- dashboardPath,
123
- updateUser,
124
- } = useSanvikaAuth();
125
- const [dropdownOpen, setDropdownOpen] = useState(false);
126
- const [imgError, setImgError] = useState(false);
127
- const [prevImage, setPrevImage] = useState(user?.image);
128
- const wrapperRef = useRef(null);
129
-
130
- // Derived state — reset imgError when image changes
131
- if (user?.image !== prevImage) {
132
- setPrevImage(user?.image);
133
- setImgError(false);
134
- }
135
-
136
- // Listen for profileUpdated event → refresh user in context
137
- useEffect(() => {
138
- if (!user) return;
139
- let disposed = false;
140
- const handleProfileUpdated = async () => {
141
- if (disposed) return;
142
- try {
143
- const stored = localStorage.getItem("sanvika_user");
144
- if (stored) updateUser(JSON.parse(stored));
145
- } catch {
146
- /* ignore */
147
- }
148
- };
149
- window.addEventListener("profileUpdated", handleProfileUpdated);
150
- return () => {
151
- disposed = true;
152
- window.removeEventListener("profileUpdated", handleProfileUpdated);
153
- };
154
- }, [user, updateUser]);
155
-
156
- // Close dropdown on outside click
157
- useEffect(() => {
158
- if (!dropdownOpen) return;
159
- function handleOutside(e) {
160
- if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
161
- setDropdownOpen(false);
162
- }
163
- }
164
- document.addEventListener("mousedown", handleOutside);
165
- return () => document.removeEventListener("mousedown", handleOutside);
166
- }, [dropdownOpen]);
167
-
168
- // ── Loading skeleton ─────────────────────────────────────────────────
169
- if (loading) {
170
- return <div className={styles.skeleton} aria-hidden="true" />;
171
- }
172
-
173
- // ── Guest state ──────────────────────────────────────────────────────
174
- if (!isLoggedIn) {
175
- return (
176
- <button
177
- className={`${styles.guestBtn} ${className}`}
178
- onClick={() => {
179
- if (onLoginClick) {
180
- onLoginClick();
181
- } else {
182
- redirectToLogin(window.location.pathname);
183
- }
184
- }}
185
- aria-label="Login or Sign Up"
186
- >
187
- <span className={styles.iconWrap}>
188
- <UserIcon size={18} />
189
- </span>
190
- <span
191
- className={
192
- hideTextOnMobile ? styles.hideTextOnMobile : styles.textWrap
193
- }
194
- >
195
- {text}
196
- </span>
197
- </button>
198
- );
199
- }
200
-
201
- // ── Logged-in state ──────────────────────────────────────────────────
202
- const displayName = user.firstName || user.mobile?.slice(-4) || "Me";
203
- const imageSrc = !imgError && user.image ? user.image : DEFAULT_AVATAR_SVG;
204
-
205
- // Profile click — onboarding check
206
- const handleProfileClick = () => {
207
- if (onProfileClick) return onProfileClick();
208
- if (onboardingPath && user.status === "onboarding") {
209
- window.location.href = onboardingPath;
210
- } else {
211
- window.location.href = dashboardPath;
212
- }
213
- };
214
-
215
- return (
216
- <div ref={wrapperRef} className={`${styles.wrapper} ${className}`}>
217
- {/* Avatar + Name button */}
218
- <button
219
- className={styles.profileBtn}
220
- onClick={() => setDropdownOpen((o) => !o)}
221
- onDoubleClick={handleProfileClick}
222
- aria-label="Account menu"
223
- aria-expanded={dropdownOpen}
224
- >
225
- {/* eslint-disable-next-line @next/next/no-img-element */}
226
- <img
227
- src={imageSrc}
228
- alt={displayName}
229
- className={styles.avatar}
230
- onError={() => setImgError(true)}
231
- />
232
- <span
233
- className={
234
- hideTextOnMobile ? styles.hideTextOnMobile : styles.textWrap
235
- }
236
- >
237
- {displayName}
238
- </span>
239
- <ChevronIcon open={dropdownOpen} />
240
- </button>
241
-
242
- {/* Dropdown */}
243
- {dropdownOpen && (
244
- <div className={styles.dropdown} role="menu">
245
- {/* User info header */}
246
- <div className={styles.dropdownHeader}>
247
- {/* eslint-disable-next-line @next/next/no-img-element */}
248
- <img
249
- src={imageSrc}
250
- alt={displayName}
251
- className={styles.dropdownAvatar}
252
- onError={() => setImgError(true)}
253
- />
254
- <div>
255
- <div className={styles.dropdownName}>
256
- {[user.firstName, user.lastName].filter(Boolean).join(" ")}
257
- </div>
258
- <div className={styles.dropdownMobile}>+91 {user.mobile}</div>
259
- </div>
260
- </div>
261
- <div className={styles.divider} />
262
-
263
- {/* Dashboard link */}
264
- <a
265
- href={
266
- onboardingPath && user.status === "onboarding"
267
- ? onboardingPath
268
- : dashboardPath
269
- }
270
- className={styles.dropdownItem}
271
- role="menuitem"
272
- onClick={() => setDropdownOpen(false)}
273
- >
274
- <DashboardIcon />
275
- <span>Dashboard</span>
276
- </a>
277
-
278
- {/* Logout */}
279
- <button
280
- className={`${styles.dropdownItem} ${styles.logoutItem}`}
281
- role="menuitem"
282
- onClick={async () => {
283
- setDropdownOpen(false);
284
- await logout();
285
- window.location.href = "/";
286
- }}
287
- >
288
- <LogoutIcon />
289
- <span>Logout</span>
290
- </button>
291
- </div>
292
- )}
293
- </div>
294
- );
295
- }
@@ -1,418 +0,0 @@
1
- /* ============================================================
2
- SanvikaAccountButton — Component Styles
3
-
4
- DESIGN DECISION:
5
- - Guest button (Login/Account): Sanvika brand colors HARDCODED
6
- → Always purple, regardless of host project's theme
7
- → CSS var with fallback: var(--sanvika-brand-color, #4f46e5)
8
- - Profile button + Dropdown: host project theme vars + fallbacks
9
- → var(--card-bg, #fff) — adapts if host has theme system
10
-
11
- clamp() for fluid sizing, 44px touch targets, CSS Modules only.
12
- ============================================================ */
13
-
14
- /* ── Shimmer keyframe ───────────────────────────────────────── */
15
- @keyframes shimmer {
16
- 0% {
17
- background-position: 200% 0;
18
- }
19
- 100% {
20
- background-position: -200% 0;
21
- }
22
- }
23
-
24
- /* ── Loading Skeleton ───────────────────────────────────────── */
25
- .skeleton {
26
- width: clamp(72px, 20vw, 96px);
27
- height: clamp(34px, 8vw, 40px);
28
- border-radius: 8px;
29
- background: linear-gradient(
30
- 90deg,
31
- var(--skeleton-base-color, #d0d0d0) 25%,
32
- var(--skeleton-highlight-color, #f0f0f0) 50%,
33
- var(--skeleton-base-color, #d0d0d0) 75%
34
- );
35
- background-size: 200% 100%;
36
- animation: shimmer 1.4s infinite;
37
- display: inline-block;
38
- }
39
-
40
- /* ── Wrapper (logged-in state relative anchor) ──────────────── */
41
- .wrapper {
42
- position: relative;
43
- display: inline-block;
44
- }
45
-
46
- /* ── Guest Button ───────────────────────────────────────────── */
47
- /* Sanvika brand identity — always purple, self-contained */
48
- .guestBtn {
49
- display: inline-flex;
50
- align-items: center;
51
- gap: 6px;
52
- padding: clamp(6px, 2vw, 8px) clamp(10px, 3vw, 16px);
53
- min-height: 44px; /* Touch target rule */
54
- min-width: 44px;
55
- background: var(--sanvika-brand-color, #4f46e5);
56
- color: #ffffff;
57
- border: none;
58
- border-radius: 8px;
59
- font-size: clamp(13px, 3vw, 14px);
60
- font-weight: 600;
61
- cursor: pointer;
62
- transition:
63
- background-color 0.3s ease,
64
- color 0.3s ease;
65
- white-space: nowrap;
66
- }
67
-
68
- .guestBtn:hover {
69
- background: var(--sanvika-brand-hover, #4338ca);
70
- }
71
-
72
- .iconWrap {
73
- display: flex;
74
- align-items: center;
75
- flex-shrink: 0;
76
- }
77
-
78
- /* ── Profile Button (logged-in) ─────────────────────────────── */
79
- /* Adapts to host project theme via vars + fallbacks */
80
- .profileBtn {
81
- display: inline-flex;
82
- align-items: center;
83
- gap: 7px;
84
- padding: 5px 10px 5px 5px;
85
- min-height: 44px; /* Touch target rule */
86
- background: var(--muted-bg, #f5f5f5);
87
- border: 1px solid var(--border-color-light, #e5e7eb);
88
- border-radius: 99px;
89
- font-size: clamp(13px, 3vw, 14px);
90
- font-weight: 600;
91
- cursor: pointer;
92
- color: var(--text-color, #1a1a1a);
93
- transition:
94
- background-color 0.3s ease,
95
- border-color 0.3s ease,
96
- color 0.3s ease;
97
- white-space: nowrap;
98
- }
99
-
100
- .profileBtn:hover {
101
- background: var(--hover-color, #ebebeb);
102
- }
103
-
104
- /* ── Avatar (nav button) ────────────────────────────────────── */
105
- .avatar {
106
- width: 28px;
107
- height: 28px;
108
- border-radius: 50%;
109
- object-fit: cover;
110
- border: 2px solid var(--sanvika-brand-color, #4f46e5);
111
- flex-shrink: 0;
112
- }
113
-
114
- /* ── Text spans ─────────────────────────────────────────────── */
115
- .textWrap {
116
- display: inline;
117
- }
118
-
119
- .hideTextOnMobile {
120
- display: none;
121
- }
122
-
123
- @media (min-width: 500px) {
124
- .hideTextOnMobile {
125
- display: inline;
126
- }
127
- }
128
-
129
- /* ── Dropdown ───────────────────────────────────────────────── */
130
- /* Adapts to host project theme */
131
- .dropdown {
132
- position: absolute;
133
- top: calc(100% + 8px);
134
- right: 0;
135
- min-width: clamp(200px, 60vw, 240px);
136
- background: var(--card-bg, #ffffff);
137
- border: 1px solid var(--border-color-light, #e5e7eb);
138
- border-radius: 12px;
139
- box-shadow: 0 8px 24px var(--shadow-color, rgba(0, 0, 0, 0.12));
140
- z-index: 9999;
141
- overflow: hidden;
142
- transition:
143
- background-color 0.3s ease,
144
- border-color 0.3s ease;
145
- }
146
-
147
- .dropdownHeader {
148
- display: flex;
149
- align-items: center;
150
- gap: 10px;
151
- padding: 14px 16px;
152
- background: var(--section-bg, #fafafa);
153
- transition: background-color 0.3s ease;
154
- }
155
-
156
- .dropdownAvatar {
157
- width: 38px;
158
- height: 38px;
159
- border-radius: 50%;
160
- object-fit: cover;
161
- border: 2px solid var(--sanvika-brand-color, #4f46e5);
162
- flex-shrink: 0;
163
- }
164
-
165
- .dropdownName {
166
- font-size: clamp(13px, 3vw, 14px);
167
- font-weight: 700;
168
- color: var(--text-color, #1a1a1a);
169
- line-height: 1.3;
170
- transition: color 0.3s ease;
171
- }
172
-
173
- .dropdownMobile {
174
- font-size: clamp(11px, 2.5vw, 12px);
175
- color: var(--secondary-text-color, #888888);
176
- margin-top: 2px;
177
- transition: color 0.3s ease;
178
- }
179
-
180
- .divider {
181
- height: 1px;
182
- background: var(--muted-border, #f0f0f0);
183
- transition: background-color 0.3s ease;
184
- }
185
-
186
- /* ── Dropdown Items ─────────────────────────────────────────── */
187
- .dropdownItem {
188
- display: flex;
189
- align-items: center;
190
- gap: 10px;
191
- width: 100%;
192
- padding: 11px 16px;
193
- min-height: 44px; /* Touch target rule */
194
- background: none;
195
- border: none;
196
- font-size: clamp(13px, 3vw, 14px);
197
- color: var(--text-color, #333333);
198
- cursor: pointer;
199
- text-decoration: none;
200
- text-align: left;
201
- transition:
202
- background-color 0.3s ease,
203
- color 0.3s ease;
204
- }
205
-
206
- .dropdownItem:hover {
207
- background: var(--hover-color, #f7f7f7);
208
- }
209
-
210
- .logoutItem {
211
- color: var(--error-color, #c0392b);
212
- }
213
-
214
- .logoutItem:hover {
215
- background: var(--error-bg, #fff5f5);
216
- }
217
-
218
- /* ── Shimmer keyframe ───────────────────────────────────────── */
219
- @keyframes shimmer {
220
- 0% {
221
- background-position: 200% 0;
222
- }
223
- 100% {
224
- background-position: -200% 0;
225
- }
226
- }
227
-
228
- /* ── Loading Skeleton ───────────────────────────────────────── */
229
- .skeleton {
230
- width: clamp(72px, 20vw, 96px);
231
- height: clamp(34px, 8vw, 40px);
232
- border-radius: 8px;
233
- background: linear-gradient(
234
- 90deg,
235
- var(--skeleton-base-color) 25%,
236
- var(--skeleton-highlight-color) 50%,
237
- var(--skeleton-base-color) 75%
238
- );
239
- background-size: 200% 100%;
240
- animation: shimmer 1.4s infinite;
241
- display: inline-block;
242
- }
243
-
244
- /* ── Wrapper (logged-in state relative anchor) ──────────────── */
245
- .wrapper {
246
- position: relative;
247
- display: inline-block;
248
- }
249
-
250
- /* ── Guest Button ───────────────────────────────────────────── */
251
- .guestBtn {
252
- display: inline-flex;
253
- align-items: center;
254
- gap: 6px;
255
- padding: clamp(6px, 2vw, 8px) clamp(10px, 3vw, 16px);
256
- min-height: 44px; /* Touch target rule */
257
- min-width: 44px;
258
- background: var(--sanvika-brand-color);
259
- color: var(--btn-primary-text);
260
- border: none;
261
- border-radius: 8px;
262
- font-size: clamp(13px, 3vw, 14px);
263
- font-weight: 600;
264
- cursor: pointer;
265
- transition:
266
- background-color 0.3s ease,
267
- color 0.3s ease;
268
- white-space: nowrap;
269
- }
270
-
271
- .guestBtn:hover {
272
- background: var(--sanvika-brand-hover);
273
- }
274
-
275
- .iconWrap {
276
- display: flex;
277
- align-items: center;
278
- flex-shrink: 0;
279
- }
280
-
281
- /* ── Profile Button (logged-in) ─────────────────────────────── */
282
- .profileBtn {
283
- display: inline-flex;
284
- align-items: center;
285
- gap: 7px;
286
- padding: 5px 10px 5px 5px;
287
- min-height: 44px; /* Touch target rule */
288
- background: var(--muted-bg);
289
- border: 1px solid var(--border-color-light);
290
- border-radius: 99px;
291
- font-size: clamp(13px, 3vw, 14px);
292
- font-weight: 600;
293
- cursor: pointer;
294
- color: var(--text-color);
295
- transition:
296
- background-color 0.3s ease,
297
- border-color 0.3s ease,
298
- color 0.3s ease;
299
- white-space: nowrap;
300
- }
301
-
302
- .profileBtn:hover {
303
- background: var(--hover-color);
304
- }
305
-
306
- /* ── Avatar (nav button) ────────────────────────────────────── */
307
- .avatar {
308
- width: 28px;
309
- height: 28px;
310
- border-radius: 50%;
311
- object-fit: cover;
312
- border: 2px solid var(--sanvika-brand-color);
313
- flex-shrink: 0;
314
- }
315
-
316
- /* ── Text spans ─────────────────────────────────────────────── */
317
- .textWrap {
318
- display: inline;
319
- }
320
-
321
- .hideTextOnMobile {
322
- display: none;
323
- }
324
-
325
- /* clamp-based show — 500px+ par text dikhao */
326
- @media (min-width: 500px) {
327
- .hideTextOnMobile {
328
- display: inline;
329
- }
330
- }
331
-
332
- /* ── Dropdown ───────────────────────────────────────────────── */
333
- .dropdown {
334
- position: absolute;
335
- top: calc(100% + 8px);
336
- right: 0;
337
- min-width: clamp(200px, 60vw, 240px);
338
- background: var(--card-bg);
339
- border: 1px solid var(--border-color-light);
340
- border-radius: 12px;
341
- box-shadow: 0 8px 24px var(--shadow-color);
342
- z-index: 9999;
343
- overflow: hidden;
344
- transition:
345
- background-color 0.3s ease,
346
- border-color 0.3s ease;
347
- }
348
-
349
- .dropdownHeader {
350
- display: flex;
351
- align-items: center;
352
- gap: 10px;
353
- padding: 14px 16px;
354
- background: var(--section-bg);
355
- transition: background-color 0.3s ease;
356
- }
357
-
358
- .dropdownAvatar {
359
- width: 38px;
360
- height: 38px;
361
- border-radius: 50%;
362
- object-fit: cover;
363
- border: 2px solid var(--sanvika-brand-color);
364
- flex-shrink: 0;
365
- }
366
-
367
- .dropdownName {
368
- font-size: clamp(13px, 3vw, 14px);
369
- font-weight: 700;
370
- color: var(--text-color);
371
- line-height: 1.3;
372
- transition: color 0.3s ease;
373
- }
374
-
375
- .dropdownMobile {
376
- font-size: clamp(11px, 2.5vw, 12px);
377
- color: var(--secondary-text-color);
378
- margin-top: 2px;
379
- transition: color 0.3s ease;
380
- }
381
-
382
- .divider {
383
- height: 1px;
384
- background: var(--muted-border);
385
- transition: background-color 0.3s ease;
386
- }
387
-
388
- /* ── Dropdown Items ─────────────────────────────────────────── */
389
- .dropdownItem {
390
- display: flex;
391
- align-items: center;
392
- gap: 10px;
393
- width: 100%;
394
- padding: 11px 16px;
395
- min-height: 44px; /* Touch target rule */
396
- background: none;
397
- border: none;
398
- font-size: clamp(13px, 3vw, 14px);
399
- color: var(--text-color);
400
- cursor: pointer;
401
- text-decoration: none;
402
- text-align: left;
403
- transition:
404
- background-color 0.3s ease,
405
- color 0.3s ease;
406
- }
407
-
408
- .dropdownItem:hover {
409
- background: var(--hover-color);
410
- }
411
-
412
- .logoutItem {
413
- color: var(--error-color);
414
- }
415
-
416
- .logoutItem:hover {
417
- background: var(--error-bg);
418
- }