@umituz/react-native-auth 3.6.86 → 3.6.88
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/infrastructure/services/AnonymousModeService.ts +5 -2
- package/src/infrastructure/services/AuthService.ts +13 -1
- package/src/infrastructure/services/UserDocument.types.ts +60 -0
- package/src/infrastructure/services/UserDocumentService.ts +85 -0
- package/src/infrastructure/utils/AuthValidation.ts +8 -4
- package/src/infrastructure/utils/UserMapper.ts +7 -3
- package/src/infrastructure/utils/authStateHandler.ts +7 -0
- package/src/infrastructure/utils/error/errorCodeMapping.constants.ts +12 -109
- package/src/infrastructure/utils/error/mappings/actionCodeErrorMappings.ts +26 -0
- package/src/infrastructure/utils/error/mappings/authErrorMappings.ts +69 -0
- package/src/infrastructure/utils/error/mappings/configErrorMappings.ts +31 -0
- package/src/infrastructure/utils/error/mappings/errorMapping.types.ts +12 -0
- package/src/infrastructure/utils/error/mappings/networkErrorMappings.ts +22 -0
- package/src/infrastructure/utils/listener/anonymousHandler.ts +31 -0
- package/src/infrastructure/utils/listener/authStateHandler.ts +59 -0
- package/src/infrastructure/utils/listener/cleanupHandlers.ts +46 -0
- package/src/infrastructure/utils/listener/initializationHandlers.ts +26 -0
- package/src/infrastructure/utils/listener/listenerLifecycle.util.ts +19 -161
- package/src/infrastructure/utils/listener/listenerState.util.ts +21 -3
- package/src/infrastructure/utils/listener/setupListener.ts +63 -0
- package/src/infrastructure/utils/userDocumentBuilder.util.ts +110 -0
- package/src/presentation/hooks/registerForm/registerFormHandlers.ts +59 -0
- package/src/presentation/hooks/registerForm/registerFormSubmit.ts +64 -0
- package/src/presentation/hooks/registerForm/useRegisterForm.types.ts +39 -0
- package/src/presentation/hooks/useProfileEdit.ts +7 -4
- package/src/presentation/hooks/useRegisterForm.ts +31 -109
- package/src/presentation/utils/form/formValidation.util.ts +23 -114
- package/src/presentation/utils/form/useFormField.hook.ts +2 -2
- package/src/presentation/utils/form/validation/formValidation.hook.ts +30 -0
- package/src/presentation/utils/form/validation/formValidation.types.ts +31 -0
- package/src/presentation/utils/form/validation/formValidation.utils.ts +17 -0
- package/src/presentation/utils/form/validation/formValidators.ts +86 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup Handlers
|
|
3
|
+
* Manages unsubscribe and cleanup logic for auth listener
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isListenerInitialized,
|
|
8
|
+
incrementRefCount,
|
|
9
|
+
decrementRefCount,
|
|
10
|
+
resetListenerState,
|
|
11
|
+
} from "./listenerState.util";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create unsubscribe function that decrements ref count
|
|
15
|
+
*/
|
|
16
|
+
export function createUnsubscribeHandler(): () => void {
|
|
17
|
+
return () => {
|
|
18
|
+
const { shouldCleanup } = decrementRefCount();
|
|
19
|
+
|
|
20
|
+
if (shouldCleanup) {
|
|
21
|
+
resetListenerState();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return existing unsubscribe if already initialized
|
|
28
|
+
* Returns null if initialization is in progress
|
|
29
|
+
*/
|
|
30
|
+
export function handleExistingInitialization(): (() => void) | null {
|
|
31
|
+
if (!isListenerInitialized()) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
incrementRefCount();
|
|
36
|
+
return createUnsubscribeHandler();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return no-op if initialization is in progress
|
|
41
|
+
*/
|
|
42
|
+
export function handleInitializationInProgress(): () => void {
|
|
43
|
+
return () => {
|
|
44
|
+
// No-op - handled by initial initialization
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialization Handlers
|
|
3
|
+
* Helper functions for listener initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthActions } from "../../../types/auth-store.types";
|
|
7
|
+
import { completeInitialization } from "./listenerState.util";
|
|
8
|
+
|
|
9
|
+
type Store = AuthActions & { isAnonymous: boolean };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle case where Firebase auth is not available
|
|
13
|
+
*/
|
|
14
|
+
export function handleNoFirebaseAuth(store: Store): () => void {
|
|
15
|
+
completeInitialization();
|
|
16
|
+
store.setLoading(false);
|
|
17
|
+
store.setInitialized(true);
|
|
18
|
+
return () => {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Complete listener setup and mark as initialized
|
|
23
|
+
*/
|
|
24
|
+
export function completeListenerSetup(): void {
|
|
25
|
+
completeInitialization();
|
|
26
|
+
}
|
|
@@ -1,168 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth Listener Lifecycle
|
|
3
|
-
*
|
|
2
|
+
* Auth Listener Lifecycle - Main Export
|
|
3
|
+
* Re-exports all listener lifecycle utilities from modular files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
setUnsubscribe,
|
|
13
|
-
incrementRefCount,
|
|
14
|
-
decrementRefCount,
|
|
15
|
-
startAnonymousSignIn,
|
|
16
|
-
completeAnonymousSignIn,
|
|
17
|
-
resetListenerState,
|
|
18
|
-
} from "./listenerState.util";
|
|
19
|
-
import { onIdTokenChanged } from "firebase/auth";
|
|
20
|
-
import { getAuthService } from "../../../infrastructure/services/AuthService";
|
|
21
|
-
import {
|
|
22
|
-
createOrUpdateUserDocument,
|
|
23
|
-
getFirestoreInstance
|
|
24
|
-
} from "../../repositories/UserDocumentRepository";
|
|
6
|
+
// Cleanup handlers
|
|
7
|
+
export {
|
|
8
|
+
createUnsubscribeHandler,
|
|
9
|
+
handleExistingInitialization,
|
|
10
|
+
handleInitializationInProgress,
|
|
11
|
+
} from "./cleanupHandlers";
|
|
25
12
|
|
|
26
|
-
|
|
13
|
+
// Setup listener
|
|
14
|
+
export { setupAuthListener } from "./setupListener";
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
export function createUnsubscribeHandler(): () => void {
|
|
32
|
-
return () => {
|
|
33
|
-
const { shouldCleanup } = decrementRefCount();
|
|
34
|
-
|
|
35
|
-
if (shouldCleanup) {
|
|
36
|
-
resetListenerState();
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Return existing unsubscribe if already initialized
|
|
43
|
-
* Returns null if initialization is in progress
|
|
44
|
-
*/
|
|
45
|
-
export function handleExistingInitialization(): (() => void) | null {
|
|
46
|
-
if (!isListenerInitialized()) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
incrementRefCount();
|
|
51
|
-
return createUnsubscribeHandler();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Return no-op if initialization is in progress
|
|
56
|
-
*/
|
|
57
|
-
export function handleInitializationInProgress(): () => void {
|
|
58
|
-
return () => {
|
|
59
|
-
// No-op - handled by initial initialization
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Setup Firebase auth listener
|
|
65
|
-
*/
|
|
66
|
-
export function setupAuthListener(
|
|
67
|
-
auth: Auth,
|
|
68
|
-
store: Store,
|
|
69
|
-
autoAnonymousSignIn: boolean,
|
|
70
|
-
onAuthStateChange?: (user: User | null) => void
|
|
71
|
-
): void {
|
|
72
|
-
const service = getAuthService();
|
|
73
|
-
|
|
74
|
-
if (service) {
|
|
75
|
-
const isAnonymous = service.getIsAnonymousMode();
|
|
76
|
-
if (isAnonymous) {
|
|
77
|
-
store.setIsAnonymous(true);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const unsubscribe = onIdTokenChanged(auth, (user) => {
|
|
83
|
-
handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange);
|
|
84
|
-
});
|
|
16
|
+
// Auth state handler
|
|
17
|
+
export { handleAuthStateChange } from "./authStateHandler";
|
|
85
18
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// If listener setup fails, ensure we clean up and mark as initialized
|
|
89
|
-
console.error("[AuthListener] Failed to setup auth listener:", error);
|
|
90
|
-
completeInitialization();
|
|
91
|
-
store.setLoading(false);
|
|
92
|
-
store.setInitialized(true);
|
|
93
|
-
store.setError("Failed to initialize authentication listener");
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Handle auth state change from Firebase
|
|
100
|
-
*/
|
|
101
|
-
function handleAuthStateChange(
|
|
102
|
-
user: User | null,
|
|
103
|
-
store: Store,
|
|
104
|
-
auth: Auth,
|
|
105
|
-
autoAnonymousSignIn: boolean,
|
|
106
|
-
onAuthStateChange?: (user: User | null) => void
|
|
107
|
-
): void {
|
|
108
|
-
try {
|
|
109
|
-
if (!user && autoAnonymousSignIn) {
|
|
110
|
-
// Start anonymous sign-in without blocking
|
|
111
|
-
void handleAnonymousMode(store, auth);
|
|
112
|
-
store.setFirebaseUser(null);
|
|
113
|
-
completeInitialization();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
store.setFirebaseUser(user);
|
|
118
|
-
store.setInitialized(true);
|
|
119
|
-
|
|
120
|
-
// Create or update Firestore user document (best practice)
|
|
121
|
-
if (user) {
|
|
122
|
-
void createOrUpdateUserDocument(getFirestoreInstance(), user);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Handle conversion from anonymous
|
|
126
|
-
if (user && !user.isAnonymous && store.isAnonymous) {
|
|
127
|
-
store.setIsAnonymous(false);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
onAuthStateChange?.(user);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error("[AuthListener] Error handling auth state change:", error);
|
|
133
|
-
// Ensure we don't leave the app in a bad state
|
|
134
|
-
store.setInitialized(true);
|
|
135
|
-
store.setLoading(false);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Handle anonymous mode sign-in
|
|
141
|
-
*/
|
|
142
|
-
async function handleAnonymousMode(store: Store, auth: Auth): Promise<void> {
|
|
143
|
-
if (!startAnonymousSignIn()) {
|
|
144
|
-
return; // Already signing in
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const handleAnonymousSignIn = createAnonymousSignInHandler(auth, store);
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
await handleAnonymousSignIn();
|
|
151
|
-
} finally {
|
|
152
|
-
completeAnonymousSignIn();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Handle case where Firebase auth is not available
|
|
158
|
-
*/
|
|
159
|
-
export function handleNoFirebaseAuth(store: Store): () => void {
|
|
160
|
-
completeInitialization();
|
|
161
|
-
store.setLoading(false);
|
|
162
|
-
store.setInitialized(true);
|
|
163
|
-
return () => {};
|
|
164
|
-
}
|
|
19
|
+
// Anonymous mode handler
|
|
20
|
+
export { handleAnonymousMode } from "./anonymousHandler";
|
|
165
21
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
22
|
+
// Initialization handlers
|
|
23
|
+
export {
|
|
24
|
+
handleNoFirebaseAuth,
|
|
25
|
+
completeListenerSetup,
|
|
26
|
+
} from "./initializationHandlers";
|
|
@@ -8,6 +8,7 @@ export interface ListenerState {
|
|
|
8
8
|
refCount: number;
|
|
9
9
|
initializationInProgress: boolean;
|
|
10
10
|
anonymousSignInInProgress: boolean;
|
|
11
|
+
cleanupInProgress: boolean;
|
|
11
12
|
unsubscribe: (() => void) | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -16,6 +17,7 @@ const state: ListenerState = {
|
|
|
16
17
|
refCount: 0,
|
|
17
18
|
initializationInProgress: false,
|
|
18
19
|
anonymousSignInInProgress: false,
|
|
20
|
+
cleanupInProgress: false,
|
|
19
21
|
unsubscribe: null,
|
|
20
22
|
};
|
|
21
23
|
|
|
@@ -60,11 +62,16 @@ export function startInitialization(): boolean {
|
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
64
|
* Complete initialization
|
|
65
|
+
* If refCount is 0, set to 1 (first subscriber)
|
|
66
|
+
* Otherwise, keep existing refCount from concurrent subscribers
|
|
63
67
|
*/
|
|
64
68
|
export function completeInitialization(): void {
|
|
65
69
|
state.initializationInProgress = false;
|
|
66
70
|
state.initialized = true;
|
|
67
|
-
|
|
71
|
+
// Only set refCount to 1 if it's still 0 (no concurrent subscribers)
|
|
72
|
+
if (state.refCount === 0) {
|
|
73
|
+
state.refCount = 1;
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
/**
|
|
@@ -85,10 +92,20 @@ export function incrementRefCount(): number {
|
|
|
85
92
|
/**
|
|
86
93
|
* Decrement reference count when a subscriber leaves
|
|
87
94
|
* Returns true if cleanup should be performed
|
|
95
|
+
* Uses cleanupInProgress flag to prevent concurrent cleanup attempts
|
|
88
96
|
*/
|
|
89
97
|
export function decrementRefCount(): { shouldCleanup: boolean; count: number } {
|
|
90
98
|
state.refCount--;
|
|
91
|
-
const shouldCleanup =
|
|
99
|
+
const shouldCleanup =
|
|
100
|
+
state.refCount <= 0 &&
|
|
101
|
+
state.unsubscribe !== null &&
|
|
102
|
+
!state.cleanupInProgress;
|
|
103
|
+
|
|
104
|
+
// If cleanup should happen, mark as in progress to prevent concurrent cleanup
|
|
105
|
+
if (shouldCleanup) {
|
|
106
|
+
state.cleanupInProgress = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
return { shouldCleanup, count: state.refCount };
|
|
93
110
|
}
|
|
94
111
|
|
|
@@ -111,7 +128,7 @@ export function completeAnonymousSignIn(): void {
|
|
|
111
128
|
}
|
|
112
129
|
|
|
113
130
|
/**
|
|
114
|
-
* Reset all state (for testing)
|
|
131
|
+
* Reset all state (for testing and cleanup)
|
|
115
132
|
*/
|
|
116
133
|
export function resetListenerState(): void {
|
|
117
134
|
if (state.unsubscribe) {
|
|
@@ -121,5 +138,6 @@ export function resetListenerState(): void {
|
|
|
121
138
|
state.refCount = 0;
|
|
122
139
|
state.initializationInProgress = false;
|
|
123
140
|
state.anonymousSignInInProgress = false;
|
|
141
|
+
state.cleanupInProgress = false;
|
|
124
142
|
state.unsubscribe = null;
|
|
125
143
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup Auth Listener
|
|
3
|
+
* Configures Firebase auth listener with timeout protection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Auth, User } from "firebase/auth";
|
|
7
|
+
import { onIdTokenChanged } from "firebase/auth";
|
|
8
|
+
import type { AuthActions } from "../../../types/auth-store.types";
|
|
9
|
+
import { getAuthService } from "../../services/AuthService";
|
|
10
|
+
import { completeInitialization, setUnsubscribe } from "./listenerState.util";
|
|
11
|
+
import { handleAuthStateChange } from "./authStateHandler";
|
|
12
|
+
|
|
13
|
+
type Store = AuthActions & { isAnonymous: boolean };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Setup Firebase auth listener with timeout protection
|
|
17
|
+
*/
|
|
18
|
+
export function setupAuthListener(
|
|
19
|
+
auth: Auth,
|
|
20
|
+
store: Store,
|
|
21
|
+
autoAnonymousSignIn: boolean,
|
|
22
|
+
onAuthStateChange?: (user: User | null) => void
|
|
23
|
+
): void {
|
|
24
|
+
const service = getAuthService();
|
|
25
|
+
|
|
26
|
+
if (service) {
|
|
27
|
+
const isAnonymous = service.getIsAnonymousMode();
|
|
28
|
+
if (isAnonymous) {
|
|
29
|
+
store.setIsAnonymous(true);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Safety timeout: if listener doesn't trigger within 10 seconds, mark as initialized
|
|
34
|
+
let hasTriggered = false;
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
if (!hasTriggered) {
|
|
37
|
+
console.warn("[AuthListener] Auth listener timeout - marking as initialized");
|
|
38
|
+
store.setInitialized(true);
|
|
39
|
+
store.setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}, 10000);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const unsubscribe = onIdTokenChanged(auth, (user) => {
|
|
45
|
+
if (!hasTriggered) {
|
|
46
|
+
hasTriggered = true;
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
}
|
|
49
|
+
handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
setUnsubscribe(unsubscribe);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
// If listener setup fails, ensure we clean up and mark as initialized
|
|
56
|
+
console.error("[AuthListener] Failed to setup auth listener:", error);
|
|
57
|
+
completeInitialization();
|
|
58
|
+
store.setLoading(false);
|
|
59
|
+
store.setInitialized(true);
|
|
60
|
+
store.setError("Failed to initialize authentication listener");
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Document Builder Utility
|
|
3
|
+
* Builds user document data for Firestore (max 100 lines)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { serverTimestamp } from "firebase/firestore";
|
|
7
|
+
import type {
|
|
8
|
+
UserDocumentUser,
|
|
9
|
+
UserDocumentExtras,
|
|
10
|
+
} from "../services/UserDocument.types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets the sign-up method from user provider data
|
|
14
|
+
*/
|
|
15
|
+
export function getSignUpMethod(user: UserDocumentUser): string | undefined {
|
|
16
|
+
if (user.isAnonymous) return "anonymous";
|
|
17
|
+
if (user.email) {
|
|
18
|
+
const providerData = (
|
|
19
|
+
user as unknown as { providerData?: { providerId: string }[] }
|
|
20
|
+
).providerData;
|
|
21
|
+
if (providerData && providerData.length > 0) {
|
|
22
|
+
const providerId = providerData[0]?.providerId;
|
|
23
|
+
if (providerId === "google.com") return "google";
|
|
24
|
+
if (providerId === "apple.com") return "apple";
|
|
25
|
+
if (providerId === "password") return "email";
|
|
26
|
+
}
|
|
27
|
+
return "email";
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds base user data from user object and extras
|
|
34
|
+
*/
|
|
35
|
+
export function buildBaseData(
|
|
36
|
+
user: UserDocumentUser,
|
|
37
|
+
extras?: UserDocumentExtras,
|
|
38
|
+
): Record<string, unknown> {
|
|
39
|
+
const data: Record<string, unknown> = {
|
|
40
|
+
uid: user.uid,
|
|
41
|
+
displayName: user.displayName,
|
|
42
|
+
email: user.email,
|
|
43
|
+
photoURL: user.photoURL,
|
|
44
|
+
isAnonymous: user.isAnonymous,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (extras) {
|
|
48
|
+
const internalFields = ["signUpMethod", "previousAnonymousUserId"];
|
|
49
|
+
Object.keys(extras).forEach((key) => {
|
|
50
|
+
if (!internalFields.includes(key)) {
|
|
51
|
+
const val = extras[key];
|
|
52
|
+
if (val !== undefined) {
|
|
53
|
+
data[key] = val;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds user document data for creation
|
|
64
|
+
*/
|
|
65
|
+
export function buildCreateData(
|
|
66
|
+
baseData: Record<string, unknown>,
|
|
67
|
+
extraFields: Record<string, unknown> | undefined,
|
|
68
|
+
extras?: UserDocumentExtras,
|
|
69
|
+
): Record<string, unknown> {
|
|
70
|
+
const createData: Record<string, unknown> = {
|
|
71
|
+
...baseData,
|
|
72
|
+
...extraFields,
|
|
73
|
+
createdAt: serverTimestamp(),
|
|
74
|
+
updatedAt: serverTimestamp(),
|
|
75
|
+
lastLoginAt: serverTimestamp(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (extras?.previousAnonymousUserId) {
|
|
79
|
+
createData.previousAnonymousUserId = extras.previousAnonymousUserId;
|
|
80
|
+
createData.convertedFromAnonymous = true;
|
|
81
|
+
createData.convertedAt = serverTimestamp();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (extras?.signUpMethod) createData.signUpMethod = extras.signUpMethod;
|
|
85
|
+
|
|
86
|
+
return createData;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds user document data for update
|
|
91
|
+
*/
|
|
92
|
+
export function buildUpdateData(
|
|
93
|
+
baseData: Record<string, unknown>,
|
|
94
|
+
extras?: UserDocumentExtras,
|
|
95
|
+
): Record<string, unknown> {
|
|
96
|
+
const updateData: Record<string, unknown> = {
|
|
97
|
+
...baseData,
|
|
98
|
+
lastLoginAt: serverTimestamp(),
|
|
99
|
+
updatedAt: serverTimestamp(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (extras?.previousAnonymousUserId) {
|
|
103
|
+
updateData.previousAnonymousUserId = extras.previousAnonymousUserId;
|
|
104
|
+
updateData.convertedFromAnonymous = true;
|
|
105
|
+
updateData.convertedAt = serverTimestamp();
|
|
106
|
+
if (extras?.signUpMethod) updateData.signUpMethod = extras.signUpMethod;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return updateData;
|
|
110
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register Form Change Handlers
|
|
3
|
+
* Input field change handlers with error clearing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import type { FieldErrors } from "./useRegisterForm.types";
|
|
8
|
+
import { clearFieldError, clearFieldErrors } from "../../utils/form/formErrorUtils";
|
|
9
|
+
|
|
10
|
+
type RegisterFieldKey = "displayName" | "email" | "password" | "confirmPassword";
|
|
11
|
+
|
|
12
|
+
export function useRegisterFormHandlers(
|
|
13
|
+
updateField: (field: RegisterFieldKey, value: string) => void,
|
|
14
|
+
setFieldErrors: React.Dispatch<React.SetStateAction<FieldErrors>>,
|
|
15
|
+
clearLocalError: () => void
|
|
16
|
+
) {
|
|
17
|
+
const handleDisplayNameChange = useCallback(
|
|
18
|
+
(text: string) => {
|
|
19
|
+
updateField("displayName", text);
|
|
20
|
+
clearFieldError(setFieldErrors, "displayName");
|
|
21
|
+
clearLocalError();
|
|
22
|
+
},
|
|
23
|
+
[updateField, clearLocalError]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const handleEmailChange = useCallback(
|
|
27
|
+
(text: string) => {
|
|
28
|
+
updateField("email", text);
|
|
29
|
+
clearFieldError(setFieldErrors, "email");
|
|
30
|
+
clearLocalError();
|
|
31
|
+
},
|
|
32
|
+
[updateField, clearLocalError]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const handlePasswordChange = useCallback(
|
|
36
|
+
(text: string) => {
|
|
37
|
+
updateField("password", text);
|
|
38
|
+
clearFieldErrors(setFieldErrors, ["password", "confirmPassword"]);
|
|
39
|
+
clearLocalError();
|
|
40
|
+
},
|
|
41
|
+
[updateField, clearLocalError]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const handleConfirmPasswordChange = useCallback(
|
|
45
|
+
(text: string) => {
|
|
46
|
+
updateField("confirmPassword", text);
|
|
47
|
+
clearFieldError(setFieldErrors, "confirmPassword");
|
|
48
|
+
clearLocalError();
|
|
49
|
+
},
|
|
50
|
+
[updateField, clearLocalError]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
handleDisplayNameChange,
|
|
55
|
+
handleEmailChange,
|
|
56
|
+
handlePasswordChange,
|
|
57
|
+
handleConfirmPasswordChange,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register Form Submit Logic
|
|
3
|
+
* Handles form validation and sign-up submission
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { alertService } from "@umituz/react-native-design-system";
|
|
8
|
+
import { DEFAULT_PASSWORD_CONFIG } from "../../../domain/value-objects/AuthConfig";
|
|
9
|
+
import {
|
|
10
|
+
validateRegisterForm,
|
|
11
|
+
errorsToFieldErrors,
|
|
12
|
+
} from "../../utils/form/formValidation.util";
|
|
13
|
+
import { getAuthErrorLocalizationKey } from "../../utils/getAuthErrorMessage";
|
|
14
|
+
import type { FieldErrors, RegisterFormTranslations } from "./useRegisterForm.types";
|
|
15
|
+
|
|
16
|
+
interface RegisterFormFields {
|
|
17
|
+
displayName: string;
|
|
18
|
+
email: string;
|
|
19
|
+
password: string;
|
|
20
|
+
confirmPassword: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useRegisterFormSubmit(
|
|
24
|
+
fields: RegisterFormFields,
|
|
25
|
+
signUp: (email: string, password: string, displayName?: string) => Promise<void>,
|
|
26
|
+
setFieldErrors: React.Dispatch<React.SetStateAction<FieldErrors>>,
|
|
27
|
+
setLocalError: React.Dispatch<React.SetStateAction<string | null>>,
|
|
28
|
+
clearFormErrors: () => void,
|
|
29
|
+
getErrorMessage: (key: string) => string,
|
|
30
|
+
translations?: RegisterFormTranslations
|
|
31
|
+
) {
|
|
32
|
+
const handleSignUp = useCallback(async () => {
|
|
33
|
+
clearFormErrors();
|
|
34
|
+
|
|
35
|
+
const validation = validateRegisterForm(
|
|
36
|
+
{
|
|
37
|
+
displayName: fields.displayName.trim() || undefined,
|
|
38
|
+
email: fields.email.trim(),
|
|
39
|
+
password: fields.password,
|
|
40
|
+
confirmPassword: fields.confirmPassword,
|
|
41
|
+
},
|
|
42
|
+
getErrorMessage,
|
|
43
|
+
DEFAULT_PASSWORD_CONFIG
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!validation.isValid) {
|
|
47
|
+
setFieldErrors(errorsToFieldErrors(validation.errors));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await signUp(fields.email.trim(), fields.password, fields.displayName.trim() || undefined);
|
|
53
|
+
|
|
54
|
+
if (translations) {
|
|
55
|
+
alertService.success(translations.successTitle, translations.signUpSuccess);
|
|
56
|
+
}
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
const localizationKey = getAuthErrorLocalizationKey(err);
|
|
59
|
+
setLocalError(getErrorMessage(localizationKey));
|
|
60
|
+
}
|
|
61
|
+
}, [fields, signUp, translations, getErrorMessage, clearFormErrors, setFieldErrors, setLocalError]);
|
|
62
|
+
|
|
63
|
+
return { handleSignUp };
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register Form Types
|
|
3
|
+
* Type definitions for register form hook
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type FieldErrors = {
|
|
7
|
+
displayName?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
confirmPassword?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface RegisterFormTranslations {
|
|
14
|
+
successTitle: string;
|
|
15
|
+
signUpSuccess: string;
|
|
16
|
+
errors: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseRegisterFormConfig {
|
|
20
|
+
translations: RegisterFormTranslations;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseRegisterFormResult {
|
|
24
|
+
displayName: string;
|
|
25
|
+
email: string;
|
|
26
|
+
password: string;
|
|
27
|
+
confirmPassword: string;
|
|
28
|
+
fieldErrors: FieldErrors;
|
|
29
|
+
localError: string | null;
|
|
30
|
+
loading: boolean;
|
|
31
|
+
passwordRequirements: { hasMinLength: boolean };
|
|
32
|
+
passwordsMatch: boolean;
|
|
33
|
+
handleDisplayNameChange: (text: string) => void;
|
|
34
|
+
handleEmailChange: (text: string) => void;
|
|
35
|
+
handlePasswordChange: (text: string) => void;
|
|
36
|
+
handleConfirmPasswordChange: (text: string) => void;
|
|
37
|
+
handleSignUp: () => Promise<void>;
|
|
38
|
+
displayError: string | null;
|
|
39
|
+
}
|
|
@@ -58,10 +58,13 @@ export const useProfileEdit = (
|
|
|
58
58
|
isValid: boolean;
|
|
59
59
|
errors: string[];
|
|
60
60
|
} => {
|
|
61
|
-
const result = validateProfileForm(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const result = validateProfileForm(
|
|
62
|
+
{
|
|
63
|
+
displayName: formState.displayName,
|
|
64
|
+
email: formState.email,
|
|
65
|
+
},
|
|
66
|
+
(key) => key // Pass-through function - returns key as-is since caller handles translation
|
|
67
|
+
);
|
|
65
68
|
|
|
66
69
|
return {
|
|
67
70
|
isValid: result.isValid,
|