@umituz/react-native-auth 4.3.62 → 4.3.63
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 +35 -0
- package/src/infrastructure/services/AuthEventService.ts +4 -3
- package/src/infrastructure/utils/calculators/authStateCalculator.ts +92 -0
- package/src/infrastructure/utils/calculators/formErrorCollection.ts +56 -0
- package/src/infrastructure/utils/calculators/index.ts +42 -0
- package/src/infrastructure/utils/calculators/passwordStrengthCalculator.ts +145 -0
- package/src/infrastructure/utils/calculators/userProfileCalculator.ts +77 -0
- package/src/presentation/components/LoginForm.tsx +5 -3
- package/src/presentation/components/RegisterForm.tsx +5 -3
- package/src/presentation/hooks/useAuth.ts +6 -19
- package/src/presentation/hooks/useAuthBottomSheet.ts +11 -1
- package/src/presentation/hooks/useAuthHandlers.ts +2 -2
- package/src/presentation/hooks/useLoginForm.ts +5 -6
- package/src/presentation/hooks/useRegisterForm.ts +1 -1
- package/src/presentation/hooks/useUserProfile.ts +5 -21
- package/src/presentation/stores/auth.selectors.ts +50 -12
- package/src/presentation/stores/authStore.ts +3 -0
- package/src/presentation/utils/form/usePasswordValidation.hook.ts +20 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.63",
|
|
4
4
|
"description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design with dependency injection, configurable validation, and comprehensive error handling.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,40 @@ export { isEmpty, isEmptyEmail, isEmptyPassword, isEmptyName, isNotEmpty, hasCon
|
|
|
39
39
|
export { safeCallback, safeCallbackSync } from './infrastructure/utils/safeCallback';
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// CALCULATOR UTILITIES
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
// Auth State Calculator
|
|
48
|
+
calculateUserId,
|
|
49
|
+
calculateHasFirebaseUser,
|
|
50
|
+
calculateIsAnonymous,
|
|
51
|
+
calculateIsAuthenticated,
|
|
52
|
+
calculateUserType,
|
|
53
|
+
calculateIsAuthReady,
|
|
54
|
+
calculateDerivedAuthState,
|
|
55
|
+
// Form Error Collection
|
|
56
|
+
collectFieldErrors,
|
|
57
|
+
extractFieldError,
|
|
58
|
+
hasFieldErrors,
|
|
59
|
+
getFirstErrorMessage,
|
|
60
|
+
// User Profile Calculator
|
|
61
|
+
calculateUserProfileDisplay,
|
|
62
|
+
calculateDisplayName,
|
|
63
|
+
hasUserAvatar,
|
|
64
|
+
getAvatarUrl,
|
|
65
|
+
// Password Strength Calculator
|
|
66
|
+
calculatePasswordRequirements,
|
|
67
|
+
calculatePasswordsMatch,
|
|
68
|
+
calculateConfirmationError,
|
|
69
|
+
calculatePasswordValidity,
|
|
70
|
+
calculatePasswordValidation,
|
|
71
|
+
hasMinLength,
|
|
72
|
+
calculatePasswordStrength,
|
|
73
|
+
} from './infrastructure/utils/calculators';
|
|
74
|
+
|
|
75
|
+
|
|
42
76
|
// =============================================================================
|
|
43
77
|
// PRESENTATION LAYER - Hooks
|
|
44
78
|
// =============================================================================
|
|
@@ -119,6 +153,7 @@ export {
|
|
|
119
153
|
selectUserType,
|
|
120
154
|
selectIsAuthReady,
|
|
121
155
|
selectFirebaseUserId,
|
|
156
|
+
selectAuthState,
|
|
122
157
|
} from './presentation/stores/auth.selectors';
|
|
123
158
|
|
|
124
159
|
// =============================================================================
|
|
@@ -58,11 +58,12 @@ class AuthEventService {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
addEventListener(event: string, listener: AuthEventListener): () => void {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
let eventListeners = this.listeners.get(event);
|
|
62
|
+
if (!eventListeners) {
|
|
63
|
+
eventListeners = [];
|
|
64
|
+
this.listeners.set(event, eventListeners);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const eventListeners = this.listeners.get(event)!;
|
|
66
67
|
eventListeners.push(listener);
|
|
67
68
|
|
|
68
69
|
// Return cleanup function
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth State Calculator
|
|
3
|
+
* Pure utility functions for deriving auth state from Firebase user
|
|
4
|
+
* These calculations are used in selectors and can be tested independently
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AuthUser } from "../../../domain/entities/AuthUser";
|
|
8
|
+
import type { UserType } from "../../../types/auth-store.types";
|
|
9
|
+
|
|
10
|
+
interface FirebaseUserLike {
|
|
11
|
+
uid: string;
|
|
12
|
+
isAnonymous: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AuthStateInput {
|
|
16
|
+
firebaseUser: FirebaseUserLike | null;
|
|
17
|
+
user: AuthUser | null;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
initialized: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculate user ID from Firebase user
|
|
25
|
+
*/
|
|
26
|
+
export function calculateUserId(firebaseUser: FirebaseUserLike | null): string | null {
|
|
27
|
+
return firebaseUser?.uid ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate if user has Firebase account
|
|
32
|
+
*/
|
|
33
|
+
export function calculateHasFirebaseUser(firebaseUser: FirebaseUserLike | null): boolean {
|
|
34
|
+
return !!firebaseUser;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calculate if user is anonymous
|
|
39
|
+
*/
|
|
40
|
+
export function calculateIsAnonymous(firebaseUser: FirebaseUserLike | null): boolean {
|
|
41
|
+
return firebaseUser?.isAnonymous ?? false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate if user is authenticated (has Firebase user, not anonymous)
|
|
46
|
+
*/
|
|
47
|
+
export function calculateIsAuthenticated(firebaseUser: FirebaseUserLike | null): boolean {
|
|
48
|
+
const hasFirebaseUser = !!firebaseUser;
|
|
49
|
+
const isNotAnonymous = !firebaseUser?.isAnonymous;
|
|
50
|
+
return hasFirebaseUser && isNotAnonymous;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate user type from Firebase user
|
|
55
|
+
*/
|
|
56
|
+
export function calculateUserType(firebaseUser: FirebaseUserLike | null): UserType {
|
|
57
|
+
if (!firebaseUser) {
|
|
58
|
+
return "none";
|
|
59
|
+
}
|
|
60
|
+
return firebaseUser.isAnonymous ? "anonymous" : "authenticated";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calculate if auth is ready (initialized and not loading)
|
|
65
|
+
*/
|
|
66
|
+
export function calculateIsAuthReady(initialized: boolean, loading: boolean): boolean {
|
|
67
|
+
return initialized && !loading;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Calculate all derived auth state at once
|
|
72
|
+
* More efficient than calling individual functions multiple times
|
|
73
|
+
*/
|
|
74
|
+
export function calculateDerivedAuthState(input: AuthStateInput): {
|
|
75
|
+
userId: string | null;
|
|
76
|
+
hasFirebaseUser: boolean;
|
|
77
|
+
isAnonymous: boolean;
|
|
78
|
+
isAuthenticated: boolean;
|
|
79
|
+
userType: UserType;
|
|
80
|
+
isAuthReady: boolean;
|
|
81
|
+
} {
|
|
82
|
+
const { firebaseUser, initialized, loading } = input;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
userId: calculateUserId(firebaseUser),
|
|
86
|
+
hasFirebaseUser: calculateHasFirebaseUser(firebaseUser),
|
|
87
|
+
isAnonymous: calculateIsAnonymous(firebaseUser),
|
|
88
|
+
isAuthenticated: calculateIsAuthenticated(firebaseUser),
|
|
89
|
+
userType: calculateUserType(firebaseUser),
|
|
90
|
+
isAuthReady: calculateIsAuthReady(initialized, loading),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Error Collection Utility
|
|
3
|
+
* Pure utility functions for collecting and extracting form errors
|
|
4
|
+
* Separates error collection logic from hooks for better testability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FormValidationError } from "../../../presentation/utils/form/validation/formValidation.types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collect field errors from validation result
|
|
11
|
+
* Extracts error messages for specific fields
|
|
12
|
+
*/
|
|
13
|
+
export function collectFieldErrors(
|
|
14
|
+
errors: FormValidationError[],
|
|
15
|
+
fields: string[]
|
|
16
|
+
): Record<string, string | null> {
|
|
17
|
+
const result: Record<string, string | null> = {};
|
|
18
|
+
|
|
19
|
+
for (const field of fields) {
|
|
20
|
+
const fieldError = errors.find((e) => e.field === field);
|
|
21
|
+
result[field] = fieldError?.message ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract single field error from validation result
|
|
29
|
+
*/
|
|
30
|
+
export function extractFieldError(
|
|
31
|
+
errors: FormValidationError[],
|
|
32
|
+
field: string
|
|
33
|
+
): string | null {
|
|
34
|
+
const fieldError = errors.find((e) => e.field === field);
|
|
35
|
+
return fieldError?.message ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if validation has errors for specific fields
|
|
40
|
+
*/
|
|
41
|
+
export function hasFieldErrors(
|
|
42
|
+
errors: FormValidationError[],
|
|
43
|
+
fields: string[]
|
|
44
|
+
): boolean {
|
|
45
|
+
return errors.some((error) => fields.includes(error.field));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get first error message from validation result
|
|
50
|
+
*/
|
|
51
|
+
export function getFirstErrorMessage(errors: FormValidationError[]): string | null {
|
|
52
|
+
if (errors.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return errors[0].message ?? null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculator Utilities Index
|
|
3
|
+
* Centralized exports for all calculator utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Auth State Calculator
|
|
7
|
+
export {
|
|
8
|
+
calculateUserId,
|
|
9
|
+
calculateHasFirebaseUser,
|
|
10
|
+
calculateIsAnonymous,
|
|
11
|
+
calculateIsAuthenticated,
|
|
12
|
+
calculateUserType,
|
|
13
|
+
calculateIsAuthReady,
|
|
14
|
+
calculateDerivedAuthState,
|
|
15
|
+
} from "./authStateCalculator";
|
|
16
|
+
|
|
17
|
+
// Form Error Collection
|
|
18
|
+
export {
|
|
19
|
+
collectFieldErrors,
|
|
20
|
+
extractFieldError,
|
|
21
|
+
hasFieldErrors,
|
|
22
|
+
getFirstErrorMessage,
|
|
23
|
+
} from "./formErrorCollection";
|
|
24
|
+
|
|
25
|
+
// User Profile Calculator
|
|
26
|
+
export {
|
|
27
|
+
calculateUserProfileDisplay,
|
|
28
|
+
calculateDisplayName,
|
|
29
|
+
hasUserAvatar,
|
|
30
|
+
getAvatarUrl,
|
|
31
|
+
} from "./userProfileCalculator";
|
|
32
|
+
|
|
33
|
+
// Password Strength Calculator
|
|
34
|
+
export {
|
|
35
|
+
calculatePasswordRequirements,
|
|
36
|
+
calculatePasswordsMatch,
|
|
37
|
+
calculateConfirmationError,
|
|
38
|
+
calculatePasswordValidity,
|
|
39
|
+
calculatePasswordValidation,
|
|
40
|
+
hasMinLength,
|
|
41
|
+
calculatePasswordStrength,
|
|
42
|
+
} from "./passwordStrengthCalculator";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Strength Calculator Utility
|
|
3
|
+
* Pure utility functions for password validation and strength calculation
|
|
4
|
+
* Separates password logic from hooks for better testability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
validatePasswordForRegister,
|
|
9
|
+
validatePasswordConfirmation,
|
|
10
|
+
} from "../AuthValidation";
|
|
11
|
+
import type { PasswordConfig } from "../../../domain/value-objects/AuthConfig";
|
|
12
|
+
import type { PasswordRequirements } from "../validation/types";
|
|
13
|
+
|
|
14
|
+
interface PasswordValidationInput {
|
|
15
|
+
password: string;
|
|
16
|
+
confirmPassword: string;
|
|
17
|
+
config?: PasswordConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PasswordValidationResult {
|
|
21
|
+
requirements: PasswordRequirements;
|
|
22
|
+
passwordsMatch: boolean;
|
|
23
|
+
isValid: boolean;
|
|
24
|
+
confirmationError: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate password requirements from validation result
|
|
29
|
+
*/
|
|
30
|
+
export function calculatePasswordRequirements(
|
|
31
|
+
password: string,
|
|
32
|
+
config?: PasswordConfig
|
|
33
|
+
): PasswordRequirements {
|
|
34
|
+
if (!password || !config) {
|
|
35
|
+
return { hasMinLength: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = validatePasswordForRegister(password, config);
|
|
39
|
+
return result.requirements;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calculate if passwords match
|
|
44
|
+
* Explicitly converts to boolean to avoid type issues
|
|
45
|
+
*/
|
|
46
|
+
export function calculatePasswordsMatch(
|
|
47
|
+
password: string,
|
|
48
|
+
confirmPassword: string
|
|
49
|
+
): boolean {
|
|
50
|
+
return !!(password && confirmPassword && password === confirmPassword);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate password confirmation error
|
|
55
|
+
*/
|
|
56
|
+
export function calculateConfirmationError(
|
|
57
|
+
password: string,
|
|
58
|
+
confirmPassword: string
|
|
59
|
+
): string | null {
|
|
60
|
+
if (!confirmPassword) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = validatePasswordConfirmation(password, confirmPassword);
|
|
65
|
+
return result.error ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculate overall password validity
|
|
70
|
+
*/
|
|
71
|
+
export function calculatePasswordValidity(
|
|
72
|
+
requirements: PasswordRequirements,
|
|
73
|
+
passwordsMatch: boolean
|
|
74
|
+
): boolean {
|
|
75
|
+
return requirements.hasMinLength && passwordsMatch;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Calculate all password validation state at once
|
|
80
|
+
* More efficient than calling individual functions
|
|
81
|
+
*/
|
|
82
|
+
export function calculatePasswordValidation(
|
|
83
|
+
input: PasswordValidationInput
|
|
84
|
+
): PasswordValidationResult {
|
|
85
|
+
const { password, confirmPassword, config } = input;
|
|
86
|
+
|
|
87
|
+
// Calculate password requirements
|
|
88
|
+
const requirements = calculatePasswordRequirements(password, config);
|
|
89
|
+
|
|
90
|
+
// Calculate if passwords match
|
|
91
|
+
const passwordsMatch = calculatePasswordsMatch(password, confirmPassword);
|
|
92
|
+
|
|
93
|
+
// Calculate confirmation error
|
|
94
|
+
const confirmationError = calculateConfirmationError(password, confirmPassword);
|
|
95
|
+
|
|
96
|
+
// Calculate overall validity
|
|
97
|
+
const isValid = calculatePasswordValidity(requirements, passwordsMatch);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
requirements,
|
|
101
|
+
passwordsMatch,
|
|
102
|
+
isValid,
|
|
103
|
+
confirmationError,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Quick check if password meets minimum length requirement
|
|
109
|
+
*/
|
|
110
|
+
export function hasMinLength(password: string, minLength: number): boolean {
|
|
111
|
+
return password.length >= minLength;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Calculate password strength score (0-100)
|
|
116
|
+
* Can be extended for more sophisticated strength calculation
|
|
117
|
+
*/
|
|
118
|
+
export function calculatePasswordStrength(
|
|
119
|
+
password: string,
|
|
120
|
+
requirements: PasswordRequirements
|
|
121
|
+
): number {
|
|
122
|
+
if (!password) return 0;
|
|
123
|
+
|
|
124
|
+
let score = 0;
|
|
125
|
+
|
|
126
|
+
// Base score for meeting minimum length
|
|
127
|
+
if (requirements.hasMinLength) {
|
|
128
|
+
score += 40;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Additional points for length
|
|
132
|
+
if (password.length >= 8) score += 20;
|
|
133
|
+
if (password.length >= 12) score += 20;
|
|
134
|
+
|
|
135
|
+
// Additional points for variety (can be extended)
|
|
136
|
+
const hasLower = /[a-z]/.test(password);
|
|
137
|
+
const hasUpper = /[A-Z]/.test(password);
|
|
138
|
+
const hasNumber = /[0-9]/.test(password);
|
|
139
|
+
const hasSpecial = /[^a-zA-Z0-9]/.test(password);
|
|
140
|
+
|
|
141
|
+
const varietyCount = [hasLower, hasUpper, hasNumber, hasSpecial].filter(Boolean).length;
|
|
142
|
+
score += varietyCount * 5;
|
|
143
|
+
|
|
144
|
+
return Math.min(score, 100);
|
|
145
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Profile Calculator Utility
|
|
3
|
+
* Pure utility functions for calculating user profile display data
|
|
4
|
+
* Separates profile calculation logic from hooks for better testability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AuthUser } from "../../../domain/entities/AuthUser";
|
|
8
|
+
|
|
9
|
+
interface UserProfileDisplayParams {
|
|
10
|
+
anonymousDisplayName?: string;
|
|
11
|
+
accountRoute?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UserProfileDisplayResult {
|
|
15
|
+
displayName: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
isAnonymous: boolean;
|
|
18
|
+
avatarUrl?: string;
|
|
19
|
+
accountSettingsRoute?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate user profile display data
|
|
24
|
+
* Handles anonymous vs authenticated user display logic
|
|
25
|
+
*/
|
|
26
|
+
export function calculateUserProfileDisplay(
|
|
27
|
+
user: AuthUser,
|
|
28
|
+
params: UserProfileDisplayParams = {}
|
|
29
|
+
): UserProfileDisplayResult {
|
|
30
|
+
const { anonymousDisplayName = "Anonymous User", accountRoute } = params;
|
|
31
|
+
|
|
32
|
+
if (user.isAnonymous) {
|
|
33
|
+
return {
|
|
34
|
+
displayName: anonymousDisplayName,
|
|
35
|
+
userId: user.uid,
|
|
36
|
+
isAnonymous: true,
|
|
37
|
+
accountSettingsRoute: accountRoute,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
displayName: user.displayName || user.email || anonymousDisplayName,
|
|
43
|
+
userId: user.uid,
|
|
44
|
+
isAnonymous: false,
|
|
45
|
+
avatarUrl: user.photoURL || undefined,
|
|
46
|
+
accountSettingsRoute: accountRoute,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get display name for user
|
|
52
|
+
* Extracts display name calculation for reusability
|
|
53
|
+
*/
|
|
54
|
+
export function calculateDisplayName(
|
|
55
|
+
user: AuthUser,
|
|
56
|
+
anonymousDisplayName: string = "Anonymous User"
|
|
57
|
+
): string {
|
|
58
|
+
if (user.isAnonymous) {
|
|
59
|
+
return anonymousDisplayName;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return user.displayName || user.email || anonymousDisplayName;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if user has avatar
|
|
67
|
+
*/
|
|
68
|
+
export function hasUserAvatar(user: AuthUser): boolean {
|
|
69
|
+
return !!user.photoURL;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get avatar URL if available
|
|
74
|
+
*/
|
|
75
|
+
export function getAvatarUrl(user: AuthUser): string | undefined {
|
|
76
|
+
return user.photoURL || undefined;
|
|
77
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef } from "react";
|
|
1
|
+
import React, { useRef, memo } from "react";
|
|
2
2
|
import { StyleSheet, TextInput } from "react-native";
|
|
3
3
|
import { AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useLoginForm } from "../hooks/useLoginForm";
|
|
@@ -22,7 +22,7 @@ interface LoginFormProps {
|
|
|
22
22
|
onNavigateToRegister: () => void;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export const LoginForm
|
|
25
|
+
export const LoginForm = memo<LoginFormProps>(({
|
|
26
26
|
translations,
|
|
27
27
|
onNavigateToRegister,
|
|
28
28
|
}) => {
|
|
@@ -84,7 +84,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|
|
84
84
|
/>
|
|
85
85
|
</>
|
|
86
86
|
);
|
|
87
|
-
};
|
|
87
|
+
});
|
|
88
88
|
|
|
89
89
|
const styles = StyleSheet.create({
|
|
90
90
|
signInButton: {
|
|
@@ -92,3 +92,5 @@ const styles = StyleSheet.create({
|
|
|
92
92
|
marginBottom: 16,
|
|
93
93
|
},
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
LoginForm.displayName = 'LoginForm';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef } from "react";
|
|
1
|
+
import React, { useRef, memo } from "react";
|
|
2
2
|
import { StyleSheet, TextInput } from "react-native";
|
|
3
3
|
import { AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useRegisterForm } from "../hooks/useRegisterForm";
|
|
@@ -38,7 +38,7 @@ interface RegisterFormProps {
|
|
|
38
38
|
onPrivacyPress?: () => void;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export const RegisterForm
|
|
41
|
+
export const RegisterForm = memo<RegisterFormProps>(({
|
|
42
42
|
translations,
|
|
43
43
|
onNavigateToLogin,
|
|
44
44
|
termsUrl,
|
|
@@ -149,10 +149,12 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
149
149
|
/>
|
|
150
150
|
</>
|
|
151
151
|
);
|
|
152
|
-
};
|
|
152
|
+
});
|
|
153
153
|
|
|
154
154
|
const styles = StyleSheet.create({
|
|
155
155
|
passwordInput: { marginBottom: 4 },
|
|
156
156
|
confirmPasswordInput: { marginBottom: 4 },
|
|
157
157
|
signUpButton: { minHeight: 52, marginBottom: 16, marginTop: 8 },
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
RegisterForm.displayName = 'RegisterForm';
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAuth Hook
|
|
3
3
|
* React hook for authentication state management
|
|
4
|
+
* PERFORMANCE: Uses single batch selector to minimize re-renders
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { useCallback } from "react";
|
|
7
8
|
import { useAuthStore } from "../stores/authStore";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
selectLoading,
|
|
11
|
-
selectError,
|
|
10
|
+
selectAuthState,
|
|
12
11
|
selectSetLoading,
|
|
13
12
|
selectSetError,
|
|
14
|
-
selectIsAuthenticated,
|
|
15
|
-
selectHasFirebaseUser,
|
|
16
|
-
selectUserId,
|
|
17
|
-
selectUserType,
|
|
18
|
-
selectIsAnonymous,
|
|
19
|
-
selectIsAuthReady,
|
|
20
13
|
} from "../stores/auth.selectors";
|
|
21
14
|
import type { UserType } from "../../types/auth-store.types";
|
|
22
15
|
import {
|
|
@@ -45,15 +38,9 @@ export interface UseAuthResult {
|
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
export function useAuth(): UseAuthResult {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const isAuthenticated = useAuthStore(selectIsAuthenticated);
|
|
52
|
-
const hasFirebaseUser = useAuthStore(selectHasFirebaseUser);
|
|
53
|
-
const userId = useAuthStore(selectUserId);
|
|
54
|
-
const userType = useAuthStore(selectUserType);
|
|
55
|
-
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
56
|
-
const isAuthReady = useAuthStore(selectIsAuthReady);
|
|
41
|
+
// PERFORMANCE: Single batch selector instead of 10 separate selectors
|
|
42
|
+
// This reduces re-renders from 10x to 1x when auth state changes
|
|
43
|
+
const authState = useAuthStore(selectAuthState);
|
|
57
44
|
const setLoading = useAuthStore(selectSetLoading);
|
|
58
45
|
const setError = useAuthStore(selectSetError);
|
|
59
46
|
|
|
@@ -124,7 +111,7 @@ export function useAuth(): UseAuthResult {
|
|
|
124
111
|
}, [setLoading, setError, anonymousModeMutation.mutateAsync]);
|
|
125
112
|
|
|
126
113
|
return {
|
|
127
|
-
|
|
114
|
+
...authState,
|
|
128
115
|
signUp, signIn, signOut, continueAnonymously, setError,
|
|
129
116
|
};
|
|
130
117
|
}
|
|
@@ -38,9 +38,19 @@ export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
|
38
38
|
const { signInWithApple, appleAvailable } = useAppleAuth();
|
|
39
39
|
|
|
40
40
|
// Determine enabled providers
|
|
41
|
+
// PERFORMANCE: Memoize to prevent recalculation when config hasn't changed
|
|
41
42
|
const providers = useMemo<SocialAuthProvider[]>(() => {
|
|
42
43
|
return determineEnabledProviders(socialConfig, appleAvailable, googleConfigured);
|
|
43
|
-
}, [
|
|
44
|
+
}, [
|
|
45
|
+
// Use deep comparison for socialConfig to avoid unnecessary recalculation
|
|
46
|
+
// when parent passes a new object reference with same values
|
|
47
|
+
socialConfig?.google?.iosClientId,
|
|
48
|
+
socialConfig?.google?.webClientId,
|
|
49
|
+
socialConfig?.google?.androidClientId,
|
|
50
|
+
socialConfig?.apple?.enabled,
|
|
51
|
+
appleAvailable,
|
|
52
|
+
googleConfigured,
|
|
53
|
+
]);
|
|
44
54
|
|
|
45
55
|
// Social auth loading states
|
|
46
56
|
const [googleLoading, setGoogleLoading] = useState(false);
|
|
@@ -56,7 +56,7 @@ export const useAuthHandlers = (appInfo: AuthHandlersAppInfo, translations?: Aut
|
|
|
56
56
|
}
|
|
57
57
|
await Linking.openURL(url);
|
|
58
58
|
} catch (error) {
|
|
59
|
-
if (
|
|
59
|
+
if (__DEV__) {
|
|
60
60
|
console.error("[useAuthHandlers] Failed to open app store:", error);
|
|
61
61
|
}
|
|
62
62
|
Alert.alert(translations?.common || "", translations?.failedToOpenAppStore || "");
|
|
@@ -67,7 +67,7 @@ export const useAuthHandlers = (appInfo: AuthHandlersAppInfo, translations?: Aut
|
|
|
67
67
|
try {
|
|
68
68
|
await signOut();
|
|
69
69
|
} catch (error) {
|
|
70
|
-
if (
|
|
70
|
+
if (__DEV__) {
|
|
71
71
|
console.error("[useAuthHandlers] Sign out failed:", error);
|
|
72
72
|
}
|
|
73
73
|
AlertService.createErrorAlert(
|
|
@@ -6,6 +6,7 @@ import { useFormFields } from "../utils/form/useFormField.hook";
|
|
|
6
6
|
import { sanitizeEmail } from "../../infrastructure/utils/validation/sanitization";
|
|
7
7
|
import { useAuthErrorHandler } from "./useAuthErrorHandler";
|
|
8
8
|
import { useLocalError } from "./useLocalError";
|
|
9
|
+
import { extractFieldError } from "../../infrastructure/utils/calculators/formErrorCollection";
|
|
9
10
|
|
|
10
11
|
interface LoginFormTranslations {
|
|
11
12
|
successTitle: string;
|
|
@@ -44,7 +45,7 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
44
45
|
setEmailError(null);
|
|
45
46
|
setPasswordError(null);
|
|
46
47
|
setLocalError(null);
|
|
47
|
-
}, [setLocalError]);
|
|
48
|
+
}, [setLocalError, setEmailError, setPasswordError]);
|
|
48
49
|
|
|
49
50
|
const { fields, updateField } = useFormFields(
|
|
50
51
|
{ email: "", password: "" },
|
|
@@ -79,11 +80,9 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
79
80
|
);
|
|
80
81
|
|
|
81
82
|
if (!validation.isValid) {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
setEmailError(emailErrorMsg);
|
|
86
|
-
setPasswordError(passwordErrorMsg);
|
|
83
|
+
// Use utility to collect field errors
|
|
84
|
+
setEmailError(extractFieldError(validation.errors, "email"));
|
|
85
|
+
setPasswordError(extractFieldError(validation.errors, "password"));
|
|
87
86
|
return;
|
|
88
87
|
}
|
|
89
88
|
|
|
@@ -32,7 +32,7 @@ export function useRegisterForm(config?: UseRegisterFormConfig): UseRegisterForm
|
|
|
32
32
|
const clearFormErrors = useCallback(() => {
|
|
33
33
|
setLocalError(null);
|
|
34
34
|
setFieldErrors({});
|
|
35
|
-
}, []);
|
|
35
|
+
}, [setLocalError, setFieldErrors]);
|
|
36
36
|
|
|
37
37
|
const { fields, updateField } = useFormFields(
|
|
38
38
|
{
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useUserProfile Hook
|
|
3
3
|
* Returns profile data for display in settings or profile screens
|
|
4
|
+
* Uses userProfileCalculator utility for calculation logic
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { useMemo } from "react";
|
|
7
8
|
import { useAuth } from "./useAuth";
|
|
9
|
+
import { calculateUserProfileDisplay } from "../../infrastructure/utils/calculators/userProfileCalculator";
|
|
8
10
|
|
|
9
11
|
export interface UserProfileData {
|
|
10
12
|
displayName?: string;
|
|
@@ -24,29 +26,11 @@ export const useUserProfile = (
|
|
|
24
26
|
params?: UseUserProfileParams,
|
|
25
27
|
): UserProfileData | undefined => {
|
|
26
28
|
const { user } = useAuth();
|
|
27
|
-
const anonymousName = params?.anonymousDisplayName ?? "Anonymous User";
|
|
28
|
-
const accountRoute = params?.accountRoute;
|
|
29
29
|
|
|
30
30
|
return useMemo(() => {
|
|
31
31
|
if (!user) return undefined;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
displayName: anonymousName,
|
|
38
|
-
userId: user.uid,
|
|
39
|
-
isAnonymous: true,
|
|
40
|
-
accountSettingsRoute: accountRoute,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
accountSettingsRoute: accountRoute,
|
|
46
|
-
displayName: user.displayName || user.email || anonymousName,
|
|
47
|
-
userId: user.uid,
|
|
48
|
-
isAnonymous: false,
|
|
49
|
-
avatarUrl: user.photoURL || undefined,
|
|
50
|
-
};
|
|
51
|
-
}, [user, anonymousName, accountRoute]);
|
|
33
|
+
// Delegate to utility function
|
|
34
|
+
return calculateUserProfileDisplay(user, params);
|
|
35
|
+
}, [user, params]);
|
|
52
36
|
};
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Store Selectors
|
|
3
3
|
* Pure functions for deriving state from auth store
|
|
4
|
+
* Uses authStateCalculator for derived state calculations
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { AuthState, AuthActions, UserType } from "../../types/auth-store.types";
|
|
7
8
|
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
9
|
+
import {
|
|
10
|
+
calculateUserId,
|
|
11
|
+
calculateHasFirebaseUser,
|
|
12
|
+
calculateIsAnonymous,
|
|
13
|
+
calculateIsAuthenticated,
|
|
14
|
+
calculateUserType,
|
|
15
|
+
calculateIsAuthReady,
|
|
16
|
+
calculateDerivedAuthState,
|
|
17
|
+
} from "../../infrastructure/utils/calculators/authStateCalculator";
|
|
8
18
|
|
|
9
19
|
// Combined store type for selectors
|
|
10
20
|
type AuthStore = AuthState & AuthActions;
|
|
@@ -63,13 +73,15 @@ export const selectShowAuthModal = (state: { showAuthModal: (callback?: () => vo
|
|
|
63
73
|
// =============================================================================
|
|
64
74
|
// DERIVED SELECTORS
|
|
65
75
|
// =============================================================================
|
|
76
|
+
// Note: These selectors delegate to authStateCalculator utilities
|
|
77
|
+
// for better separation of concerns and testability
|
|
66
78
|
|
|
67
79
|
/**
|
|
68
80
|
* Get current user ID
|
|
69
81
|
* Uses firebaseUser as single source of truth
|
|
70
82
|
*/
|
|
71
83
|
export const selectUserId = (state: AuthStore): string | null => {
|
|
72
|
-
return state.firebaseUser
|
|
84
|
+
return calculateUserId(state.firebaseUser);
|
|
73
85
|
};
|
|
74
86
|
|
|
75
87
|
/**
|
|
@@ -77,13 +89,11 @@ export const selectUserId = (state: AuthStore): string | null => {
|
|
|
77
89
|
* Uses firebaseUser as single source of truth
|
|
78
90
|
*/
|
|
79
91
|
export const selectIsAuthenticated = (state: AuthStore): boolean => {
|
|
80
|
-
|
|
81
|
-
const isNotAnonymous = !state.firebaseUser?.isAnonymous;
|
|
82
|
-
return hasFirebaseUser && isNotAnonymous;
|
|
92
|
+
return calculateIsAuthenticated(state.firebaseUser);
|
|
83
93
|
};
|
|
84
94
|
|
|
85
95
|
export const selectHasFirebaseUser = (state: AuthStore): boolean => {
|
|
86
|
-
return
|
|
96
|
+
return calculateHasFirebaseUser(state.firebaseUser);
|
|
87
97
|
};
|
|
88
98
|
|
|
89
99
|
/**
|
|
@@ -91,7 +101,7 @@ export const selectHasFirebaseUser = (state: AuthStore): boolean => {
|
|
|
91
101
|
* Uses firebaseUser as single source of truth
|
|
92
102
|
*/
|
|
93
103
|
export const selectIsAnonymous = (state: AuthStore): boolean => {
|
|
94
|
-
return state.firebaseUser
|
|
104
|
+
return calculateIsAnonymous(state.firebaseUser);
|
|
95
105
|
};
|
|
96
106
|
|
|
97
107
|
/**
|
|
@@ -99,17 +109,45 @@ export const selectIsAnonymous = (state: AuthStore): boolean => {
|
|
|
99
109
|
* Derived from firebaseUser state
|
|
100
110
|
*/
|
|
101
111
|
export const selectUserType = (state: AuthStore): UserType => {
|
|
102
|
-
|
|
103
|
-
return "none";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return state.firebaseUser.isAnonymous ? "anonymous" : "authenticated";
|
|
112
|
+
return calculateUserType(state.firebaseUser);
|
|
107
113
|
};
|
|
108
114
|
|
|
109
115
|
/**
|
|
110
116
|
* Check if auth is ready (initialized and not loading)
|
|
111
117
|
*/
|
|
112
118
|
export const selectIsAuthReady = (state: AuthStore): boolean => {
|
|
113
|
-
return state.initialized
|
|
119
|
+
return calculateIsAuthReady(state.initialized, state.loading);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Batch selector - get all auth state at once to minimize re-renders
|
|
124
|
+
* More efficient than calling selectors individually
|
|
125
|
+
*/
|
|
126
|
+
export const selectAuthState = (state: AuthStore): {
|
|
127
|
+
user: AuthUser | null;
|
|
128
|
+
userId: string | null;
|
|
129
|
+
userType: UserType;
|
|
130
|
+
loading: boolean;
|
|
131
|
+
isAuthReady: boolean;
|
|
132
|
+
isAnonymous: boolean;
|
|
133
|
+
isAuthenticated: boolean;
|
|
134
|
+
hasFirebaseUser: boolean;
|
|
135
|
+
error: string | null;
|
|
136
|
+
} => {
|
|
137
|
+
// Use calculateDerivedAuthState for batch calculation efficiency
|
|
138
|
+
const derivedState = calculateDerivedAuthState({
|
|
139
|
+
firebaseUser: state.firebaseUser,
|
|
140
|
+
user: state.user,
|
|
141
|
+
loading: state.loading,
|
|
142
|
+
initialized: state.initialized,
|
|
143
|
+
error: state.error,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
user: selectUser(state),
|
|
148
|
+
loading: selectLoading(state),
|
|
149
|
+
error: selectError(state),
|
|
150
|
+
...derivedState,
|
|
151
|
+
};
|
|
114
152
|
};
|
|
115
153
|
|
|
@@ -28,9 +28,12 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
|
28
28
|
removeItem: (name) => storageService.removeItem(name),
|
|
29
29
|
},
|
|
30
30
|
version: 2,
|
|
31
|
+
// PERFORMANCE: Only persist essential flags
|
|
32
|
+
// Firebase handles user persistence, we only need anonymous mode and initialized state
|
|
31
33
|
partialize: (state) => ({
|
|
32
34
|
isAnonymous: state.isAnonymous,
|
|
33
35
|
initialized: state.initialized,
|
|
36
|
+
loading: false, // Always restore as false to prevent loading state on app restart
|
|
34
37
|
}),
|
|
35
38
|
migrate: (persistedState: unknown) => {
|
|
36
39
|
const state = (persistedState && typeof persistedState === "object" ? persistedState : {}) as Partial<AuthState>;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Password Validation Hook
|
|
3
3
|
* Provides reusable password validation logic with requirements tracking
|
|
4
|
+
* PERFORMANCE: Single useMemo using passwordStrengthCalculator utility
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { useMemo } from "react";
|
|
7
|
-
import {
|
|
8
|
-
validatePasswordForRegister,
|
|
9
|
-
validatePasswordConfirmation,
|
|
10
|
-
} from "../../../infrastructure/utils/AuthValidation";
|
|
11
8
|
import type { PasswordRequirements } from "../../../infrastructure/utils/validation/types";
|
|
12
9
|
import type { PasswordConfig } from "../../../domain/value-objects/AuthConfig";
|
|
10
|
+
import { calculatePasswordValidation } from "../../../infrastructure/utils/calculators/passwordStrengthCalculator";
|
|
13
11
|
|
|
14
12
|
interface UsePasswordValidationResult {
|
|
15
13
|
passwordRequirements: PasswordRequirements;
|
|
@@ -34,40 +32,23 @@ export function usePasswordValidation(
|
|
|
34
32
|
confirmPassword: string,
|
|
35
33
|
options?: UsePasswordValidationOptions
|
|
36
34
|
): UsePasswordValidationResult {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}, [password, confirmPassword]);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!confirmPassword) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const result = validatePasswordConfirmation(password, confirmPassword);
|
|
59
|
-
return result.error ?? null;
|
|
60
|
-
}, [password, confirmPassword]);
|
|
61
|
-
|
|
62
|
-
const isValid = useMemo(() => {
|
|
63
|
-
return passwordRequirements.hasMinLength && passwordsMatch;
|
|
64
|
-
}, [passwordRequirements, passwordsMatch]);
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
passwordRequirements,
|
|
68
|
-
passwordsMatch,
|
|
69
|
-
isValid,
|
|
70
|
-
confirmationError,
|
|
71
|
-
};
|
|
35
|
+
// PERFORMANCE: Use utility function for batch calculation
|
|
36
|
+
const result = useMemo(() => {
|
|
37
|
+
const validation = calculatePasswordValidation({
|
|
38
|
+
password,
|
|
39
|
+
confirmPassword,
|
|
40
|
+
config: options?.passwordConfig,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Map to expected return type
|
|
44
|
+
return {
|
|
45
|
+
passwordRequirements: validation.requirements,
|
|
46
|
+
passwordsMatch: validation.passwordsMatch,
|
|
47
|
+
isValid: validation.isValid,
|
|
48
|
+
confirmationError: validation.confirmationError,
|
|
49
|
+
};
|
|
50
|
+
}, [password, confirmPassword, options?.passwordConfig]);
|
|
51
|
+
|
|
52
|
+
return result;
|
|
72
53
|
}
|
|
73
54
|
|