@umituz/react-native-auth 3.4.21 → 3.4.23
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/__tests__/services/AnonymousModeService.test.ts +22 -153
- package/src/__tests__/services/AuthCoreInitialization.test.ts +45 -0
- package/src/__tests__/services/AuthCoreOperations.test.ts +71 -0
- package/src/__tests__/services/AuthPackage.test.ts +24 -171
- package/src/__tests__/utils/AuthDisplayNameValidation.test.ts +44 -0
- package/src/__tests__/utils/AuthEmailValidation.test.ts +38 -0
- package/src/__tests__/utils/AuthPasswordValidation.test.ts +90 -0
- package/src/index.ts +12 -99
- package/src/infrastructure/services/UserDocument.types.ts +44 -0
- package/src/infrastructure/services/UserDocumentService.ts +33 -106
- package/src/infrastructure/utils/AuthValidation.ts +37 -156
- package/src/presentation/components/AuthBottomSheet.tsx +10 -10
- package/src/presentation/hooks/useAuthBottomSheet.ts +54 -16
- package/src/__tests__/services/AuthCoreService.test.ts +0 -247
- package/src/__tests__/utils/AuthValidation.test.ts +0 -270
- package/src/presentation/components/AuthBottomSheetWrapper.tsx +0 -63
- package/src/presentation/hooks/useAuthBottomSheetWrapper.ts +0 -70
|
@@ -1,38 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth Validation Utilities
|
|
3
|
-
* Single Responsibility: Email and password validation for authentication
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { PasswordConfig } from "../../domain/value-objects/AuthConfig";
|
|
7
2
|
import { getAuthPackage } from "../services/AuthPackage";
|
|
8
3
|
|
|
9
|
-
export interface ValidationResult {
|
|
10
|
-
|
|
11
|
-
error?: string; // This should be a localization key
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface PasswordStrengthResult extends ValidationResult {
|
|
15
|
-
requirements: PasswordRequirements;
|
|
16
|
-
}
|
|
17
|
-
|
|
4
|
+
export interface ValidationResult { isValid: boolean; error?: string; }
|
|
5
|
+
export interface PasswordStrengthResult extends ValidationResult { requirements: PasswordRequirements; }
|
|
18
6
|
export interface PasswordRequirements {
|
|
19
|
-
hasMinLength: boolean;
|
|
20
|
-
hasUppercase: boolean;
|
|
21
|
-
hasLowercase: boolean;
|
|
22
|
-
hasNumber: boolean;
|
|
23
|
-
hasSpecialChar: boolean;
|
|
7
|
+
hasMinLength: boolean; hasUppercase: boolean; hasLowercase: boolean; hasNumber: boolean; hasSpecialChar: boolean;
|
|
24
8
|
}
|
|
25
|
-
|
|
26
9
|
export interface ValidationConfig {
|
|
27
|
-
emailRegex: RegExp;
|
|
28
|
-
|
|
29
|
-
lowercaseRegex: RegExp;
|
|
30
|
-
numberRegex: RegExp;
|
|
31
|
-
specialCharRegex: RegExp;
|
|
32
|
-
displayNameMinLength: number;
|
|
10
|
+
emailRegex: RegExp; uppercaseRegex: RegExp; lowercaseRegex: RegExp;
|
|
11
|
+
numberRegex: RegExp; specialCharRegex: RegExp; displayNameMinLength: number;
|
|
33
12
|
}
|
|
34
13
|
|
|
35
|
-
const
|
|
14
|
+
const DEFAULT_VAL_CONFIG: ValidationConfig = {
|
|
36
15
|
emailRegex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
37
16
|
uppercaseRegex: /[A-Z]/,
|
|
38
17
|
lowercaseRegex: /[a-z]/,
|
|
@@ -41,155 +20,57 @@ const DEFAULT_VALIDATION_CONFIG: ValidationConfig = {
|
|
|
41
20
|
displayNameMinLength: 2,
|
|
42
21
|
};
|
|
43
22
|
|
|
44
|
-
function
|
|
45
|
-
const
|
|
23
|
+
function getValConfig(): ValidationConfig {
|
|
24
|
+
const p = getAuthPackage()?.getConfig();
|
|
46
25
|
return {
|
|
47
|
-
emailRegex:
|
|
48
|
-
uppercaseRegex:
|
|
49
|
-
lowercaseRegex:
|
|
50
|
-
numberRegex:
|
|
51
|
-
specialCharRegex:
|
|
52
|
-
displayNameMinLength:
|
|
26
|
+
emailRegex: p?.validation.emailRegex || DEFAULT_VAL_CONFIG.emailRegex,
|
|
27
|
+
uppercaseRegex: DEFAULT_VAL_CONFIG.uppercaseRegex,
|
|
28
|
+
lowercaseRegex: DEFAULT_VAL_CONFIG.lowercaseRegex,
|
|
29
|
+
numberRegex: DEFAULT_VAL_CONFIG.numberRegex,
|
|
30
|
+
specialCharRegex: DEFAULT_VAL_CONFIG.specialCharRegex,
|
|
31
|
+
displayNameMinLength: DEFAULT_VAL_CONFIG.displayNameMinLength,
|
|
53
32
|
};
|
|
54
33
|
}
|
|
55
34
|
|
|
56
|
-
/**
|
|
57
|
-
* Validate email format
|
|
58
|
-
*/
|
|
59
35
|
export function validateEmail(email: string): ValidationResult {
|
|
60
|
-
if (!email || email.trim() === "") {
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const config = getValidationConfig();
|
|
65
|
-
if (!config.emailRegex.test(email.trim())) {
|
|
66
|
-
return { isValid: false, error: "auth.validation.invalidEmail" };
|
|
67
|
-
}
|
|
68
|
-
|
|
36
|
+
if (!email || email.trim() === "") return { isValid: false, error: "auth.validation.emailRequired" };
|
|
37
|
+
if (!getValConfig().emailRegex.test(email.trim())) return { isValid: false, error: "auth.validation.invalidEmail" };
|
|
69
38
|
return { isValid: true };
|
|
70
39
|
}
|
|
71
40
|
|
|
72
|
-
/**
|
|
73
|
-
* Validate password for login - only checks if password is provided
|
|
74
|
-
* No strength requirements for login (existing users may have old passwords)
|
|
75
|
-
*/
|
|
76
41
|
export function validatePasswordForLogin(password: string): ValidationResult {
|
|
77
|
-
if (!password || password.length === 0) {
|
|
78
|
-
return { isValid: false, error: "auth.validation.passwordRequired" };
|
|
79
|
-
}
|
|
80
|
-
|
|
42
|
+
if (!password || password.length === 0) return { isValid: false, error: "auth.validation.passwordRequired" };
|
|
81
43
|
return { isValid: true };
|
|
82
44
|
}
|
|
83
45
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
export function validatePasswordForRegister(
|
|
89
|
-
password: string,
|
|
90
|
-
config: PasswordConfig
|
|
91
|
-
): PasswordStrengthResult {
|
|
92
|
-
const validationConfig = getValidationConfig();
|
|
93
|
-
const requirements: PasswordRequirements = {
|
|
46
|
+
export function validatePasswordForRegister(password: string, config: PasswordConfig): PasswordStrengthResult {
|
|
47
|
+
const v = getValConfig();
|
|
48
|
+
const req: PasswordRequirements = {
|
|
94
49
|
hasMinLength: password.length >= config.minLength,
|
|
95
|
-
hasUppercase: !config.requireUppercase ||
|
|
96
|
-
hasLowercase: !config.requireLowercase ||
|
|
97
|
-
hasNumber: !config.requireNumber ||
|
|
98
|
-
hasSpecialChar:
|
|
99
|
-
!config.requireSpecialChar || validationConfig.specialCharRegex.test(password),
|
|
50
|
+
hasUppercase: !config.requireUppercase || v.uppercaseRegex.test(password),
|
|
51
|
+
hasLowercase: !config.requireLowercase || v.lowercaseRegex.test(password),
|
|
52
|
+
hasNumber: !config.requireNumber || v.numberRegex.test(password),
|
|
53
|
+
hasSpecialChar: !config.requireSpecialChar || v.specialCharRegex.test(password),
|
|
100
54
|
};
|
|
101
55
|
|
|
102
|
-
if (!password
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!requirements.hasMinLength) {
|
|
111
|
-
return {
|
|
112
|
-
isValid: false,
|
|
113
|
-
error: "auth.validation.passwordTooShort",
|
|
114
|
-
requirements,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (config.requireUppercase && !validationConfig.uppercaseRegex.test(password)) {
|
|
119
|
-
return {
|
|
120
|
-
isValid: false,
|
|
121
|
-
error: "auth.validation.passwordRequireUppercase",
|
|
122
|
-
requirements,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
56
|
+
if (!password) return { isValid: false, error: "auth.validation.passwordRequired", requirements: req };
|
|
57
|
+
if (!req.hasMinLength) return { isValid: false, error: "auth.validation.passwordTooShort", requirements: req };
|
|
58
|
+
if (config.requireUppercase && !req.hasUppercase) return { isValid: false, error: "auth.validation.passwordRequireUppercase", requirements: req };
|
|
59
|
+
if (config.requireLowercase && !req.hasLowercase) return { isValid: false, error: "auth.validation.passwordRequireLowercase", requirements: req };
|
|
60
|
+
if (config.requireNumber && !req.hasNumber) return { isValid: false, error: "auth.validation.passwordRequireNumber", requirements: req };
|
|
61
|
+
if (config.requireSpecialChar && !req.hasSpecialChar) return { isValid: false, error: "auth.validation.passwordRequireSpecialChar", requirements: req };
|
|
125
62
|
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
isValid: false,
|
|
129
|
-
error: "auth.validation.passwordRequireLowercase",
|
|
130
|
-
requirements,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (config.requireNumber && !validationConfig.numberRegex.test(password)) {
|
|
135
|
-
return {
|
|
136
|
-
isValid: false,
|
|
137
|
-
error: "auth.validation.passwordRequireNumber",
|
|
138
|
-
requirements,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (config.requireSpecialChar && !validationConfig.specialCharRegex.test(password)) {
|
|
143
|
-
return {
|
|
144
|
-
isValid: false,
|
|
145
|
-
error: "auth.validation.passwordRequireSpecialChar",
|
|
146
|
-
requirements,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { isValid: true, requirements };
|
|
63
|
+
return { isValid: true, requirements: req };
|
|
151
64
|
}
|
|
152
65
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
export function validatePasswordConfirmation(
|
|
157
|
-
password: string,
|
|
158
|
-
confirmPassword: string
|
|
159
|
-
): ValidationResult {
|
|
160
|
-
if (!confirmPassword) {
|
|
161
|
-
return { isValid: false, error: "auth.validation.confirmPasswordRequired" };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (password !== confirmPassword) {
|
|
165
|
-
return { isValid: false, error: "auth.validation.passwordsDoNotMatch" };
|
|
166
|
-
}
|
|
167
|
-
|
|
66
|
+
export function validatePasswordConfirmation(password: string, confirm: string): ValidationResult {
|
|
67
|
+
if (!confirm) return { isValid: false, error: "auth.validation.confirmPasswordRequired" };
|
|
68
|
+
if (password !== confirm) return { isValid: false, error: "auth.validation.passwordsDoNotMatch" };
|
|
168
69
|
return { isValid: true };
|
|
169
70
|
}
|
|
170
71
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
export function validateDisplayName(
|
|
175
|
-
displayName: string,
|
|
176
|
-
minLength?: number
|
|
177
|
-
): ValidationResult {
|
|
178
|
-
if (!displayName || displayName.trim() === "") {
|
|
179
|
-
return { isValid: false, error: "auth.validation.nameRequired" };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const config = getValidationConfig();
|
|
183
|
-
const actualMinLength = minLength ?? config.displayNameMinLength;
|
|
184
|
-
|
|
185
|
-
if (displayName.trim().length < actualMinLength) {
|
|
186
|
-
return {
|
|
187
|
-
isValid: false,
|
|
188
|
-
error: "auth.validation.nameTooShort",
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
72
|
+
export function validateDisplayName(name: string, minLength?: number): ValidationResult {
|
|
73
|
+
if (!name || name.trim() === "") return { isValid: false, error: "auth.validation.nameRequired" };
|
|
74
|
+
if (name.trim().length < (minLength ?? getValConfig().displayNameMinLength)) return { isValid: false, error: "auth.validation.nameTooShort" };
|
|
192
75
|
return { isValid: true };
|
|
193
76
|
}
|
|
194
|
-
|
|
195
|
-
|
|
@@ -13,23 +13,22 @@ import {
|
|
|
13
13
|
BottomSheetModal,
|
|
14
14
|
} from "@umituz/react-native-design-system";
|
|
15
15
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
16
|
-
import { useAuthBottomSheet } from "../hooks/useAuthBottomSheet";
|
|
16
|
+
import { useAuthBottomSheet, type SocialAuthConfiguration } from "../hooks/useAuthBottomSheet";
|
|
17
17
|
import { LoginForm } from "./LoginForm";
|
|
18
18
|
import { RegisterForm } from "./RegisterForm";
|
|
19
19
|
import { SocialLoginButtons } from "./SocialLoginButtons";
|
|
20
20
|
import { styles } from "./AuthBottomSheet.styles";
|
|
21
|
-
import type { SocialAuthProvider } from "../../domain/value-objects/AuthConfig";
|
|
22
21
|
|
|
23
22
|
export interface AuthBottomSheetProps {
|
|
24
23
|
termsUrl?: string;
|
|
25
24
|
privacyUrl?: string;
|
|
26
25
|
onTermsPress?: () => void;
|
|
27
26
|
onPrivacyPress?: () => void;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/** Called when Google sign-in is requested */
|
|
27
|
+
/** Social auth configuration */
|
|
28
|
+
socialConfig?: SocialAuthConfiguration;
|
|
29
|
+
/** Called when Google sign-in is requested (overrides internal behavior) */
|
|
31
30
|
onGoogleSignIn?: () => Promise<void>;
|
|
32
|
-
/** Called when Apple sign-in is requested */
|
|
31
|
+
/** Called when Apple sign-in is requested (overrides internal behavior) */
|
|
33
32
|
onAppleSignIn?: () => Promise<void>;
|
|
34
33
|
}
|
|
35
34
|
|
|
@@ -38,7 +37,7 @@ export const AuthBottomSheet: React.FC<AuthBottomSheetProps> = ({
|
|
|
38
37
|
privacyUrl,
|
|
39
38
|
onTermsPress,
|
|
40
39
|
onPrivacyPress,
|
|
41
|
-
|
|
40
|
+
socialConfig,
|
|
42
41
|
onGoogleSignIn,
|
|
43
42
|
onAppleSignIn,
|
|
44
43
|
}) => {
|
|
@@ -50,13 +49,14 @@ export const AuthBottomSheet: React.FC<AuthBottomSheetProps> = ({
|
|
|
50
49
|
googleLoading,
|
|
51
50
|
appleLoading,
|
|
52
51
|
mode,
|
|
52
|
+
providers,
|
|
53
53
|
handleDismiss,
|
|
54
54
|
handleClose,
|
|
55
55
|
handleNavigateToRegister,
|
|
56
56
|
handleNavigateToLogin,
|
|
57
57
|
handleGoogleSignIn,
|
|
58
58
|
handleAppleSignIn,
|
|
59
|
-
} = useAuthBottomSheet({ onGoogleSignIn, onAppleSignIn });
|
|
59
|
+
} = useAuthBottomSheet({ socialConfig, onGoogleSignIn, onAppleSignIn });
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
<BottomSheetModal
|
|
@@ -106,9 +106,9 @@ export const AuthBottomSheet: React.FC<AuthBottomSheetProps> = ({
|
|
|
106
106
|
/>
|
|
107
107
|
)}
|
|
108
108
|
|
|
109
|
-
{
|
|
109
|
+
{providers.length > 0 && (
|
|
110
110
|
<SocialLoginButtons
|
|
111
|
-
enabledProviders={
|
|
111
|
+
enabledProviders={providers}
|
|
112
112
|
onGooglePress={() => { void handleGoogleSignIn(); }}
|
|
113
113
|
onApplePress={() => { void handleAppleSignIn(); }}
|
|
114
114
|
googleLoading={googleLoading}
|
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
2
3
|
import type { BottomSheetModalRef } from "@umituz/react-native-design-system";
|
|
3
4
|
import { useAuthModalStore } from "../stores/authModalStore";
|
|
4
5
|
import { useAuth } from "../hooks/useAuth";
|
|
6
|
+
import { useGoogleAuth, type GoogleAuthConfig } from "./useGoogleAuth";
|
|
7
|
+
import { useAppleAuth } from "./useAppleAuth";
|
|
8
|
+
import type { SocialAuthProvider } from "../../domain/value-objects/AuthConfig";
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
12
|
+
export interface SocialAuthConfiguration {
|
|
13
|
+
google?: GoogleAuthConfig;
|
|
14
|
+
apple?: { enabled: boolean };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UseAuthBottomSheetParams {
|
|
18
|
+
socialConfig?: SocialAuthConfiguration;
|
|
7
19
|
onGoogleSignIn?: () => Promise<void>;
|
|
8
20
|
onAppleSignIn?: () => Promise<void>;
|
|
9
21
|
}
|
|
10
22
|
|
|
11
|
-
export function useAuthBottomSheet(
|
|
23
|
+
export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
24
|
+
const { socialConfig, onGoogleSignIn, onAppleSignIn } = params;
|
|
25
|
+
|
|
12
26
|
const modalRef = useRef<BottomSheetModalRef>(null);
|
|
13
27
|
const [googleLoading, setGoogleLoading] = useState(false);
|
|
14
28
|
const [appleLoading, setAppleLoading] = useState(false);
|
|
@@ -17,6 +31,25 @@ export function useAuthBottomSheet({ onGoogleSignIn, onAppleSignIn }: UseAuthBot
|
|
|
17
31
|
useAuthModalStore();
|
|
18
32
|
const { isAuthenticated, isAnonymous } = useAuth();
|
|
19
33
|
|
|
34
|
+
// Social Auth Hooks
|
|
35
|
+
const { signInWithGoogle, googleConfigured } = useGoogleAuth(socialConfig?.google);
|
|
36
|
+
const { signInWithApple, appleAvailable } = useAppleAuth();
|
|
37
|
+
|
|
38
|
+
// Determine enabled providers
|
|
39
|
+
const providers = useMemo<SocialAuthProvider[]>(() => {
|
|
40
|
+
const result: SocialAuthProvider[] = [];
|
|
41
|
+
|
|
42
|
+
if (Platform.OS === "ios" && socialConfig?.apple?.enabled && appleAvailable) {
|
|
43
|
+
result.push("apple");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (googleConfigured) {
|
|
47
|
+
result.push("google");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
}, [socialConfig?.apple?.enabled, appleAvailable, googleConfigured]);
|
|
52
|
+
|
|
20
53
|
// Handle visibility sync with modalRef
|
|
21
54
|
useEffect(() => {
|
|
22
55
|
if (isVisible) {
|
|
@@ -41,14 +74,13 @@ export function useAuthBottomSheet({ onGoogleSignIn, onAppleSignIn }: UseAuthBot
|
|
|
41
74
|
const prevIsAnonymousRef = useRef(isAnonymous);
|
|
42
75
|
|
|
43
76
|
useEffect(() => {
|
|
44
|
-
// Determine if user just successfully authenticated (either A: were not authed at all, or B: were anonymous and now aren't)
|
|
45
77
|
const justAuthenticated = !prevIsAuthenticatedRef.current && isAuthenticated;
|
|
46
78
|
const justConvertedFromAnonymous = prevIsAnonymousRef.current && !isAnonymous && isAuthenticated;
|
|
47
79
|
|
|
48
80
|
if ((justAuthenticated || justConvertedFromAnonymous) && isVisible && !isAnonymous) {
|
|
49
81
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
50
82
|
// eslint-disable-next-line no-console
|
|
51
|
-
console.log("[
|
|
83
|
+
console.log("[useAuthBottomSheet] Auto-closing due to successful authentication transition", {
|
|
52
84
|
justAuthenticated,
|
|
53
85
|
justConvertedFromAnonymous,
|
|
54
86
|
});
|
|
@@ -57,7 +89,6 @@ export function useAuthBottomSheet({ onGoogleSignIn, onAppleSignIn }: UseAuthBot
|
|
|
57
89
|
executePendingCallback();
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
// Update refs for next render
|
|
61
92
|
prevIsAuthenticatedRef.current = isAuthenticated;
|
|
62
93
|
prevIsVisibleRef.current = isVisible;
|
|
63
94
|
prevIsAnonymousRef.current = isAnonymous;
|
|
@@ -71,36 +102,43 @@ export function useAuthBottomSheet({ onGoogleSignIn, onAppleSignIn }: UseAuthBot
|
|
|
71
102
|
setMode("login");
|
|
72
103
|
}, [setMode]);
|
|
73
104
|
|
|
74
|
-
const
|
|
75
|
-
if (!onGoogleSignIn) return;
|
|
105
|
+
const handleGoogleSignInInternal = useCallback(async () => {
|
|
76
106
|
setGoogleLoading(true);
|
|
77
107
|
try {
|
|
78
|
-
|
|
108
|
+
if (onGoogleSignIn) {
|
|
109
|
+
await onGoogleSignIn();
|
|
110
|
+
} else if (signInWithGoogle) {
|
|
111
|
+
await signInWithGoogle();
|
|
112
|
+
}
|
|
79
113
|
} finally {
|
|
80
114
|
setGoogleLoading(false);
|
|
81
115
|
}
|
|
82
|
-
}, [onGoogleSignIn]);
|
|
116
|
+
}, [onGoogleSignIn, signInWithGoogle]);
|
|
83
117
|
|
|
84
|
-
const
|
|
85
|
-
if (!onAppleSignIn) return;
|
|
118
|
+
const handleAppleSignInInternal = useCallback(async () => {
|
|
86
119
|
setAppleLoading(true);
|
|
87
120
|
try {
|
|
88
|
-
|
|
121
|
+
if (onAppleSignIn) {
|
|
122
|
+
await onAppleSignIn();
|
|
123
|
+
} else if (signInWithApple) {
|
|
124
|
+
await signInWithApple();
|
|
125
|
+
}
|
|
89
126
|
} finally {
|
|
90
127
|
setAppleLoading(false);
|
|
91
128
|
}
|
|
92
|
-
}, [onAppleSignIn]);
|
|
129
|
+
}, [onAppleSignIn, signInWithApple]);
|
|
93
130
|
|
|
94
131
|
return {
|
|
95
132
|
modalRef,
|
|
96
133
|
googleLoading,
|
|
97
134
|
appleLoading,
|
|
98
135
|
mode,
|
|
136
|
+
providers,
|
|
99
137
|
handleDismiss,
|
|
100
138
|
handleClose,
|
|
101
139
|
handleNavigateToRegister,
|
|
102
140
|
handleNavigateToLogin,
|
|
103
|
-
handleGoogleSignIn,
|
|
104
|
-
handleAppleSignIn,
|
|
141
|
+
handleGoogleSignIn: handleGoogleSignInInternal,
|
|
142
|
+
handleAppleSignIn: handleAppleSignInInternal,
|
|
105
143
|
};
|
|
106
144
|
}
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AuthCoreService Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { AuthCoreService } from '../../../src/infrastructure/services/AuthCoreService';
|
|
6
|
-
import { DEFAULT_AUTH_CONFIG } from '../../../src/domain/value-objects/AuthConfig';
|
|
7
|
-
import type { IAuthProvider } from '../../../src/application/ports/IAuthProvider';
|
|
8
|
-
import type { AuthUser } from '../../../src/domain/entities/AuthUser';
|
|
9
|
-
|
|
10
|
-
describe('AuthCoreService', () => {
|
|
11
|
-
let authCoreService: AuthCoreService;
|
|
12
|
-
let mockProvider: jest.Mocked<IAuthProvider>;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
mockProvider = {
|
|
16
|
-
initialize: jest.fn(),
|
|
17
|
-
isInitialized: jest.fn().mockReturnValue(true),
|
|
18
|
-
signIn: jest.fn(),
|
|
19
|
-
signUp: jest.fn(),
|
|
20
|
-
signOut: jest.fn(),
|
|
21
|
-
getCurrentUser: jest.fn(),
|
|
22
|
-
onAuthStateChange: jest.fn().mockReturnValue(jest.fn()),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
authCoreService = new AuthCoreService(DEFAULT_AUTH_CONFIG);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('constructor', () => {
|
|
29
|
-
it('should initialize with provided config', () => {
|
|
30
|
-
const customConfig = {
|
|
31
|
-
...DEFAULT_AUTH_CONFIG,
|
|
32
|
-
password: {
|
|
33
|
-
...DEFAULT_AUTH_CONFIG.password,
|
|
34
|
-
minLength: 12,
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const service = new AuthCoreService(customConfig);
|
|
39
|
-
expect(service.getConfig()).toEqual(customConfig);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('initialize', () => {
|
|
44
|
-
it('should initialize with IAuthProvider', async () => {
|
|
45
|
-
await authCoreService.initialize(mockProvider);
|
|
46
|
-
expect(mockProvider.initialize).toHaveBeenCalled();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should initialize with Firebase Auth instance', async () => {
|
|
50
|
-
const mockFirebaseAuth = {
|
|
51
|
-
currentUser: null,
|
|
52
|
-
} as any;
|
|
53
|
-
|
|
54
|
-
await expect(authCoreService.initialize(mockFirebaseAuth)).rejects.toThrow();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should throw error when no provider provided', async () => {
|
|
58
|
-
await expect(authCoreService.initialize(null as any)).rejects.toThrow(
|
|
59
|
-
'Auth provider or Firebase Auth instance is required'
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('isInitialized', () => {
|
|
65
|
-
it('should return false when not initialized', () => {
|
|
66
|
-
expect(authCoreService.isInitialized()).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should return true when initialized', async () => {
|
|
70
|
-
await authCoreService.initialize(mockProvider);
|
|
71
|
-
expect(authCoreService.isInitialized()).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('signUp', () => {
|
|
76
|
-
const mockUser: AuthUser = {
|
|
77
|
-
uid: 'test-uid',
|
|
78
|
-
email: 'test@example.com',
|
|
79
|
-
displayName: 'Test User',
|
|
80
|
-
isAnonymous: false,
|
|
81
|
-
emailVerified: false,
|
|
82
|
-
photoURL: null,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
beforeEach(async () => {
|
|
86
|
-
await authCoreService.initialize(mockProvider);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should sign up successfully with valid credentials', async () => {
|
|
90
|
-
mockProvider.signUp.mockResolvedValue(mockUser);
|
|
91
|
-
|
|
92
|
-
const result = await authCoreService.signUp({
|
|
93
|
-
email: 'test@example.com',
|
|
94
|
-
password: 'password123',
|
|
95
|
-
displayName: 'Test User',
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
expect(result).toEqual(mockUser);
|
|
99
|
-
expect(mockProvider.signUp).toHaveBeenCalledWith({
|
|
100
|
-
email: 'test@example.com',
|
|
101
|
-
password: 'password123',
|
|
102
|
-
displayName: 'Test User',
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should sign up without display name', async () => {
|
|
107
|
-
mockProvider.signUp.mockResolvedValue(mockUser);
|
|
108
|
-
|
|
109
|
-
await authCoreService.signUp({
|
|
110
|
-
email: 'test@example.com',
|
|
111
|
-
password: 'password123',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
expect(mockProvider.signUp).toHaveBeenCalledWith({
|
|
115
|
-
email: 'test@example.com',
|
|
116
|
-
password: 'password123',
|
|
117
|
-
displayName: undefined,
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should throw error when not initialized', async () => {
|
|
122
|
-
const uninitializedService = new AuthCoreService(DEFAULT_AUTH_CONFIG);
|
|
123
|
-
|
|
124
|
-
await expect(uninitializedService.signUp({
|
|
125
|
-
email: 'test@example.com',
|
|
126
|
-
password: 'password123',
|
|
127
|
-
})).rejects.toThrow('Auth service is not initialized');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('signIn', () => {
|
|
132
|
-
const mockUser: AuthUser = {
|
|
133
|
-
uid: 'test-uid',
|
|
134
|
-
email: 'test@example.com',
|
|
135
|
-
displayName: 'Test User',
|
|
136
|
-
isAnonymous: false,
|
|
137
|
-
emailVerified: false,
|
|
138
|
-
photoURL: null,
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
beforeEach(async () => {
|
|
142
|
-
await authCoreService.initialize(mockProvider);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should sign in successfully with valid credentials', async () => {
|
|
146
|
-
mockProvider.signIn.mockResolvedValue(mockUser);
|
|
147
|
-
|
|
148
|
-
const result = await authCoreService.signIn({
|
|
149
|
-
email: 'test@example.com',
|
|
150
|
-
password: 'password123',
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
expect(result).toEqual(mockUser);
|
|
154
|
-
expect(mockProvider.signIn).toHaveBeenCalledWith({
|
|
155
|
-
email: 'test@example.com',
|
|
156
|
-
password: 'password123',
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should throw error when not initialized', async () => {
|
|
161
|
-
const uninitializedService = new AuthCoreService(DEFAULT_AUTH_CONFIG);
|
|
162
|
-
|
|
163
|
-
await expect(uninitializedService.signIn({
|
|
164
|
-
email: 'test@example.com',
|
|
165
|
-
password: 'password123',
|
|
166
|
-
})).rejects.toThrow('Auth service is not initialized');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('signOut', () => {
|
|
171
|
-
beforeEach(async () => {
|
|
172
|
-
await authCoreService.initialize(mockProvider);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should sign out successfully', async () => {
|
|
176
|
-
await authCoreService.signOut();
|
|
177
|
-
expect(mockProvider.signOut).toHaveBeenCalled();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should handle sign out when not initialized', async () => {
|
|
181
|
-
const uninitializedService = new AuthCoreService(DEFAULT_AUTH_CONFIG);
|
|
182
|
-
|
|
183
|
-
await expect(uninitializedService.signOut()).resolves.not.toThrow();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('getCurrentUser', () => {
|
|
188
|
-
const mockUser: AuthUser = {
|
|
189
|
-
uid: 'test-uid',
|
|
190
|
-
email: 'test@example.com',
|
|
191
|
-
displayName: 'Test User',
|
|
192
|
-
isAnonymous: false,
|
|
193
|
-
emailVerified: false,
|
|
194
|
-
photoURL: null,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
it('should return null when not initialized', () => {
|
|
198
|
-
const result = authCoreService.getCurrentUser();
|
|
199
|
-
expect(result).toBeNull();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should return current user when initialized', async () => {
|
|
203
|
-
mockProvider.getCurrentUser.mockReturnValue(mockUser);
|
|
204
|
-
await authCoreService.initialize(mockProvider);
|
|
205
|
-
|
|
206
|
-
const result = authCoreService.getCurrentUser();
|
|
207
|
-
expect(result).toEqual(mockUser);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should return null when no current user', async () => {
|
|
211
|
-
mockProvider.getCurrentUser.mockReturnValue(null);
|
|
212
|
-
await authCoreService.initialize(mockProvider);
|
|
213
|
-
|
|
214
|
-
const result = authCoreService.getCurrentUser();
|
|
215
|
-
expect(result).toBeNull();
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe('onAuthStateChange', () => {
|
|
220
|
-
it('should return cleanup function when not initialized', () => {
|
|
221
|
-
const callback = jest.fn();
|
|
222
|
-
const cleanup = authCoreService.onAuthStateChange(callback);
|
|
223
|
-
|
|
224
|
-
expect(callback).toHaveBeenCalledWith(null);
|
|
225
|
-
expect(typeof cleanup).toBe('function');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('should subscribe to auth state changes when initialized', async () => {
|
|
229
|
-
const callback = jest.fn();
|
|
230
|
-
const mockCleanup = jest.fn();
|
|
231
|
-
mockProvider.onAuthStateChange.mockReturnValue(mockCleanup);
|
|
232
|
-
|
|
233
|
-
await authCoreService.initialize(mockProvider);
|
|
234
|
-
const cleanup = authCoreService.onAuthStateChange(callback);
|
|
235
|
-
|
|
236
|
-
expect(mockProvider.onAuthStateChange).toHaveBeenCalledWith(callback);
|
|
237
|
-
expect(cleanup).toBe(mockCleanup);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('getConfig', () => {
|
|
242
|
-
it('should return the current config', () => {
|
|
243
|
-
const config = authCoreService.getConfig();
|
|
244
|
-
expect(config).toEqual(DEFAULT_AUTH_CONFIG);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
});
|