@umituz/react-native-auth 3.6.75 → 3.6.77
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 +4 -2
- package/src/infrastructure/providers/FirebaseAuthProvider.ts +11 -3
- package/src/infrastructure/utils/AuthErrorMapper.ts +18 -74
- package/src/infrastructure/utils/error/errorCodeMapping.constants.ts +120 -0
- package/src/infrastructure/utils/listener/listenerLifecycle.util.ts +42 -27
- package/src/presentation/components/LoginForm.tsx +10 -25
- package/src/presentation/components/RegisterForm.tsx +23 -43
- package/src/presentation/components/form/FormEmailInput.tsx +60 -0
- package/src/presentation/components/form/FormPasswordInput.tsx +62 -0
- package/src/presentation/components/form/FormTextInput.tsx +60 -0
- package/src/presentation/components/form/index.ts +6 -0
- package/src/presentation/hooks/useAccountManagement.ts +32 -2
- package/src/presentation/hooks/useAuth.ts +9 -3
- package/src/presentation/hooks/useAuthBottomSheet.ts +8 -6
- package/src/presentation/hooks/useLoginForm.ts +30 -17
- package/src/presentation/hooks/useRegisterForm.ts +74 -59
- package/src/presentation/stores/authStore.ts +13 -5
- package/src/presentation/stores/initializeAuthListener.ts +9 -3
- package/src/presentation/utils/authOperation.util.ts +4 -5
- package/src/presentation/utils/authTransition.util.ts +13 -2
- package/src/presentation/utils/form/useFormField.hook.ts +97 -0
- package/src/presentation/utils/form/usePasswordValidation.hook.ts +87 -0
- package/src/presentation/utils/getAuthErrorMessage.ts +17 -40
- package/src/presentation/utils/socialAuthHandler.util.ts +20 -37
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { TextInput, StyleSheet, ViewStyle } from "react-native";
|
|
3
|
+
import { AtomicInput } from "@umituz/react-native-design-system";
|
|
4
|
+
|
|
5
|
+
export interface FormPasswordInputProps {
|
|
6
|
+
value: string;
|
|
7
|
+
onChangeText: (text: string) => void;
|
|
8
|
+
label: string;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
error?: string | null;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onSubmitEditing?: () => void;
|
|
13
|
+
returnKeyType?: "next" | "done";
|
|
14
|
+
style?: ViewStyle;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FormPasswordInput = forwardRef<React.ElementRef<typeof TextInput>, FormPasswordInputProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
value,
|
|
21
|
+
onChangeText,
|
|
22
|
+
label,
|
|
23
|
+
placeholder,
|
|
24
|
+
error,
|
|
25
|
+
disabled = false,
|
|
26
|
+
onSubmitEditing,
|
|
27
|
+
returnKeyType = "done",
|
|
28
|
+
style,
|
|
29
|
+
},
|
|
30
|
+
ref
|
|
31
|
+
) => {
|
|
32
|
+
return (
|
|
33
|
+
<AtomicInput
|
|
34
|
+
ref={ref}
|
|
35
|
+
label={label}
|
|
36
|
+
value={value}
|
|
37
|
+
onChangeText={onChangeText}
|
|
38
|
+
placeholder={placeholder}
|
|
39
|
+
secureTextEntry
|
|
40
|
+
showPasswordToggle
|
|
41
|
+
autoCapitalize="none"
|
|
42
|
+
autoCorrect={false}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
state={error ? "error" : "default"}
|
|
45
|
+
helperText={error || undefined}
|
|
46
|
+
returnKeyType={returnKeyType}
|
|
47
|
+
onSubmitEditing={onSubmitEditing}
|
|
48
|
+
blurOnSubmit={returnKeyType === "done"}
|
|
49
|
+
textContentType="oneTimeCode"
|
|
50
|
+
style={[styles.input, style]}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
FormPasswordInput.displayName = "FormPasswordInput";
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
input: {
|
|
60
|
+
marginBottom: 20,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { TextInput, StyleSheet, ViewStyle } from "react-native";
|
|
3
|
+
import { AtomicInput } from "@umituz/react-native-design-system";
|
|
4
|
+
|
|
5
|
+
export interface FormTextInputProps {
|
|
6
|
+
value: string;
|
|
7
|
+
onChangeText: (text: string) => void;
|
|
8
|
+
label: string;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
error?: string | null;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
autoCapitalize?: "none" | "sentences" | "words" | "characters";
|
|
13
|
+
onSubmitEditing?: () => void;
|
|
14
|
+
returnKeyType?: "next" | "done";
|
|
15
|
+
style?: ViewStyle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FormTextInput = forwardRef<React.ElementRef<typeof TextInput>, FormTextInputProps>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
value,
|
|
22
|
+
onChangeText,
|
|
23
|
+
label,
|
|
24
|
+
placeholder,
|
|
25
|
+
error,
|
|
26
|
+
disabled = false,
|
|
27
|
+
autoCapitalize = "none",
|
|
28
|
+
onSubmitEditing,
|
|
29
|
+
returnKeyType = "next",
|
|
30
|
+
style,
|
|
31
|
+
},
|
|
32
|
+
ref
|
|
33
|
+
) => {
|
|
34
|
+
return (
|
|
35
|
+
<AtomicInput
|
|
36
|
+
ref={ref}
|
|
37
|
+
label={label}
|
|
38
|
+
value={value}
|
|
39
|
+
onChangeText={onChangeText}
|
|
40
|
+
placeholder={placeholder}
|
|
41
|
+
autoCapitalize={autoCapitalize}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
state={error ? "error" : "default"}
|
|
44
|
+
helperText={error || undefined}
|
|
45
|
+
returnKeyType={returnKeyType}
|
|
46
|
+
onSubmitEditing={onSubmitEditing}
|
|
47
|
+
blurOnSubmit={returnKeyType === "done"}
|
|
48
|
+
style={[styles.input, style]}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
FormTextInput.displayName = "FormTextInput";
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
input: {
|
|
58
|
+
marginBottom: 20,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FormEmailInput } from "./FormEmailInput";
|
|
2
|
+
export { FormPasswordInput } from "./FormPasswordInput";
|
|
3
|
+
export { FormTextInput } from "./FormTextInput";
|
|
4
|
+
export type { FormEmailInputProps } from "./FormEmailInput";
|
|
5
|
+
export type { FormPasswordInputProps } from "./FormPasswordInput";
|
|
6
|
+
export type { FormTextInputProps } from "./FormTextInput";
|
|
@@ -49,14 +49,44 @@ export const useAccountManagement = (
|
|
|
49
49
|
passwordPromptConfirm = "Confirm",
|
|
50
50
|
} = options;
|
|
51
51
|
|
|
52
|
+
const PASSWORD_PROMPT_TIMEOUT_MS = 300000; // 5 minutes
|
|
53
|
+
|
|
52
54
|
const defaultPasswordPrompt = useCallback((): Promise<string | null> => {
|
|
53
55
|
return new Promise((resolve) => {
|
|
56
|
+
let resolved = false;
|
|
57
|
+
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
resolved = true;
|
|
61
|
+
resolve(null); // Treat timeout as cancellation
|
|
62
|
+
}
|
|
63
|
+
}, PASSWORD_PROMPT_TIMEOUT_MS);
|
|
64
|
+
|
|
54
65
|
Alert.prompt(
|
|
55
66
|
passwordPromptTitle,
|
|
56
67
|
passwordPromptMessage,
|
|
57
68
|
[
|
|
58
|
-
{
|
|
59
|
-
|
|
69
|
+
{
|
|
70
|
+
text: passwordPromptCancel,
|
|
71
|
+
style: "cancel",
|
|
72
|
+
onPress: () => {
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
resolved = true;
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
resolve(null);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
text: passwordPromptConfirm,
|
|
82
|
+
onPress: (pwd?: string) => {
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
resolved = true;
|
|
85
|
+
clearTimeout(timeoutId);
|
|
86
|
+
resolve(pwd || null);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
60
90
|
],
|
|
61
91
|
"secure-text"
|
|
62
92
|
);
|
|
@@ -70,6 +70,7 @@ export function useAuth(): UseAuthResult {
|
|
|
70
70
|
setLoading(true);
|
|
71
71
|
setError(null);
|
|
72
72
|
await signUpMutation.mutateAsync({ email, password, displayName });
|
|
73
|
+
// Only clear anonymous flag after successful signup
|
|
73
74
|
setIsAnonymous(false);
|
|
74
75
|
} catch (err: unknown) {
|
|
75
76
|
setError(err instanceof Error ? err.message : "Sign up failed");
|
|
@@ -87,6 +88,7 @@ export function useAuth(): UseAuthResult {
|
|
|
87
88
|
setLoading(true);
|
|
88
89
|
setError(null);
|
|
89
90
|
await signInMutation.mutateAsync({ email, password });
|
|
91
|
+
// Only clear anonymous flag after successful signin
|
|
90
92
|
setIsAnonymous(false);
|
|
91
93
|
} catch (err: unknown) {
|
|
92
94
|
setError(err instanceof Error ? err.message : "Sign in failed");
|
|
@@ -114,14 +116,18 @@ export function useAuth(): UseAuthResult {
|
|
|
114
116
|
const continueAnonymously = useCallback(async () => {
|
|
115
117
|
try {
|
|
116
118
|
setLoading(true);
|
|
119
|
+
setError(null);
|
|
117
120
|
await anonymousModeMutation.mutateAsync();
|
|
121
|
+
// Only set anonymous flag after successful mutation
|
|
118
122
|
setIsAnonymous(true);
|
|
119
|
-
} catch {
|
|
120
|
-
|
|
123
|
+
} catch (err: unknown) {
|
|
124
|
+
// Don't set anonymous flag on error - let user try again or choose another option
|
|
125
|
+
setError(err instanceof Error ? err.message : "Failed to continue anonymously");
|
|
126
|
+
throw err;
|
|
121
127
|
} finally {
|
|
122
128
|
setLoading(false);
|
|
123
129
|
}
|
|
124
|
-
}, [setIsAnonymous, setLoading, anonymousModeMutation]);
|
|
130
|
+
}, [setIsAnonymous, setLoading, setError, anonymousModeMutation]);
|
|
125
131
|
|
|
126
132
|
return {
|
|
127
133
|
user, userId, userType, loading, isAuthReady, isAnonymous, isAuthenticated, isRegisteredUser, error,
|
|
@@ -28,8 +28,6 @@ export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
|
28
28
|
const { socialConfig, onGoogleSignIn, onAppleSignIn, onAuthSuccess } = params;
|
|
29
29
|
|
|
30
30
|
const modalRef = useRef<BottomSheetModalRef>(null);
|
|
31
|
-
const [googleLoading, setGoogleLoading] = useState(false);
|
|
32
|
-
const [appleLoading, setAppleLoading] = useState(false);
|
|
33
31
|
|
|
34
32
|
const { isVisible, mode, hideAuthModal, setMode, executePendingCallback, clearPendingCallback } =
|
|
35
33
|
useAuthModalStore();
|
|
@@ -44,6 +42,10 @@ export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
|
44
42
|
return determineEnabledProviders(socialConfig, appleAvailable, googleConfigured);
|
|
45
43
|
}, [socialConfig, appleAvailable, googleConfigured]);
|
|
46
44
|
|
|
45
|
+
// Social auth loading states
|
|
46
|
+
const [googleLoading, setGoogleLoading] = useState(false);
|
|
47
|
+
const [appleLoading, setAppleLoading] = useState(false);
|
|
48
|
+
|
|
47
49
|
// Handle visibility sync with modalRef
|
|
48
50
|
useEffect(() => {
|
|
49
51
|
if (isVisible) {
|
|
@@ -90,7 +92,7 @@ export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
|
90
92
|
setMode("login");
|
|
91
93
|
}, [setMode]);
|
|
92
94
|
|
|
93
|
-
const
|
|
95
|
+
const handleGoogleSignIn = useCallback(async () => {
|
|
94
96
|
setGoogleLoading(true);
|
|
95
97
|
try {
|
|
96
98
|
if (onGoogleSignIn) {
|
|
@@ -103,7 +105,7 @@ export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
|
103
105
|
}
|
|
104
106
|
}, [onGoogleSignIn, signInWithGoogle]);
|
|
105
107
|
|
|
106
|
-
const
|
|
108
|
+
const handleAppleSignIn = useCallback(async () => {
|
|
107
109
|
setAppleLoading(true);
|
|
108
110
|
try {
|
|
109
111
|
if (onAppleSignIn) {
|
|
@@ -126,7 +128,7 @@ export function useAuthBottomSheet(params: UseAuthBottomSheetParams = {}) {
|
|
|
126
128
|
handleClose,
|
|
127
129
|
handleNavigateToRegister,
|
|
128
130
|
handleNavigateToLogin,
|
|
129
|
-
handleGoogleSignIn
|
|
130
|
-
handleAppleSignIn
|
|
131
|
+
handleGoogleSignIn,
|
|
132
|
+
handleAppleSignIn,
|
|
131
133
|
};
|
|
132
134
|
}
|
|
@@ -3,6 +3,7 @@ import { useAuth } from "./useAuth";
|
|
|
3
3
|
import { getAuthErrorLocalizationKey, resolveErrorMessage } from "../utils/getAuthErrorMessage";
|
|
4
4
|
import { validateLoginForm } from "../utils/form/formValidation.util";
|
|
5
5
|
import { alertService } from "@umituz/react-native-design-system";
|
|
6
|
+
import { useFormFields } from "../utils/form/useFormField.hook";
|
|
6
7
|
|
|
7
8
|
export interface LoginFormTranslations {
|
|
8
9
|
successTitle: string;
|
|
@@ -32,43 +33,55 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
32
33
|
const { signIn, loading, error, continueAnonymously } = useAuth();
|
|
33
34
|
const translations = config?.translations;
|
|
34
35
|
|
|
35
|
-
const [email, setEmail] = useState("");
|
|
36
|
-
const [password, setPassword] = useState("");
|
|
37
36
|
const [emailError, setEmailError] = useState<string | null>(null);
|
|
38
37
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
|
39
38
|
const [localError, setLocalError] = useState<string | null>(null);
|
|
40
39
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
}, [
|
|
40
|
+
const clearLocalError = useCallback(() => {
|
|
41
|
+
setLocalError(null);
|
|
42
|
+
}, []);
|
|
44
43
|
|
|
45
|
-
const
|
|
44
|
+
const clearFieldErrorsState = useCallback(() => {
|
|
46
45
|
setEmailError(null);
|
|
47
46
|
setPasswordError(null);
|
|
48
47
|
setLocalError(null);
|
|
49
48
|
}, []);
|
|
50
49
|
|
|
50
|
+
const { fields, updateField } = useFormFields(
|
|
51
|
+
{ email: "", password: "" },
|
|
52
|
+
null,
|
|
53
|
+
{ clearLocalError }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const getErrorMessage = useCallback((key: string) => {
|
|
57
|
+
return resolveErrorMessage(key, translations?.errors);
|
|
58
|
+
}, [translations]);
|
|
59
|
+
|
|
60
|
+
const clearErrors = useCallback(() => {
|
|
61
|
+
clearFieldErrorsState();
|
|
62
|
+
}, [clearFieldErrorsState]);
|
|
63
|
+
|
|
51
64
|
const handleEmailChange = useCallback(
|
|
52
65
|
(text: string) => {
|
|
53
|
-
|
|
54
|
-
if (emailError || localError)
|
|
66
|
+
updateField("email", text);
|
|
67
|
+
if (emailError || localError) clearFieldErrorsState();
|
|
55
68
|
},
|
|
56
|
-
[emailError, localError,
|
|
69
|
+
[updateField, emailError, localError, clearFieldErrorsState]
|
|
57
70
|
);
|
|
58
71
|
|
|
59
72
|
const handlePasswordChange = useCallback(
|
|
60
73
|
(text: string) => {
|
|
61
|
-
|
|
62
|
-
if (passwordError || localError)
|
|
74
|
+
updateField("password", text);
|
|
75
|
+
if (passwordError || localError) clearFieldErrorsState();
|
|
63
76
|
},
|
|
64
|
-
[passwordError, localError,
|
|
77
|
+
[updateField, passwordError, localError, clearFieldErrorsState]
|
|
65
78
|
);
|
|
66
79
|
|
|
67
80
|
const handleSignIn = useCallback(async () => {
|
|
68
81
|
clearErrors();
|
|
69
82
|
|
|
70
83
|
const validation = validateLoginForm(
|
|
71
|
-
{ email: email.trim(), password },
|
|
84
|
+
{ email: fields.email.trim(), password: fields.password },
|
|
72
85
|
getErrorMessage
|
|
73
86
|
);
|
|
74
87
|
|
|
@@ -81,7 +94,7 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
try {
|
|
84
|
-
await signIn(email.trim(), password);
|
|
97
|
+
await signIn(fields.email.trim(), fields.password);
|
|
85
98
|
|
|
86
99
|
if (translations) {
|
|
87
100
|
alertService.success(
|
|
@@ -93,7 +106,7 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
93
106
|
const localizationKey = getAuthErrorLocalizationKey(err);
|
|
94
107
|
setLocalError(getErrorMessage(localizationKey));
|
|
95
108
|
}
|
|
96
|
-
}, [
|
|
109
|
+
}, [fields, signIn, translations, getErrorMessage, clearErrors, updateField]);
|
|
97
110
|
|
|
98
111
|
const handleContinueAnonymously = useCallback(async () => {
|
|
99
112
|
try {
|
|
@@ -106,8 +119,8 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
|
|
|
106
119
|
const displayError = localError || error;
|
|
107
120
|
|
|
108
121
|
return {
|
|
109
|
-
email,
|
|
110
|
-
password,
|
|
122
|
+
email: fields.email,
|
|
123
|
+
password: fields.password,
|
|
111
124
|
emailError,
|
|
112
125
|
passwordError,
|
|
113
126
|
localError,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { useState, useCallback
|
|
2
|
-
import {
|
|
3
|
-
validatePasswordForRegister,
|
|
4
|
-
type PasswordRequirements,
|
|
5
|
-
} from "../../infrastructure/utils/AuthValidation";
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
6
2
|
import { DEFAULT_PASSWORD_CONFIG } from "../../domain/value-objects/AuthConfig";
|
|
7
3
|
import { useAuth } from "./useAuth";
|
|
8
4
|
import { getAuthErrorLocalizationKey, resolveErrorMessage } from "../utils/getAuthErrorMessage";
|
|
9
5
|
import { validateRegisterForm, errorsToFieldErrors } from "../utils/form/formValidation.util";
|
|
10
6
|
import { alertService } from "@umituz/react-native-design-system";
|
|
11
|
-
import {
|
|
7
|
+
import { useFormFields } from "../utils/form/useFormField.hook";
|
|
8
|
+
import { usePasswordValidation } from "../utils/form/usePasswordValidation.hook";
|
|
9
|
+
import { clearFieldError, clearFieldErrors } from "../utils/form/formErrorUtils";
|
|
12
10
|
|
|
13
11
|
export type FieldErrors = {
|
|
14
12
|
displayName?: string;
|
|
@@ -35,7 +33,7 @@ export interface UseRegisterFormResult {
|
|
|
35
33
|
fieldErrors: FieldErrors;
|
|
36
34
|
localError: string | null;
|
|
37
35
|
loading: boolean;
|
|
38
|
-
passwordRequirements:
|
|
36
|
+
passwordRequirements: { hasMinLength: boolean };
|
|
39
37
|
passwordsMatch: boolean;
|
|
40
38
|
handleDisplayNameChange: (text: string) => void;
|
|
41
39
|
handleEmailChange: (text: string) => void;
|
|
@@ -49,67 +47,84 @@ export function useRegisterForm(config?: UseRegisterFormConfig): UseRegisterForm
|
|
|
49
47
|
const { signUp, loading, error } = useAuth();
|
|
50
48
|
const translations = config?.translations;
|
|
51
49
|
|
|
52
|
-
const [displayName, setDisplayName] = useState("");
|
|
53
|
-
const [email, setEmail] = useState("");
|
|
54
|
-
const [password, setPassword] = useState("");
|
|
55
|
-
const [confirmPassword, setConfirmPassword] = useState("");
|
|
56
50
|
const [localError, setLocalError] = useState<string | null>(null);
|
|
57
51
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
|
58
52
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
}, [
|
|
62
|
-
|
|
63
|
-
const passwordRequirements = useMemo((): PasswordRequirements => {
|
|
64
|
-
if (!password) {
|
|
65
|
-
return { hasMinLength: false };
|
|
66
|
-
}
|
|
67
|
-
const result = validatePasswordForRegister(password, DEFAULT_PASSWORD_CONFIG);
|
|
68
|
-
return result.requirements;
|
|
69
|
-
}, [password]);
|
|
70
|
-
|
|
71
|
-
const passwordsMatch = useMemo(() => {
|
|
72
|
-
return password.length > 0 && password === confirmPassword;
|
|
73
|
-
}, [password, confirmPassword]);
|
|
53
|
+
const clearLocalError = useCallback(() => {
|
|
54
|
+
setLocalError(null);
|
|
55
|
+
}, []);
|
|
74
56
|
|
|
75
57
|
const clearFormErrors = useCallback(() => {
|
|
76
58
|
setLocalError(null);
|
|
77
59
|
setFieldErrors({});
|
|
78
60
|
}, []);
|
|
79
61
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
62
|
+
const { fields, updateField } = useFormFields(
|
|
63
|
+
{
|
|
64
|
+
displayName: "",
|
|
65
|
+
email: "",
|
|
66
|
+
password: "",
|
|
67
|
+
confirmPassword: "",
|
|
68
|
+
},
|
|
69
|
+
setFieldErrors,
|
|
70
|
+
{ clearLocalError }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const getErrorMessage = useCallback((key: string) => {
|
|
74
|
+
return resolveErrorMessage(key, translations?.errors);
|
|
75
|
+
}, [translations]);
|
|
76
|
+
|
|
77
|
+
const { passwordRequirements, passwordsMatch } = usePasswordValidation(
|
|
78
|
+
fields.password,
|
|
79
|
+
fields.confirmPassword,
|
|
80
|
+
{ passwordConfig: DEFAULT_PASSWORD_CONFIG }
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleDisplayNameChange = useCallback(
|
|
84
|
+
(text: string) => {
|
|
85
|
+
updateField("displayName", text);
|
|
86
|
+
clearFieldError(setFieldErrors, "displayName");
|
|
87
|
+
clearLocalError();
|
|
88
|
+
},
|
|
89
|
+
[updateField, clearLocalError]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const handleEmailChange = useCallback(
|
|
93
|
+
(text: string) => {
|
|
94
|
+
updateField("email", text);
|
|
95
|
+
clearFieldError(setFieldErrors, "email");
|
|
96
|
+
clearLocalError();
|
|
97
|
+
},
|
|
98
|
+
[updateField, clearLocalError]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const handlePasswordChange = useCallback(
|
|
102
|
+
(text: string) => {
|
|
103
|
+
updateField("password", text);
|
|
104
|
+
clearFieldErrors(setFieldErrors, ["password", "confirmPassword"]);
|
|
105
|
+
clearLocalError();
|
|
106
|
+
},
|
|
107
|
+
[updateField, clearLocalError]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const handleConfirmPasswordChange = useCallback(
|
|
111
|
+
(text: string) => {
|
|
112
|
+
updateField("confirmPassword", text);
|
|
113
|
+
clearFieldError(setFieldErrors, "confirmPassword");
|
|
114
|
+
clearLocalError();
|
|
115
|
+
},
|
|
116
|
+
[updateField, clearLocalError]
|
|
117
|
+
);
|
|
103
118
|
|
|
104
119
|
const handleSignUp = useCallback(async () => {
|
|
105
120
|
clearFormErrors();
|
|
106
121
|
|
|
107
122
|
const validation = validateRegisterForm(
|
|
108
123
|
{
|
|
109
|
-
displayName: displayName.trim() || undefined,
|
|
110
|
-
email: email.trim(),
|
|
111
|
-
password,
|
|
112
|
-
confirmPassword,
|
|
124
|
+
displayName: fields.displayName.trim() || undefined,
|
|
125
|
+
email: fields.email.trim(),
|
|
126
|
+
password: fields.password,
|
|
127
|
+
confirmPassword: fields.confirmPassword,
|
|
113
128
|
},
|
|
114
129
|
getErrorMessage,
|
|
115
130
|
DEFAULT_PASSWORD_CONFIG
|
|
@@ -121,7 +136,7 @@ export function useRegisterForm(config?: UseRegisterFormConfig): UseRegisterForm
|
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
try {
|
|
124
|
-
await signUp(email.trim(), password, displayName.trim() || undefined);
|
|
139
|
+
await signUp(fields.email.trim(), fields.password, fields.displayName.trim() || undefined);
|
|
125
140
|
|
|
126
141
|
if (translations) {
|
|
127
142
|
alertService.success(translations.successTitle, translations.signUpSuccess);
|
|
@@ -130,15 +145,15 @@ export function useRegisterForm(config?: UseRegisterFormConfig): UseRegisterForm
|
|
|
130
145
|
const localizationKey = getAuthErrorLocalizationKey(err);
|
|
131
146
|
setLocalError(getErrorMessage(localizationKey));
|
|
132
147
|
}
|
|
133
|
-
}, [
|
|
148
|
+
}, [fields, signUp, translations, getErrorMessage, clearFormErrors, updateField]);
|
|
134
149
|
|
|
135
150
|
const displayError = localError || error;
|
|
136
151
|
|
|
137
152
|
return {
|
|
138
|
-
displayName,
|
|
139
|
-
email,
|
|
140
|
-
password,
|
|
141
|
-
confirmPassword,
|
|
153
|
+
displayName: fields.displayName,
|
|
154
|
+
email: fields.email,
|
|
155
|
+
password: fields.password,
|
|
156
|
+
confirmPassword: fields.confirmPassword,
|
|
142
157
|
fieldErrors,
|
|
143
158
|
localError,
|
|
144
159
|
loading,
|
|
@@ -45,7 +45,7 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
|
45
45
|
}
|
|
46
46
|
return { ...initialAuthState, ...state };
|
|
47
47
|
},
|
|
48
|
-
actions: (set,
|
|
48
|
+
actions: (set, get) => ({
|
|
49
49
|
setFirebaseUser: (firebaseUser) => {
|
|
50
50
|
const user = firebaseUser ? mapToAuthUser(firebaseUser) : null;
|
|
51
51
|
const isAnonymous = firebaseUser?.isAnonymous ?? false;
|
|
@@ -57,10 +57,18 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
|
57
57
|
},
|
|
58
58
|
|
|
59
59
|
setIsAnonymous: (isAnonymous) => {
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
60
|
+
const currentState = get();
|
|
61
|
+
// Only update isAnonymous if it's consistent with the firebaseUser state
|
|
62
|
+
// If we have a firebaseUser, isAnonymous should match it
|
|
63
|
+
const currentUserIsAnonymous = currentState.firebaseUser?.isAnonymous ?? false;
|
|
64
|
+
|
|
65
|
+
if (currentState.firebaseUser) {
|
|
66
|
+
// We have a firebase user - sync isAnonymous with it
|
|
67
|
+
set({ isAnonymous: currentUserIsAnonymous });
|
|
68
|
+
} else {
|
|
69
|
+
// No firebase user yet, allow setting isAnonymous for anonymous mode preference
|
|
70
|
+
set({ isAnonymous });
|
|
71
|
+
}
|
|
64
72
|
},
|
|
65
73
|
|
|
66
74
|
setError: (error) => {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
completeListenerSetup,
|
|
16
16
|
} from "../../infrastructure/utils/listener/listenerLifecycle.util";
|
|
17
17
|
import {
|
|
18
|
-
|
|
18
|
+
startInitialization,
|
|
19
19
|
isListenerInitialized,
|
|
20
20
|
resetListenerState,
|
|
21
21
|
decrementRefCount,
|
|
@@ -30,8 +30,12 @@ export function initializeAuthListener(
|
|
|
30
30
|
): () => void {
|
|
31
31
|
const { autoAnonymousSignIn = true, onAuthStateChange } = options;
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
if (
|
|
33
|
+
// Atomic check-and-set to prevent race conditions
|
|
34
|
+
if (!startInitialization()) {
|
|
35
|
+
// Either already initializing or initialized - handle accordingly
|
|
36
|
+
if (isListenerInitialized()) {
|
|
37
|
+
return handleExistingInitialization()!;
|
|
38
|
+
}
|
|
35
39
|
return handleInitializationInProgress();
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -44,6 +48,8 @@ export function initializeAuthListener(
|
|
|
44
48
|
const store = useAuthStore.getState();
|
|
45
49
|
|
|
46
50
|
if (!auth) {
|
|
51
|
+
// Reset initialization state since we can't proceed
|
|
52
|
+
completeListenerSetup();
|
|
47
53
|
return handleNoFirebaseAuth(store);
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useCallback } from "react";
|
|
7
|
-
import type { MutationFunction } from "@tanstack/react-query";
|
|
8
7
|
|
|
9
8
|
export interface AuthOperationOptions {
|
|
10
9
|
setLoading: (loading: boolean) => void;
|
|
@@ -16,7 +15,7 @@ export interface AuthOperationOptions {
|
|
|
16
15
|
* Create an auth operation wrapper with consistent error handling
|
|
17
16
|
*/
|
|
18
17
|
export function createAuthOperation<T>(
|
|
19
|
-
mutation:
|
|
18
|
+
mutation: (params: T) => Promise<unknown>,
|
|
20
19
|
options: AuthOperationOptions
|
|
21
20
|
) {
|
|
22
21
|
const { setLoading, setError, onSuccess } = options;
|
|
@@ -26,7 +25,7 @@ export function createAuthOperation<T>(
|
|
|
26
25
|
try {
|
|
27
26
|
setLoading(true);
|
|
28
27
|
setError(null);
|
|
29
|
-
await mutation
|
|
28
|
+
await mutation(params);
|
|
30
29
|
onSuccess?.();
|
|
31
30
|
} catch (err: unknown) {
|
|
32
31
|
const errorMessage = err instanceof Error ? err.message : "Operation failed";
|
|
@@ -44,7 +43,7 @@ export function createAuthOperation<T>(
|
|
|
44
43
|
* Create auth operation that doesn't throw on failure
|
|
45
44
|
*/
|
|
46
45
|
export function createSilentAuthOperation<T>(
|
|
47
|
-
mutation:
|
|
46
|
+
mutation: (params: T) => Promise<unknown>,
|
|
48
47
|
options: AuthOperationOptions
|
|
49
48
|
) {
|
|
50
49
|
const { setLoading, setError, onSuccess } = options;
|
|
@@ -54,7 +53,7 @@ export function createSilentAuthOperation<T>(
|
|
|
54
53
|
try {
|
|
55
54
|
setLoading(true);
|
|
56
55
|
setError(null);
|
|
57
|
-
await mutation
|
|
56
|
+
await mutation(params as T);
|
|
58
57
|
onSuccess?.();
|
|
59
58
|
} catch {
|
|
60
59
|
// Silently fail
|