@instroc/auth 1.0.1 → 1.1.0

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.
@@ -0,0 +1,935 @@
1
+ // src/forms/use-login-form.ts
2
+ import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
3
+
4
+ // src/provider.tsx
5
+ import {
6
+ createContext,
7
+ useContext,
8
+ useState,
9
+ useCallback,
10
+ useEffect,
11
+ useRef
12
+ } from "react";
13
+
14
+ // src/errors.ts
15
+ var AuthError = class _AuthError extends Error {
16
+ constructor(message, status, code) {
17
+ super(message);
18
+ this.name = "AuthError";
19
+ this.status = status;
20
+ this.code = code;
21
+ Object.setPrototypeOf(this, _AuthError.prototype);
22
+ }
23
+ };
24
+ function authErrorFromResponse(response, data, fallbackMessage) {
25
+ const body = data ?? {};
26
+ const message = body.error || fallbackMessage;
27
+ return new AuthError(message, response.status, body.code);
28
+ }
29
+
30
+ // src/provider.tsx
31
+ import { jsx } from "react/jsx-runtime";
32
+ var AuthContext = createContext(null);
33
+ var STORAGE_KEY = "instroc_auth_session";
34
+ function AuthProvider({
35
+ children,
36
+ projectId: initialProjectId,
37
+ baseUrl = "/api/baas",
38
+ persistSession = true,
39
+ onAuthStateChange
40
+ }) {
41
+ const [projectId, setProjectId] = useState(
42
+ initialProjectId || null
43
+ );
44
+ const [user, setUser] = useState(null);
45
+ const [session, setSession] = useState(null);
46
+ const [loading, setLoading] = useState(true);
47
+ const [error, setError] = useState(null);
48
+ const [authConfig, setAuthConfig] = useState(null);
49
+ const [visibilityConfig, setVisibilityConfig] = useState(null);
50
+ const refreshTimeoutRef = useRef(null);
51
+ const buildUrl = useCallback(
52
+ (endpoint) => {
53
+ if (!projectId) {
54
+ throw new Error("Project ID is required");
55
+ }
56
+ return `${baseUrl}/${projectId}/auth/${endpoint}`;
57
+ },
58
+ [projectId, baseUrl]
59
+ );
60
+ const authFetch = useCallback(
61
+ (url, init) => fetch(url, { ...init, credentials: "include" }),
62
+ []
63
+ );
64
+ const saveSession = useCallback(
65
+ (sessionData) => {
66
+ if (persistSession && typeof window !== "undefined") {
67
+ if (sessionData) {
68
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(sessionData));
69
+ } else {
70
+ localStorage.removeItem(STORAGE_KEY);
71
+ }
72
+ }
73
+ },
74
+ [persistSession]
75
+ );
76
+ const loadSession = useCallback(() => {
77
+ if (persistSession && typeof window !== "undefined") {
78
+ const stored = localStorage.getItem(STORAGE_KEY);
79
+ if (stored) {
80
+ try {
81
+ return JSON.parse(stored);
82
+ } catch {
83
+ localStorage.removeItem(STORAGE_KEY);
84
+ }
85
+ }
86
+ }
87
+ return null;
88
+ }, [persistSession]);
89
+ const scheduleRefresh = useCallback(
90
+ (expiresAt) => {
91
+ if (refreshTimeoutRef.current) {
92
+ clearTimeout(refreshTimeoutRef.current);
93
+ }
94
+ const expiresTime = new Date(expiresAt).getTime();
95
+ const now = Date.now();
96
+ const refreshIn = expiresTime - now - 5 * 60 * 1e3;
97
+ if (refreshIn > 0) {
98
+ refreshTimeoutRef.current = setTimeout(async () => {
99
+ try {
100
+ await refreshSession();
101
+ } catch (err) {
102
+ console.warn("Token refresh failed:", err);
103
+ setUser(null);
104
+ setSession(null);
105
+ saveSession(null);
106
+ }
107
+ }, refreshIn);
108
+ }
109
+ },
110
+ [saveSession]
111
+ );
112
+ const updateAuthState = useCallback(
113
+ (newUser, newSession) => {
114
+ setUser(newUser);
115
+ setSession(newSession);
116
+ saveSession(newSession);
117
+ if (newSession) {
118
+ scheduleRefresh(newSession.expires_at);
119
+ }
120
+ if (onAuthStateChange) {
121
+ onAuthStateChange(newUser);
122
+ }
123
+ },
124
+ [saveSession, scheduleRefresh, onAuthStateChange]
125
+ );
126
+ const fetchUser = useCallback(async () => {
127
+ const storedSession = loadSession();
128
+ if (!storedSession || !projectId) {
129
+ setLoading(false);
130
+ return;
131
+ }
132
+ try {
133
+ const response = await authFetch(buildUrl("me"), {
134
+ headers: {
135
+ Authorization: `Bearer ${storedSession.access_token}`
136
+ }
137
+ });
138
+ if (response.ok) {
139
+ const data = await response.json();
140
+ updateAuthState(data.user, storedSession);
141
+ } else if (response.status === 401) {
142
+ try {
143
+ const refreshResponse = await authFetch(buildUrl("refresh"), {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ refreshToken: storedSession.refresh_token })
147
+ });
148
+ if (refreshResponse.ok) {
149
+ const refreshData = await refreshResponse.json();
150
+ const newSession = {
151
+ ...storedSession,
152
+ access_token: refreshData.access_token,
153
+ expires_at: refreshData.expires_at
154
+ };
155
+ const userResponse = await authFetch(buildUrl("me"), {
156
+ headers: {
157
+ Authorization: `Bearer ${newSession.access_token}`
158
+ }
159
+ });
160
+ if (userResponse.ok) {
161
+ const userData = await userResponse.json();
162
+ updateAuthState(userData.user, newSession);
163
+ } else {
164
+ updateAuthState(null, null);
165
+ }
166
+ } else {
167
+ updateAuthState(null, null);
168
+ }
169
+ } catch {
170
+ updateAuthState(null, null);
171
+ }
172
+ } else {
173
+ updateAuthState(null, null);
174
+ }
175
+ } catch (err) {
176
+ console.error("Failed to fetch user:", err);
177
+ updateAuthState(null, null);
178
+ } finally {
179
+ setLoading(false);
180
+ }
181
+ }, [projectId, loadSession, buildUrl, updateAuthState]);
182
+ useEffect(() => {
183
+ if (projectId) {
184
+ fetchUser();
185
+ } else {
186
+ setLoading(false);
187
+ }
188
+ return () => {
189
+ if (refreshTimeoutRef.current) {
190
+ clearTimeout(refreshTimeoutRef.current);
191
+ }
192
+ };
193
+ }, [projectId, fetchUser]);
194
+ const login = useCallback(
195
+ async (credentials) => {
196
+ setError(null);
197
+ setLoading(true);
198
+ try {
199
+ const response = await authFetch(buildUrl("login"), {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json" },
202
+ body: JSON.stringify(credentials)
203
+ });
204
+ const data = await response.json();
205
+ if (!response.ok) {
206
+ throw authErrorFromResponse(response, data, "Login failed");
207
+ }
208
+ const authResponse = data;
209
+ updateAuthState(authResponse.user, authResponse.session);
210
+ } catch (err) {
211
+ const message = err instanceof Error ? err.message : "Login failed";
212
+ setError(message);
213
+ throw err;
214
+ } finally {
215
+ setLoading(false);
216
+ }
217
+ },
218
+ [buildUrl, updateAuthState]
219
+ );
220
+ const signup = useCallback(
221
+ async (credentials) => {
222
+ setError(null);
223
+ setLoading(true);
224
+ try {
225
+ const response = await authFetch(buildUrl("signup"), {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/json" },
228
+ body: JSON.stringify(credentials)
229
+ });
230
+ const data = await response.json();
231
+ if (!response.ok) {
232
+ throw authErrorFromResponse(response, data, "Signup failed");
233
+ }
234
+ const authResponse = data;
235
+ if (authResponse.needsVerification) {
236
+ return {
237
+ status: "needs_verification",
238
+ email: credentials.email
239
+ };
240
+ }
241
+ if (authResponse.needsApproval) {
242
+ updateAuthState(authResponse.user, authResponse.session);
243
+ return { status: "needs_approval", user: authResponse.user };
244
+ }
245
+ if (authResponse.session.access_token) {
246
+ updateAuthState(authResponse.user, authResponse.session);
247
+ return { status: "authenticated", user: authResponse.user };
248
+ }
249
+ return {
250
+ status: "needs_verification",
251
+ email: credentials.email
252
+ };
253
+ } catch (err) {
254
+ const message = err instanceof Error ? err.message : "Signup failed";
255
+ setError(message);
256
+ throw err;
257
+ } finally {
258
+ setLoading(false);
259
+ }
260
+ },
261
+ [buildUrl, updateAuthState]
262
+ );
263
+ const logout = useCallback(async () => {
264
+ try {
265
+ if (session) {
266
+ await authFetch(buildUrl("logout"), {
267
+ method: "POST",
268
+ headers: {
269
+ Authorization: `Bearer ${session.access_token}`
270
+ }
271
+ });
272
+ }
273
+ } catch (err) {
274
+ console.error("Logout error:", err);
275
+ } finally {
276
+ updateAuthState(null, null);
277
+ if (refreshTimeoutRef.current) {
278
+ clearTimeout(refreshTimeoutRef.current);
279
+ }
280
+ }
281
+ }, [session, buildUrl, updateAuthState]);
282
+ const updateProfile = useCallback(
283
+ async (data) => {
284
+ if (!session) {
285
+ throw new AuthError("Not authenticated", 401);
286
+ }
287
+ setError(null);
288
+ try {
289
+ const response = await authFetch(buildUrl("me"), {
290
+ method: "PATCH",
291
+ headers: {
292
+ "Content-Type": "application/json",
293
+ Authorization: `Bearer ${session.access_token}`
294
+ },
295
+ body: JSON.stringify(data)
296
+ });
297
+ const result = await response.json();
298
+ if (!response.ok) {
299
+ throw authErrorFromResponse(response, result, "Update failed");
300
+ }
301
+ setUser(result.user);
302
+ } catch (err) {
303
+ const message = err instanceof Error ? err.message : "Update failed";
304
+ setError(message);
305
+ throw err;
306
+ }
307
+ },
308
+ [session, buildUrl]
309
+ );
310
+ const refreshSession = useCallback(async () => {
311
+ if (!session)
312
+ return;
313
+ const response = await authFetch(buildUrl("refresh"), {
314
+ method: "POST",
315
+ headers: { "Content-Type": "application/json" },
316
+ body: JSON.stringify({ refreshToken: session.refresh_token })
317
+ });
318
+ const data = await response.json();
319
+ if (!response.ok) {
320
+ throw authErrorFromResponse(response, data, "Refresh failed");
321
+ }
322
+ const refreshData = data;
323
+ const newSession = {
324
+ ...session,
325
+ access_token: refreshData.access_token,
326
+ expires_at: refreshData.expires_at
327
+ };
328
+ updateAuthState(user, newSession);
329
+ }, [session, user, buildUrl, updateAuthState]);
330
+ const forgotPassword = useCallback(
331
+ async (email) => {
332
+ setError(null);
333
+ try {
334
+ const response = await authFetch(buildUrl("forgot-password"), {
335
+ method: "POST",
336
+ headers: { "Content-Type": "application/json" },
337
+ body: JSON.stringify({ email })
338
+ });
339
+ const data = await response.json();
340
+ if (!response.ok) {
341
+ throw authErrorFromResponse(response, data, "Request failed");
342
+ }
343
+ } catch (err) {
344
+ const message = err instanceof Error ? err.message : "Request failed";
345
+ setError(message);
346
+ throw err;
347
+ }
348
+ },
349
+ [buildUrl]
350
+ );
351
+ const resetPassword = useCallback(
352
+ async (token, newPassword) => {
353
+ setError(null);
354
+ try {
355
+ const response = await authFetch(buildUrl("reset-password"), {
356
+ method: "POST",
357
+ headers: { "Content-Type": "application/json" },
358
+ body: JSON.stringify({ token, password: newPassword })
359
+ });
360
+ const data = await response.json();
361
+ if (!response.ok) {
362
+ throw authErrorFromResponse(response, data, "Reset failed");
363
+ }
364
+ } catch (err) {
365
+ const message = err instanceof Error ? err.message : "Reset failed";
366
+ setError(message);
367
+ throw err;
368
+ }
369
+ },
370
+ [buildUrl]
371
+ );
372
+ const signInWithOAuth = useCallback(
373
+ (provider) => {
374
+ if (!projectId) {
375
+ setError("Project ID is required");
376
+ return;
377
+ }
378
+ const currentUrl = window.location.origin + window.location.pathname;
379
+ const oauthUrl = `${baseUrl}/${projectId}/auth/oauth/${provider}?redirect_uri=${encodeURIComponent(currentUrl)}`;
380
+ window.location.href = oauthUrl;
381
+ },
382
+ [projectId, baseUrl]
383
+ );
384
+ const verifyOTP = useCallback(
385
+ async (email, code) => {
386
+ setError(null);
387
+ setLoading(true);
388
+ try {
389
+ const response = await authFetch(buildUrl("verify-email"), {
390
+ method: "POST",
391
+ headers: { "Content-Type": "application/json" },
392
+ body: JSON.stringify({ email, otp: code })
393
+ });
394
+ const data = await response.json();
395
+ if (!response.ok) {
396
+ throw authErrorFromResponse(response, data, "Verification failed");
397
+ }
398
+ if (data.session) {
399
+ updateAuthState(data.user, data.session);
400
+ }
401
+ } catch (err) {
402
+ const message = err instanceof Error ? err.message : "Verification failed";
403
+ setError(message);
404
+ throw err;
405
+ } finally {
406
+ setLoading(false);
407
+ }
408
+ },
409
+ [buildUrl, updateAuthState]
410
+ );
411
+ const resendOTP = useCallback(
412
+ async (email) => {
413
+ setError(null);
414
+ try {
415
+ const response = await authFetch(buildUrl("resend-verification"), {
416
+ method: "POST",
417
+ headers: { "Content-Type": "application/json" },
418
+ body: JSON.stringify({ email })
419
+ });
420
+ const data = await response.json();
421
+ if (!response.ok) {
422
+ throw authErrorFromResponse(response, data, "Failed to resend code");
423
+ }
424
+ } catch (err) {
425
+ const message = err instanceof Error ? err.message : "Failed to resend code";
426
+ setError(message);
427
+ throw err;
428
+ }
429
+ },
430
+ [buildUrl]
431
+ );
432
+ const fetchAuthConfig = useCallback(async () => {
433
+ if (!projectId)
434
+ return;
435
+ try {
436
+ const response = await fetch(`${baseUrl}/${projectId}/auth/config`);
437
+ if (response.ok) {
438
+ const data = await response.json();
439
+ setAuthConfig(data.config || null);
440
+ setVisibilityConfig(
441
+ data.visibility || {
442
+ projectName: "App",
443
+ visibility: "public",
444
+ requireLogin: false,
445
+ logoUrl: null,
446
+ welcomeMessage: null
447
+ }
448
+ );
449
+ }
450
+ } catch (err) {
451
+ console.error("Failed to fetch auth config:", err);
452
+ }
453
+ }, [projectId, baseUrl]);
454
+ const parseOAuthCallback = useCallback(() => {
455
+ if (typeof window === "undefined")
456
+ return;
457
+ const hash = window.location.hash;
458
+ if (hash && hash.startsWith("#auth=")) {
459
+ try {
460
+ const authData = decodeURIComponent(hash.substring(6));
461
+ const tokenData = JSON.parse(authData);
462
+ if (tokenData.access_token && tokenData.refresh_token) {
463
+ const newSession = {
464
+ access_token: tokenData.access_token,
465
+ refresh_token: tokenData.refresh_token,
466
+ expires_at: tokenData.expires_at
467
+ };
468
+ saveSession(newSession);
469
+ setSession(newSession);
470
+ window.history.replaceState({}, "", window.location.pathname);
471
+ return true;
472
+ }
473
+ } catch {
474
+ }
475
+ }
476
+ const params = new URLSearchParams(window.location.search);
477
+ const verifyEmailToken = params.get("verify_email");
478
+ if (verifyEmailToken && projectId) {
479
+ fetch(buildUrl("verify-email"), {
480
+ method: "POST",
481
+ headers: { "Content-Type": "application/json" },
482
+ body: JSON.stringify({ token: verifyEmailToken })
483
+ }).then((res) => res.json()).then(() => {
484
+ window.history.replaceState({}, "", window.location.pathname);
485
+ }).catch(() => {
486
+ window.history.replaceState({}, "", window.location.pathname);
487
+ });
488
+ return true;
489
+ }
490
+ const accessToken = params.get("access_token");
491
+ const refreshToken = params.get("refresh_token");
492
+ const expiresAt = params.get("expires_at");
493
+ if (accessToken && refreshToken && expiresAt) {
494
+ const newSession = {
495
+ access_token: accessToken,
496
+ refresh_token: refreshToken,
497
+ expires_at: expiresAt
498
+ };
499
+ saveSession(newSession);
500
+ setSession(newSession);
501
+ window.history.replaceState({}, "", window.location.pathname);
502
+ return true;
503
+ }
504
+ return false;
505
+ }, [projectId, buildUrl, saveSession]);
506
+ useEffect(() => {
507
+ fetchAuthConfig();
508
+ }, [fetchAuthConfig]);
509
+ useEffect(() => {
510
+ parseOAuthCallback();
511
+ }, [parseOAuthCallback]);
512
+ const value = {
513
+ user,
514
+ session,
515
+ loading,
516
+ error,
517
+ login,
518
+ signup,
519
+ logout,
520
+ signInWithOAuth,
521
+ verifyOTP,
522
+ resendOTP,
523
+ updateProfile,
524
+ refreshSession,
525
+ forgotPassword,
526
+ resetPassword,
527
+ setProjectId,
528
+ projectId,
529
+ authConfig,
530
+ visibilityConfig
531
+ };
532
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
533
+ }
534
+ function useAuthContext() {
535
+ const context = useContext(AuthContext);
536
+ if (!context) {
537
+ throw new Error("useAuthContext must be used within an AuthProvider");
538
+ }
539
+ return context;
540
+ }
541
+
542
+ // src/use-auth.ts
543
+ function useAuth() {
544
+ return useAuthContext();
545
+ }
546
+ function useUser() {
547
+ const { user, loading } = useAuthContext();
548
+ return { user, loading };
549
+ }
550
+ function useSession() {
551
+ const { session, loading } = useAuthContext();
552
+ return { session, loading };
553
+ }
554
+ function useAuthRequired() {
555
+ const { user, loading, session } = useAuthContext();
556
+ if (loading) {
557
+ return { user: null, session: null, loading: true };
558
+ }
559
+ if (!user) {
560
+ throw new Error("Authentication required");
561
+ }
562
+ return { user, session, loading: false };
563
+ }
564
+
565
+ // src/forms/use-form-action.ts
566
+ function resolveErrorMessage(err, messages, fallback) {
567
+ if (err instanceof AuthError) {
568
+ return messages?.[err.status] ?? err.message ?? fallback;
569
+ }
570
+ if (err instanceof Error)
571
+ return err.message;
572
+ return fallback;
573
+ }
574
+ function applyErrorResult(err, onError, messages, fallback, setError) {
575
+ const override = onError?.(err);
576
+ if (override === null) {
577
+ setError(null);
578
+ return;
579
+ }
580
+ if (typeof override === "string") {
581
+ setError(override);
582
+ return;
583
+ }
584
+ setError(resolveErrorMessage(err, messages, fallback));
585
+ }
586
+
587
+ // src/forms/use-login-form.ts
588
+ function useLoginForm(options = {}) {
589
+ const { login, user } = useAuth();
590
+ const { onSuccess, onNeedsVerification, onError, messages } = options;
591
+ const [loading, setLoading] = useState2(false);
592
+ const [error, setError] = useState2(null);
593
+ const mountedRef = useRef2(true);
594
+ useEffect2(() => {
595
+ mountedRef.current = true;
596
+ return () => {
597
+ mountedRef.current = false;
598
+ };
599
+ }, []);
600
+ const safeSetError = useCallback2((msg) => {
601
+ if (mountedRef.current)
602
+ setError(msg);
603
+ }, []);
604
+ const clearError = useCallback2(() => safeSetError(null), [safeSetError]);
605
+ const submit = useCallback2(
606
+ async (credentials) => {
607
+ if (mountedRef.current) {
608
+ setLoading(true);
609
+ setError(null);
610
+ }
611
+ try {
612
+ await login(credentials);
613
+ onSuccess?.(user ?? { id: "", email: credentials.email });
614
+ } catch (err) {
615
+ if (err instanceof AuthError && err.status === 403 && onNeedsVerification) {
616
+ safeSetError(null);
617
+ onNeedsVerification(credentials.email);
618
+ return;
619
+ }
620
+ applyErrorResult(
621
+ err,
622
+ onError,
623
+ messages,
624
+ "Login failed. Please try again.",
625
+ safeSetError
626
+ );
627
+ } finally {
628
+ if (mountedRef.current)
629
+ setLoading(false);
630
+ }
631
+ },
632
+ [
633
+ login,
634
+ user,
635
+ onSuccess,
636
+ onNeedsVerification,
637
+ onError,
638
+ messages,
639
+ safeSetError
640
+ ]
641
+ );
642
+ return { submit, loading, error, setError: safeSetError, clearError };
643
+ }
644
+
645
+ // src/forms/use-signup-form.ts
646
+ import { useState as useState3, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect3 } from "react";
647
+ function useSignupForm(options = {}) {
648
+ const { signup } = useAuth();
649
+ const { onSuccess, onNeedsVerification, onNeedsApproval, onError, messages } = options;
650
+ const [loading, setLoading] = useState3(false);
651
+ const [error, setError] = useState3(null);
652
+ const mountedRef = useRef3(true);
653
+ useEffect3(() => {
654
+ mountedRef.current = true;
655
+ return () => {
656
+ mountedRef.current = false;
657
+ };
658
+ }, []);
659
+ const safeSetError = useCallback3((msg) => {
660
+ if (mountedRef.current)
661
+ setError(msg);
662
+ }, []);
663
+ const clearError = useCallback3(() => safeSetError(null), [safeSetError]);
664
+ const submit = useCallback3(
665
+ async (credentials) => {
666
+ if (mountedRef.current) {
667
+ setLoading(true);
668
+ setError(null);
669
+ }
670
+ try {
671
+ const result = await signup(credentials);
672
+ switch (result.status) {
673
+ case "authenticated":
674
+ onSuccess?.(result.user);
675
+ return;
676
+ case "needs_verification":
677
+ onNeedsVerification?.(result.email);
678
+ return;
679
+ case "needs_approval":
680
+ onNeedsApproval?.(result.user);
681
+ return;
682
+ }
683
+ } catch (err) {
684
+ applyErrorResult(
685
+ err,
686
+ onError,
687
+ messages,
688
+ "Signup failed. Please try again.",
689
+ safeSetError
690
+ );
691
+ } finally {
692
+ if (mountedRef.current)
693
+ setLoading(false);
694
+ }
695
+ },
696
+ [
697
+ signup,
698
+ onSuccess,
699
+ onNeedsVerification,
700
+ onNeedsApproval,
701
+ onError,
702
+ messages,
703
+ safeSetError
704
+ ]
705
+ );
706
+ return { submit, loading, error, setError: safeSetError, clearError };
707
+ }
708
+
709
+ // src/forms/use-otp-form.ts
710
+ import { useState as useState4, useCallback as useCallback4, useRef as useRef4, useEffect as useEffect4 } from "react";
711
+ function useOtpForm(options) {
712
+ const { verifyOTP, resendOTP } = useAuth();
713
+ const {
714
+ email,
715
+ onSuccess,
716
+ onResendSuccess,
717
+ onError,
718
+ messages,
719
+ resendCooldownSeconds = 60
720
+ } = options;
721
+ const [loading, setLoading] = useState4(false);
722
+ const [resending, setResending] = useState4(false);
723
+ const [resendCooldown, setResendCooldown] = useState4(0);
724
+ const [error, setError] = useState4(null);
725
+ const mountedRef = useRef4(true);
726
+ useEffect4(() => {
727
+ mountedRef.current = true;
728
+ return () => {
729
+ mountedRef.current = false;
730
+ };
731
+ }, []);
732
+ useEffect4(() => {
733
+ if (resendCooldown <= 0)
734
+ return;
735
+ const id = setTimeout(() => setResendCooldown((s) => s - 1), 1e3);
736
+ return () => clearTimeout(id);
737
+ }, [resendCooldown]);
738
+ const safeSetError = useCallback4((msg) => {
739
+ if (mountedRef.current)
740
+ setError(msg);
741
+ }, []);
742
+ const clearError = useCallback4(() => safeSetError(null), [safeSetError]);
743
+ const submit = useCallback4(
744
+ async (code) => {
745
+ if (mountedRef.current) {
746
+ setLoading(true);
747
+ setError(null);
748
+ }
749
+ try {
750
+ await verifyOTP(email, code);
751
+ onSuccess?.();
752
+ } catch (err) {
753
+ applyErrorResult(
754
+ err,
755
+ onError,
756
+ messages,
757
+ "Verification failed. Please try again.",
758
+ safeSetError
759
+ );
760
+ } finally {
761
+ if (mountedRef.current)
762
+ setLoading(false);
763
+ }
764
+ },
765
+ [verifyOTP, email, onSuccess, onError, messages, safeSetError]
766
+ );
767
+ const resend = useCallback4(async () => {
768
+ if (resendCooldown > 0)
769
+ return;
770
+ if (mountedRef.current) {
771
+ setResending(true);
772
+ setError(null);
773
+ }
774
+ try {
775
+ await resendOTP(email);
776
+ if (mountedRef.current)
777
+ setResendCooldown(resendCooldownSeconds);
778
+ onResendSuccess?.();
779
+ } catch (err) {
780
+ applyErrorResult(
781
+ err,
782
+ onError,
783
+ messages,
784
+ "Could not resend code. Please try again.",
785
+ safeSetError
786
+ );
787
+ } finally {
788
+ if (mountedRef.current)
789
+ setResending(false);
790
+ }
791
+ }, [
792
+ resendCooldown,
793
+ resendOTP,
794
+ email,
795
+ resendCooldownSeconds,
796
+ onResendSuccess,
797
+ onError,
798
+ messages,
799
+ safeSetError
800
+ ]);
801
+ return {
802
+ submit,
803
+ resend,
804
+ loading,
805
+ resending,
806
+ resendCooldown,
807
+ error,
808
+ setError: safeSetError,
809
+ clearError
810
+ };
811
+ }
812
+
813
+ // src/forms/use-forgot-password-form.ts
814
+ import { useState as useState5, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect5 } from "react";
815
+ function useForgotPasswordForm(options = {}) {
816
+ const { forgotPassword } = useAuth();
817
+ const { onSuccess, onError, messages } = options;
818
+ const [loading, setLoading] = useState5(false);
819
+ const [submitted, setSubmitted] = useState5(false);
820
+ const [error, setError] = useState5(null);
821
+ const mountedRef = useRef5(true);
822
+ useEffect5(() => {
823
+ mountedRef.current = true;
824
+ return () => {
825
+ mountedRef.current = false;
826
+ };
827
+ }, []);
828
+ const safeSetError = useCallback5((msg) => {
829
+ if (mountedRef.current)
830
+ setError(msg);
831
+ }, []);
832
+ const clearError = useCallback5(() => safeSetError(null), [safeSetError]);
833
+ const reset = useCallback5(() => {
834
+ if (mountedRef.current) {
835
+ setSubmitted(false);
836
+ setError(null);
837
+ }
838
+ }, []);
839
+ const submit = useCallback5(
840
+ async (email) => {
841
+ if (mountedRef.current) {
842
+ setLoading(true);
843
+ setError(null);
844
+ }
845
+ try {
846
+ await forgotPassword(email);
847
+ if (mountedRef.current)
848
+ setSubmitted(true);
849
+ onSuccess?.(email);
850
+ } catch (err) {
851
+ applyErrorResult(
852
+ err,
853
+ onError,
854
+ messages,
855
+ "Could not send reset link. Please try again.",
856
+ safeSetError
857
+ );
858
+ } finally {
859
+ if (mountedRef.current)
860
+ setLoading(false);
861
+ }
862
+ },
863
+ [forgotPassword, onSuccess, onError, messages, safeSetError]
864
+ );
865
+ return {
866
+ submit,
867
+ loading,
868
+ submitted,
869
+ error,
870
+ setError: safeSetError,
871
+ clearError,
872
+ reset
873
+ };
874
+ }
875
+
876
+ // src/forms/use-reset-password-form.ts
877
+ import { useState as useState6, useCallback as useCallback6, useRef as useRef6, useEffect as useEffect6 } from "react";
878
+ function useResetPasswordForm(options) {
879
+ const { resetPassword } = useAuth();
880
+ const { token, onSuccess, onError, messages } = options;
881
+ const [loading, setLoading] = useState6(false);
882
+ const [error, setError] = useState6(null);
883
+ const mountedRef = useRef6(true);
884
+ useEffect6(() => {
885
+ mountedRef.current = true;
886
+ return () => {
887
+ mountedRef.current = false;
888
+ };
889
+ }, []);
890
+ const safeSetError = useCallback6((msg) => {
891
+ if (mountedRef.current)
892
+ setError(msg);
893
+ }, []);
894
+ const clearError = useCallback6(() => safeSetError(null), [safeSetError]);
895
+ const submit = useCallback6(
896
+ async (newPassword) => {
897
+ if (mountedRef.current) {
898
+ setLoading(true);
899
+ setError(null);
900
+ }
901
+ try {
902
+ await resetPassword(token, newPassword);
903
+ onSuccess?.();
904
+ } catch (err) {
905
+ applyErrorResult(
906
+ err,
907
+ onError,
908
+ messages,
909
+ "Could not reset password. Please try again.",
910
+ safeSetError
911
+ );
912
+ } finally {
913
+ if (mountedRef.current)
914
+ setLoading(false);
915
+ }
916
+ },
917
+ [resetPassword, token, onSuccess, onError, messages, safeSetError]
918
+ );
919
+ return { submit, loading, error, setError: safeSetError, clearError };
920
+ }
921
+
922
+ export {
923
+ AuthError,
924
+ AuthProvider,
925
+ useAuthContext,
926
+ useAuth,
927
+ useUser,
928
+ useSession,
929
+ useAuthRequired,
930
+ useLoginForm,
931
+ useSignupForm,
932
+ useOtpForm,
933
+ useForgotPasswordForm,
934
+ useResetPasswordForm
935
+ };