@umituz/react-native-auth 3.6.76 → 3.6.78

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.
@@ -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
- { text: passwordPromptCancel, style: "cancel", onPress: () => resolve(null) },
59
- { text: passwordPromptConfirm, onPress: (pwd?: string) => resolve(pwd || null) },
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
- setIsAnonymous(true);
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 handleGoogleSignInInternal = useCallback(async () => {
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 handleAppleSignInInternal = useCallback(async () => {
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: handleGoogleSignInInternal,
130
- handleAppleSignIn: handleAppleSignInInternal,
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 getErrorMessage = useCallback((key: string) => {
42
- return resolveErrorMessage(key, translations?.errors);
43
- }, [translations]);
40
+ const clearLocalError = useCallback(() => {
41
+ setLocalError(null);
42
+ }, []);
44
43
 
45
- const clearErrors = useCallback(() => {
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
- setEmail(text);
54
- if (emailError || localError) clearErrors();
66
+ updateField("email", text);
67
+ if (emailError || localError) clearFieldErrorsState();
55
68
  },
56
- [emailError, localError, clearErrors],
69
+ [updateField, emailError, localError, clearFieldErrorsState]
57
70
  );
58
71
 
59
72
  const handlePasswordChange = useCallback(
60
73
  (text: string) => {
61
- setPassword(text);
62
- if (passwordError || localError) clearErrors();
74
+ updateField("password", text);
75
+ if (passwordError || localError) clearFieldErrorsState();
63
76
  },
64
- [passwordError, localError, clearErrors],
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
- }, [email, password, signIn, translations, getErrorMessage, clearErrors]);
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, useMemo } from "react";
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 { clearFieldErrors, clearFieldError } from "../utils/form/formErrorUtils";
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: 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 getErrorMessage = useCallback((key: string) => {
60
- return resolveErrorMessage(key, translations?.errors);
61
- }, [translations]);
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 handleDisplayNameChange = useCallback((text: string) => {
81
- setDisplayName(text);
82
- clearFieldError(setFieldErrors, "displayName");
83
- if (localError) setLocalError(null);
84
- }, [localError]);
85
-
86
- const handleEmailChange = useCallback((text: string) => {
87
- setEmail(text);
88
- clearFieldError(setFieldErrors, "email");
89
- if (localError) setLocalError(null);
90
- }, [localError]);
91
-
92
- const handlePasswordChange = useCallback((text: string) => {
93
- setPassword(text);
94
- clearFieldErrors(setFieldErrors, ["password", "confirmPassword"]);
95
- if (localError) setLocalError(null);
96
- }, [localError]);
97
-
98
- const handleConfirmPasswordChange = useCallback((text: string) => {
99
- setConfirmPassword(text);
100
- clearFieldError(setFieldErrors, "confirmPassword");
101
- if (localError) setLocalError(null);
102
- }, [localError]);
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
- }, [displayName, email, password, confirmPassword, signUp, translations, getErrorMessage, clearFormErrors]);
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,
@@ -9,7 +9,7 @@
9
9
  * The isAnonymous flag indicates the user type, not whether user is null.
10
10
  */
11
11
 
12
- import { createStore } from "@umituz/react-native-design-system";
12
+ import { createStore, storageService } from "@umituz/react-native-design-system";
13
13
  import { mapToAuthUser } from "../../infrastructure/utils/UserMapper";
14
14
  import type { AuthState, AuthActions, UserType } from "../../types/auth-store.types";
15
15
  import { initialAuthState } from "../../types/auth-store.types";
@@ -29,6 +29,7 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
29
29
  name: "auth-store",
30
30
  initialState: initialAuthState,
31
31
  persist: true,
32
+ storage: storageService,
32
33
  version: 2,
33
34
  partialize: (state) => ({
34
35
  isAnonymous: state.isAnonymous,
@@ -45,7 +46,7 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
45
46
  }
46
47
  return { ...initialAuthState, ...state };
47
48
  },
48
- actions: (set, _get) => ({
49
+ actions: (set, get) => ({
49
50
  setFirebaseUser: (firebaseUser) => {
50
51
  const user = firebaseUser ? mapToAuthUser(firebaseUser) : null;
51
52
  const isAnonymous = firebaseUser?.isAnonymous ?? false;
@@ -57,10 +58,18 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
57
58
  },
58
59
 
59
60
  setIsAnonymous: (isAnonymous) => {
60
- // Only update the isAnonymous flag
61
- // The user object will be updated by setFirebaseUser when needed
62
- // This prevents inconsistencies between firebaseUser and user
63
- set({ isAnonymous });
61
+ const currentState = get();
62
+ // Only update isAnonymous if it's consistent with the firebaseUser state
63
+ // If we have a firebaseUser, isAnonymous should match it
64
+ const currentUserIsAnonymous = currentState.firebaseUser?.isAnonymous ?? false;
65
+
66
+ if (currentState.firebaseUser) {
67
+ // We have a firebase user - sync isAnonymous with it
68
+ set({ isAnonymous: currentUserIsAnonymous });
69
+ } else {
70
+ // No firebase user yet, allow setting isAnonymous for anonymous mode preference
71
+ set({ isAnonymous });
72
+ }
64
73
  },
65
74
 
66
75
  setError: (error) => {
@@ -15,7 +15,7 @@ import {
15
15
  completeListenerSetup,
16
16
  } from "../../infrastructure/utils/listener/listenerLifecycle.util";
17
17
  import {
18
- isInitializationInProgress,
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
- // Prevent duplicate initialization
34
- if (isInitializationInProgress()) {
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
 
@@ -22,11 +22,12 @@ export interface AuthTransitionResult {
22
22
  */
23
23
  export function useAuthTransitions(
24
24
  state: AuthTransitionState,
25
- onTransition?: (result: AuthTransitionResult) => void
25
+ onTransition?: (result: AuthTransitionResult) => (() => void) | void
26
26
  ): void {
27
27
  const prevIsAuthenticatedRef = useRef(state.isAuthenticated);
28
28
  const prevIsAnonymousRef = useRef(state.isAnonymous);
29
29
  const prevIsVisibleRef = useRef(state.isVisible);
30
+ const cleanupRef = useRef<(() => void) | null>(null);
30
31
 
31
32
  useEffect(() => {
32
33
  const justAuthenticated = !prevIsAuthenticatedRef.current && state.isAuthenticated;
@@ -41,11 +42,21 @@ export function useAuthTransitions(
41
42
  shouldClose,
42
43
  };
43
44
 
44
- onTransition?.(result);
45
+ // Call previous cleanup before running new transition
46
+ cleanupRef.current?.();
47
+
48
+ const cleanup = onTransition?.(result);
49
+ cleanupRef.current = cleanup ?? null;
45
50
 
46
51
  prevIsAuthenticatedRef.current = state.isAuthenticated;
47
52
  prevIsVisibleRef.current = state.isVisible;
48
53
  prevIsAnonymousRef.current = state.isAnonymous;
54
+
55
+ // Return cleanup function for useEffect
56
+ return () => {
57
+ cleanupRef.current?.();
58
+ cleanupRef.current = null;
59
+ };
49
60
  }, [state.isAuthenticated, state.isVisible, state.isAnonymous, onTransition]);
50
61
  }
51
62