@umituz/react-native-auth 3.6.71 → 3.6.73
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/infrastructure/adapters/StorageProviderAdapter.ts +1 -4
- package/src/infrastructure/providers/FirebaseAuthProvider.ts +5 -32
- package/src/infrastructure/repositories/AuthRepository.ts +0 -5
- package/src/infrastructure/services/AnonymousModeService.ts +6 -20
- package/src/infrastructure/services/AuthEventService.ts +2 -4
- package/src/infrastructure/services/initializeAuth.ts +2 -11
- package/src/infrastructure/utils/AuthErrorMapper.ts +0 -4
- package/src/infrastructure/utils/authStateHandler.ts +2 -4
- package/src/infrastructure/utils/listener/anonymousSignInHandler.ts +2 -22
- package/src/infrastructure/utils/listener/listenerLifecycle.util.ts +144 -0
- package/src/infrastructure/utils/listener/listenerState.util.ts +125 -0
- package/src/init/createAuthInitModule.ts +3 -22
- package/src/presentation/components/AccountActions.tsx +2 -4
- package/src/presentation/components/AuthBottomSheet.tsx +1 -17
- package/src/presentation/hooks/useAccountManagement.ts +0 -7
- package/src/presentation/hooks/useAuth.ts +2 -4
- package/src/presentation/hooks/useAuthBottomSheet.ts +23 -79
- package/src/presentation/hooks/useGoogleAuth.ts +0 -3
- package/src/presentation/hooks/useLoginForm.ts +26 -30
- package/src/presentation/hooks/useProfileEdit.ts +56 -64
- package/src/presentation/hooks/useRegisterForm.ts +41 -44
- package/src/presentation/providers/AuthProvider.tsx +2 -7
- package/src/presentation/stores/authModalStore.ts +0 -7
- package/src/presentation/stores/authStore.ts +1 -28
- package/src/presentation/stores/initializeAuthListener.ts +30 -160
- package/src/presentation/utils/accountDeleteHandler.util.ts +0 -51
- package/src/presentation/utils/authTransition.util.ts +72 -0
- package/src/presentation/utils/form/formFieldState.util.ts +82 -0
- package/src/presentation/utils/form/formValidation.util.ts +173 -0
- package/src/presentation/utils/socialAuthHandler.util.ts +106 -0
|
@@ -4,18 +4,22 @@
|
|
|
4
4
|
* Uses onIdTokenChanged for profile updates (displayName, email)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { onIdTokenChanged } from "firebase/auth";
|
|
8
7
|
import { getFirebaseAuth } from "@umituz/react-native-firebase";
|
|
9
8
|
import { useAuthStore } from "./authStore";
|
|
10
|
-
import { getAuthService } from "../../infrastructure/services/AuthService";
|
|
11
9
|
import type { AuthListenerOptions } from "../../types/auth-store.types";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
import {
|
|
11
|
+
handleExistingInitialization,
|
|
12
|
+
handleInitializationInProgress,
|
|
13
|
+
handleNoFirebaseAuth,
|
|
14
|
+
setupAuthListener,
|
|
15
|
+
completeListenerSetup,
|
|
16
|
+
} from "../../infrastructure/utils/listener/listenerLifecycle.util";
|
|
17
|
+
import {
|
|
18
|
+
isInitializationInProgress,
|
|
19
|
+
isListenerInitialized,
|
|
20
|
+
resetListenerState,
|
|
21
|
+
decrementRefCount,
|
|
22
|
+
} from "../../infrastructure/utils/listener/listenerState.util";
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Initialize Firebase auth listener
|
|
@@ -26,160 +30,33 @@ export function initializeAuthListener(
|
|
|
26
30
|
): () => void {
|
|
27
31
|
const { autoAnonymousSignIn = true, onAuthStateChange } = options;
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
alreadyInitialized: listenerInitialized,
|
|
33
|
-
initializationInProgress,
|
|
34
|
-
});
|
|
33
|
+
// Prevent duplicate initialization
|
|
34
|
+
if (isInitializationInProgress()) {
|
|
35
|
+
return handleInitializationInProgress();
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
console.warn("[AuthListener] Initialization already in progress, returning existing unsubscribe");
|
|
41
|
-
}
|
|
42
|
-
return () => {
|
|
43
|
-
// No-op - will be handled by the initial initialization
|
|
44
|
-
};
|
|
38
|
+
// If already initialized, increment ref count and return unsubscribe
|
|
39
|
+
if (isListenerInitialized()) {
|
|
40
|
+
return handleExistingInitialization()!;
|
|
45
41
|
}
|
|
46
42
|
|
|
47
|
-
// If already initialized, increment ref count and return unsubscribe that decrements
|
|
48
|
-
if (listenerInitialized) {
|
|
49
|
-
listenerRefCount++;
|
|
50
|
-
if (__DEV__) {
|
|
51
|
-
console.log("[AuthListener] Already initialized, incrementing ref count:", listenerRefCount);
|
|
52
|
-
}
|
|
53
|
-
// Return function that decrements ref count
|
|
54
|
-
return () => {
|
|
55
|
-
listenerRefCount--;
|
|
56
|
-
if (__DEV__) {
|
|
57
|
-
console.log("[AuthListener] Ref count decremented:", listenerRefCount);
|
|
58
|
-
}
|
|
59
|
-
// Only cleanup when all subscribers unsubscribe
|
|
60
|
-
if (listenerRefCount <= 0 && firebaseUnsubscribe) {
|
|
61
|
-
if (__DEV__) {
|
|
62
|
-
console.log("[AuthListener] Last subscriber, cleaning up");
|
|
63
|
-
}
|
|
64
|
-
firebaseUnsubscribe();
|
|
65
|
-
firebaseUnsubscribe = null;
|
|
66
|
-
listenerInitialized = false;
|
|
67
|
-
listenerRefCount = 0;
|
|
68
|
-
anonymousSignInInProgress = false;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Mark initialization as in progress
|
|
74
|
-
initializationInProgress = true;
|
|
75
|
-
|
|
76
43
|
const auth = getFirebaseAuth();
|
|
77
44
|
const store = useAuthStore.getState();
|
|
78
45
|
|
|
79
46
|
if (!auth) {
|
|
80
|
-
|
|
81
|
-
console.log("[AuthListener] No Firebase auth, marking initialized");
|
|
82
|
-
}
|
|
83
|
-
initializationInProgress = false;
|
|
84
|
-
store.setLoading(false);
|
|
85
|
-
store.setInitialized(true);
|
|
86
|
-
return () => {};
|
|
47
|
+
return handleNoFirebaseAuth(store);
|
|
87
48
|
}
|
|
88
49
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Initialize listener first, then check anonymous mode
|
|
93
|
-
// This prevents race conditions where the listener fires before we set up state
|
|
94
|
-
const service = getAuthService();
|
|
95
|
-
if (service) {
|
|
96
|
-
const isAnonymous = service.getIsAnonymousMode();
|
|
97
|
-
if (__DEV__) {
|
|
98
|
-
console.log("[AuthListener] Service isAnonymousMode:", isAnonymous);
|
|
99
|
-
}
|
|
100
|
-
// Set anonymous mode flag before setting up listener
|
|
101
|
-
// This ensures consistent state when listener first fires
|
|
102
|
-
if (isAnonymous) {
|
|
103
|
-
store.setIsAnonymous(true);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
firebaseUnsubscribe = onIdTokenChanged(auth, (user) => {
|
|
108
|
-
if (__DEV__) {
|
|
109
|
-
console.log("[AuthListener] onIdTokenChanged:", {
|
|
110
|
-
uid: user?.uid ?? null,
|
|
111
|
-
isAnonymous: user?.isAnonymous ?? null,
|
|
112
|
-
email: user?.email ?? null,
|
|
113
|
-
displayName: user?.displayName ?? null,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!user && autoAnonymousSignIn) {
|
|
118
|
-
// Prevent race condition: only one anonymous sign-in at a time
|
|
119
|
-
if (anonymousSignInInProgress) {
|
|
120
|
-
if (__DEV__) {
|
|
121
|
-
console.log("[AuthListener] Anonymous sign-in already in progress, skipping");
|
|
122
|
-
}
|
|
123
|
-
store.setFirebaseUser(null);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (__DEV__) {
|
|
128
|
-
console.log("[AuthListener] No user, auto signing in anonymously...");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
anonymousSignInInProgress = true;
|
|
132
|
-
|
|
133
|
-
// Create and execute anonymous sign-in handler
|
|
134
|
-
const handleAnonymousSignIn = createAnonymousSignInHandler(auth, store);
|
|
135
|
-
|
|
136
|
-
// Start sign-in without blocking the listener
|
|
137
|
-
void (async () => {
|
|
138
|
-
try {
|
|
139
|
-
await handleAnonymousSignIn();
|
|
140
|
-
} finally {
|
|
141
|
-
anonymousSignInInProgress = false;
|
|
142
|
-
}
|
|
143
|
-
})();
|
|
144
|
-
|
|
145
|
-
// Continue execution - don't return early
|
|
146
|
-
// The listener will be triggered again when sign-in succeeds
|
|
147
|
-
store.setFirebaseUser(null);
|
|
148
|
-
initializationInProgress = false;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
store.setFirebaseUser(user);
|
|
153
|
-
store.setInitialized(true);
|
|
154
|
-
|
|
155
|
-
if (user && !user.isAnonymous && store.isAnonymous) {
|
|
156
|
-
if (__DEV__) {
|
|
157
|
-
console.log("[AuthListener] User converted from anonymous, updating");
|
|
158
|
-
}
|
|
159
|
-
store.setIsAnonymous(false);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
onAuthStateChange?.(user);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
initializationInProgress = false;
|
|
50
|
+
// Setup the listener
|
|
51
|
+
setupAuthListener(auth, store, autoAnonymousSignIn, onAuthStateChange);
|
|
52
|
+
completeListenerSetup();
|
|
166
53
|
|
|
54
|
+
// Return cleanup function
|
|
167
55
|
return () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Only cleanup when all subscribers unsubscribe
|
|
173
|
-
if (listenerRefCount <= 0 && firebaseUnsubscribe) {
|
|
174
|
-
if (__DEV__) {
|
|
175
|
-
console.log("[AuthListener] Last subscriber, cleaning up listener");
|
|
176
|
-
}
|
|
177
|
-
firebaseUnsubscribe();
|
|
178
|
-
firebaseUnsubscribe = null;
|
|
179
|
-
listenerInitialized = false;
|
|
180
|
-
listenerRefCount = 0;
|
|
181
|
-
anonymousSignInInProgress = false;
|
|
182
|
-
initializationInProgress = false;
|
|
56
|
+
const { shouldCleanup } = decrementRefCount();
|
|
57
|
+
|
|
58
|
+
if (shouldCleanup) {
|
|
59
|
+
resetListenerState();
|
|
183
60
|
}
|
|
184
61
|
};
|
|
185
62
|
}
|
|
@@ -188,19 +65,12 @@ export function initializeAuthListener(
|
|
|
188
65
|
* Reset listener state (for testing)
|
|
189
66
|
*/
|
|
190
67
|
export function resetAuthListener(): void {
|
|
191
|
-
|
|
192
|
-
firebaseUnsubscribe();
|
|
193
|
-
firebaseUnsubscribe = null;
|
|
194
|
-
}
|
|
195
|
-
listenerInitialized = false;
|
|
196
|
-
listenerRefCount = 0;
|
|
197
|
-
anonymousSignInInProgress = false;
|
|
198
|
-
initializationInProgress = false;
|
|
68
|
+
resetListenerState();
|
|
199
69
|
}
|
|
200
70
|
|
|
201
71
|
/**
|
|
202
72
|
* Check if listener is initialized
|
|
203
73
|
*/
|
|
204
74
|
export function isAuthListenerInitialized(): boolean {
|
|
205
|
-
return
|
|
75
|
+
return isListenerInitialized();
|
|
206
76
|
}
|
|
@@ -21,20 +21,9 @@ export interface DeleteAccountOptions {
|
|
|
21
21
|
export async function handleAccountDeletion(
|
|
22
22
|
callbacks: DeleteAccountCallbacks
|
|
23
23
|
): Promise<void> {
|
|
24
|
-
if (__DEV__) {
|
|
25
|
-
console.log("[AccountDeleteHandler] Starting deletion with auto-reauthenticate");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
const result = await deleteCurrentUser({ autoReauthenticate: true });
|
|
29
25
|
|
|
30
|
-
if (__DEV__) {
|
|
31
|
-
console.log("[AccountDeleteHandler] First attempt result:", result);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
26
|
if (result.success) {
|
|
35
|
-
if (__DEV__) {
|
|
36
|
-
console.log("[AccountDeleteHandler] Delete successful");
|
|
37
|
-
}
|
|
38
27
|
return;
|
|
39
28
|
}
|
|
40
29
|
|
|
@@ -44,9 +33,6 @@ export async function handleAccountDeletion(
|
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
if (result.error) {
|
|
47
|
-
if (__DEV__) {
|
|
48
|
-
console.log("[AccountDeleteHandler] Delete failed:", result.error);
|
|
49
|
-
}
|
|
50
36
|
throw new Error(result.error.message);
|
|
51
37
|
}
|
|
52
38
|
}
|
|
@@ -71,36 +57,18 @@ async function handleReauthentication(
|
|
|
71
57
|
}
|
|
72
58
|
|
|
73
59
|
async function retryWithPassword(onPasswordRequired: () => Promise<string | null>): Promise<void> {
|
|
74
|
-
if (__DEV__) {
|
|
75
|
-
console.log("[AccountDeleteHandler] Prompting for password");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
60
|
const password = await onPasswordRequired();
|
|
79
61
|
|
|
80
62
|
if (!password) {
|
|
81
|
-
if (__DEV__) {
|
|
82
|
-
console.log("[AccountDeleteHandler] Password prompt cancelled");
|
|
83
|
-
}
|
|
84
63
|
throw new Error("Password required to delete account");
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
if (__DEV__) {
|
|
88
|
-
console.log("[AccountDeleteHandler] Retrying with password");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
66
|
const result = await deleteCurrentUser({
|
|
92
67
|
autoReauthenticate: false,
|
|
93
68
|
password,
|
|
94
69
|
});
|
|
95
70
|
|
|
96
|
-
if (__DEV__) {
|
|
97
|
-
console.log("[AccountDeleteHandler] Password retry result:", result);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
71
|
if (result.success) {
|
|
101
|
-
if (__DEV__) {
|
|
102
|
-
console.log("[AccountDeleteHandler] Delete successful after password reauth");
|
|
103
|
-
}
|
|
104
72
|
return;
|
|
105
73
|
}
|
|
106
74
|
|
|
@@ -110,34 +78,15 @@ async function retryWithPassword(onPasswordRequired: () => Promise<string | null
|
|
|
110
78
|
}
|
|
111
79
|
|
|
112
80
|
async function retryWithSocialAuth(onReauthRequired: () => Promise<boolean>): Promise<void> {
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.log("[AccountDeleteHandler] Requesting social auth reauth");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
81
|
const reauthSuccess = await onReauthRequired();
|
|
118
82
|
|
|
119
|
-
if (__DEV__) {
|
|
120
|
-
console.log("[AccountDeleteHandler] Reauth result:", reauthSuccess);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
83
|
if (!reauthSuccess) {
|
|
124
84
|
throw new Error("Reauthentication required to delete account");
|
|
125
85
|
}
|
|
126
86
|
|
|
127
|
-
if (__DEV__) {
|
|
128
|
-
console.log("[AccountDeleteHandler] Retrying deletion after reauth");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
87
|
const result = await deleteCurrentUser({ autoReauthenticate: false });
|
|
132
88
|
|
|
133
|
-
if (__DEV__) {
|
|
134
|
-
console.log("[AccountDeleteHandler] Reauth retry result:", result);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
89
|
if (result.success) {
|
|
138
|
-
if (__DEV__) {
|
|
139
|
-
console.log("[AccountDeleteHandler] Delete successful after reauth");
|
|
140
|
-
}
|
|
141
90
|
return;
|
|
142
91
|
}
|
|
143
92
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Transition Detection Utilities
|
|
3
|
+
* Detects and handles authentication state transitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useRef, useEffect } from "react";
|
|
7
|
+
|
|
8
|
+
export interface AuthTransitionState {
|
|
9
|
+
isAuthenticated: boolean;
|
|
10
|
+
isAnonymous: boolean;
|
|
11
|
+
isVisible: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AuthTransitionResult {
|
|
15
|
+
justAuthenticated: boolean;
|
|
16
|
+
justConvertedFromAnonymous: boolean;
|
|
17
|
+
shouldClose: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Track auth state transitions
|
|
22
|
+
*/
|
|
23
|
+
export function useAuthTransitions(
|
|
24
|
+
state: AuthTransitionState,
|
|
25
|
+
onTransition?: (result: AuthTransitionResult) => void
|
|
26
|
+
): void {
|
|
27
|
+
const prevIsAuthenticatedRef = useRef(state.isAuthenticated);
|
|
28
|
+
const prevIsAnonymousRef = useRef(state.isAnonymous);
|
|
29
|
+
const prevIsVisibleRef = useRef(state.isVisible);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const justAuthenticated = !prevIsAuthenticatedRef.current && state.isAuthenticated;
|
|
33
|
+
const justConvertedFromAnonymous =
|
|
34
|
+
prevIsAnonymousRef.current && !state.isAnonymous && state.isAuthenticated;
|
|
35
|
+
const shouldClose =
|
|
36
|
+
(justAuthenticated || justConvertedFromAnonymous) && state.isVisible && !state.isAnonymous;
|
|
37
|
+
|
|
38
|
+
const result: AuthTransitionResult = {
|
|
39
|
+
justAuthenticated,
|
|
40
|
+
justConvertedFromAnonymous,
|
|
41
|
+
shouldClose,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
onTransition?.(result);
|
|
45
|
+
|
|
46
|
+
prevIsAuthenticatedRef.current = state.isAuthenticated;
|
|
47
|
+
prevIsVisibleRef.current = state.isVisible;
|
|
48
|
+
prevIsAnonymousRef.current = state.isAnonymous;
|
|
49
|
+
}, [state.isAuthenticated, state.isVisible, state.isAnonymous, onTransition]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if should close modal after auth transition
|
|
54
|
+
*/
|
|
55
|
+
export function shouldCloseAfterAuth(
|
|
56
|
+
justAuthenticated: boolean,
|
|
57
|
+
justConvertedFromAnonymous: boolean,
|
|
58
|
+
isVisible: boolean,
|
|
59
|
+
isAnonymous: boolean
|
|
60
|
+
): boolean {
|
|
61
|
+
return (justAuthenticated || justConvertedFromAnonymous) && isVisible && !isAnonymous;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute callback with delay after auth
|
|
66
|
+
*/
|
|
67
|
+
export function executeAfterAuth(
|
|
68
|
+
callback: () => void,
|
|
69
|
+
delay: number = 100
|
|
70
|
+
): NodeJS.Timeout {
|
|
71
|
+
return setTimeout(callback, delay);
|
|
72
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Field State Management Utilities
|
|
3
|
+
* Shared utilities for form field state management across all auth forms
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useState } from "react";
|
|
7
|
+
|
|
8
|
+
export type FieldState = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
export interface UseFieldStateResult<T extends FieldState> {
|
|
11
|
+
fields: T;
|
|
12
|
+
updateField: <K extends keyof T>(field: K, value: T[K]) => void;
|
|
13
|
+
setFields: (fields: T | ((prev: T) => T)) => void;
|
|
14
|
+
resetFields: (initial: T) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook for managing form field state with automatic error clearing
|
|
19
|
+
* @param initialFields - Initial field values
|
|
20
|
+
* @param clearErrors - Function to clear errors when fields change
|
|
21
|
+
* @returns Field state and handlers
|
|
22
|
+
*/
|
|
23
|
+
export function useFieldState<T extends FieldState>(
|
|
24
|
+
initialFields: T,
|
|
25
|
+
clearErrors?: (fields?: (keyof T)[]) => void
|
|
26
|
+
): UseFieldStateResult<T> {
|
|
27
|
+
const [fields, setFields] = useState<T>(initialFields);
|
|
28
|
+
|
|
29
|
+
const updateField = useCallback(
|
|
30
|
+
<K extends keyof T>(field: K, value: T[K]) => {
|
|
31
|
+
setFields((prev) => ({ ...prev, [field]: value }));
|
|
32
|
+
clearErrors?.([field as string]);
|
|
33
|
+
},
|
|
34
|
+
[clearErrors]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const resetFields = useCallback((initial: T) => {
|
|
38
|
+
setFields(initial);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
fields,
|
|
43
|
+
updateField,
|
|
44
|
+
setFields,
|
|
45
|
+
resetFields,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a field change handler that updates field value and clears errors
|
|
51
|
+
* @param setter - State setter for the field
|
|
52
|
+
* @param clearErrors - Function to clear errors
|
|
53
|
+
* @param fieldsToClear - Fields to clear when this field changes
|
|
54
|
+
* @returns Field change handler
|
|
55
|
+
*/
|
|
56
|
+
export function createFieldChangeHandler(
|
|
57
|
+
setter: (value: string) => void,
|
|
58
|
+
clearErrors?: () => void
|
|
59
|
+
): (value: string) => void {
|
|
60
|
+
return (value: string) => {
|
|
61
|
+
setter(value);
|
|
62
|
+
clearErrors?.();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create multiple field change handlers
|
|
68
|
+
* @param setters - Object mapping field names to their setters
|
|
69
|
+
* @param clearErrors - Function to clear errors
|
|
70
|
+
* @returns Object mapping field names to change handlers
|
|
71
|
+
*/
|
|
72
|
+
export function createFieldChangeHandlers<T extends Record<string, (value: string) => void>>(
|
|
73
|
+
setters: T,
|
|
74
|
+
clearErrors?: () => void
|
|
75
|
+
): T {
|
|
76
|
+
return Object.fromEntries(
|
|
77
|
+
Object.entries(setters).map(([key, setter]) => [
|
|
78
|
+
key,
|
|
79
|
+
createFieldChangeHandler(setter, clearErrors),
|
|
80
|
+
])
|
|
81
|
+
) as T;
|
|
82
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Validation Utilities
|
|
3
|
+
* Shared validation logic for all auth forms
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import {
|
|
8
|
+
validateEmail,
|
|
9
|
+
validatePasswordForLogin,
|
|
10
|
+
validatePasswordForRegister,
|
|
11
|
+
validatePasswordConfirmation,
|
|
12
|
+
} from "../../../infrastructure/utils/AuthValidation";
|
|
13
|
+
import type { PasswordConfig } from "../../../domain/value-objects/AuthConfig";
|
|
14
|
+
|
|
15
|
+
export interface FormValidationError {
|
|
16
|
+
field: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FormValidationResult {
|
|
21
|
+
isValid: boolean;
|
|
22
|
+
errors: FormValidationError[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LoginFormValues {
|
|
26
|
+
email: string;
|
|
27
|
+
password: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RegisterFormValues {
|
|
31
|
+
displayName?: string;
|
|
32
|
+
email: string;
|
|
33
|
+
password: string;
|
|
34
|
+
confirmPassword: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProfileFormValues {
|
|
38
|
+
displayName: string;
|
|
39
|
+
email: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate login form fields
|
|
44
|
+
* @param values - Form values to validate
|
|
45
|
+
* @param getErrorMessage - Function to get localized error messages
|
|
46
|
+
* @returns Validation result
|
|
47
|
+
*/
|
|
48
|
+
export function validateLoginForm(
|
|
49
|
+
values: LoginFormValues,
|
|
50
|
+
getErrorMessage: (key: string) => string
|
|
51
|
+
): FormValidationResult {
|
|
52
|
+
const errors: FormValidationError[] = [];
|
|
53
|
+
|
|
54
|
+
const emailResult = validateEmail(values.email.trim());
|
|
55
|
+
if (!emailResult.isValid && emailResult.error) {
|
|
56
|
+
errors.push({ field: "email", message: getErrorMessage(emailResult.error) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const passwordResult = validatePasswordForLogin(values.password);
|
|
60
|
+
if (!passwordResult.isValid && passwordResult.error) {
|
|
61
|
+
errors.push({ field: "password", message: getErrorMessage(passwordResult.error) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
isValid: errors.length === 0,
|
|
66
|
+
errors,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate register form fields
|
|
72
|
+
* @param values - Form values to validate
|
|
73
|
+
* @param getErrorMessage - Function to get localized error messages
|
|
74
|
+
* @param passwordConfig - Password configuration
|
|
75
|
+
* @returns Validation result
|
|
76
|
+
*/
|
|
77
|
+
export function validateRegisterForm(
|
|
78
|
+
values: RegisterFormValues,
|
|
79
|
+
getErrorMessage: (key: string) => string,
|
|
80
|
+
passwordConfig: PasswordConfig
|
|
81
|
+
): FormValidationResult {
|
|
82
|
+
const errors: FormValidationError[] = [];
|
|
83
|
+
|
|
84
|
+
const emailResult = validateEmail(values.email.trim());
|
|
85
|
+
if (!emailResult.isValid && emailResult.error) {
|
|
86
|
+
errors.push({ field: "email", message: getErrorMessage(emailResult.error) });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const passwordResult = validatePasswordForRegister(values.password, passwordConfig);
|
|
90
|
+
if (!passwordResult.isValid && passwordResult.error) {
|
|
91
|
+
errors.push({ field: "password", message: getErrorMessage(passwordResult.error) });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const confirmResult = validatePasswordConfirmation(values.password, values.confirmPassword);
|
|
95
|
+
if (!confirmResult.isValid && confirmResult.error) {
|
|
96
|
+
errors.push({ field: "confirmPassword", message: getErrorMessage(confirmResult.error) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
isValid: errors.length === 0,
|
|
101
|
+
errors,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate profile form fields
|
|
107
|
+
* @param values - Form values to validate
|
|
108
|
+
* @returns Validation result
|
|
109
|
+
*/
|
|
110
|
+
export function validateProfileForm(values: ProfileFormValues): FormValidationResult {
|
|
111
|
+
const errors: FormValidationError[] = [];
|
|
112
|
+
|
|
113
|
+
if (!values.displayName.trim()) {
|
|
114
|
+
errors.push({ field: "displayName", message: "Display name is required" });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (values.email) {
|
|
118
|
+
const emailResult = validateEmail(values.email);
|
|
119
|
+
if (!emailResult.isValid && emailResult.error) {
|
|
120
|
+
errors.push({ field: "email", message: emailResult.error });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
isValid: errors.length === 0,
|
|
126
|
+
errors,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convert validation errors to field error object
|
|
132
|
+
* @param errors - Validation errors
|
|
133
|
+
* @returns Object mapping field names to error messages
|
|
134
|
+
*/
|
|
135
|
+
export function errorsToFieldErrors(
|
|
136
|
+
errors: FormValidationError[]
|
|
137
|
+
): Record<string, string> {
|
|
138
|
+
const result: Record<string, string> = {};
|
|
139
|
+
for (const error of errors) {
|
|
140
|
+
result[error.field] = error.message;
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Hook for form validation with error message resolution
|
|
147
|
+
* @param getErrorMessage - Function to get localized error messages
|
|
148
|
+
* @returns Validation functions
|
|
149
|
+
*/
|
|
150
|
+
export function useFormValidation(getErrorMessage: (key: string) => string) {
|
|
151
|
+
const validateLogin = useCallback(
|
|
152
|
+
(values: LoginFormValues) => validateLoginForm(values, getErrorMessage),
|
|
153
|
+
[getErrorMessage]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const validateRegister = useCallback(
|
|
157
|
+
(values: RegisterFormValues, passwordConfig: PasswordConfig) =>
|
|
158
|
+
validateRegisterForm(values, getErrorMessage, passwordConfig),
|
|
159
|
+
[getErrorMessage]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const validateProfile = useCallback(
|
|
163
|
+
(values: ProfileFormValues) => validateProfileForm(values),
|
|
164
|
+
[]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
validateLogin,
|
|
169
|
+
validateRegister,
|
|
170
|
+
validateProfile,
|
|
171
|
+
errorsToFieldErrors,
|
|
172
|
+
};
|
|
173
|
+
}
|