@umituz/react-native-auth 4.3.53 → 4.3.55

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-auth",
3
- "version": "4.3.53",
3
+ "version": "4.3.55",
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",
@@ -89,13 +89,16 @@ class AuthEventService {
89
89
  private notifyListeners(event: string, payload: AuthEventPayload): void {
90
90
  const eventListeners = this.listeners.get(event);
91
91
  if (eventListeners) {
92
- eventListeners.forEach((listener) => {
92
+ // Copy array before iterating to prevent issues if a listener
93
+ // removes itself or other listeners during notification
94
+ const snapshot = [...eventListeners];
95
+ for (const listener of snapshot) {
93
96
  try {
94
97
  listener(payload);
95
98
  } catch (error) {
96
99
  console.error(`[AuthEventService] Listener error for "${event}":`, error);
97
100
  }
98
- });
101
+ }
99
102
  }
100
103
  }
101
104
  }
@@ -27,6 +27,7 @@ export interface InitializeAuthOptions {
27
27
 
28
28
  let isInitialized = false;
29
29
  let initializationPromise: Promise<{ success: boolean }> | null = null;
30
+ let listenerUnsubscribe: (() => void) | null = null;
30
31
  const conversionState: { current: ConversionState } = {
31
32
  current: { previousUserId: null, wasAnonymous: false },
32
33
  };
@@ -102,7 +103,7 @@ async function doInitializeAuth(
102
103
  onAuthStateChange,
103
104
  });
104
105
 
105
- initializeAuthListener({
106
+ listenerUnsubscribe = initializeAuthListener({
106
107
  autoAnonymousSignIn,
107
108
  onAuthStateChange: (user) => {
108
109
  void handleAuthStateChange(user);
@@ -118,6 +119,10 @@ export function isAuthInitialized(): boolean {
118
119
  }
119
120
 
120
121
  export function resetAuthInitialization(): void {
122
+ if (listenerUnsubscribe) {
123
+ listenerUnsubscribe();
124
+ listenerUnsubscribe = null;
125
+ }
121
126
  isInitialized = false;
122
127
  conversionState.current = { previousUserId: null, wasAnonymous: false };
123
128
  }
@@ -11,12 +11,10 @@ import {
11
11
  completeAnonymousSignIn,
12
12
  } from "./listenerState.util";
13
13
 
14
- type Store = AuthActions & { isAnonymous: boolean };
15
-
16
14
  /**
17
15
  * Handle anonymous mode sign-in
18
16
  */
19
- export async function handleAnonymousMode(store: Store, auth: Auth): Promise<void> {
17
+ export async function handleAnonymousMode(store: AuthActions, auth: Auth): Promise<void> {
20
18
  if (!startAnonymousSignIn()) {
21
19
  return; // Already signing in
22
20
  }
@@ -42,11 +42,13 @@ async function attemptAnonymousSignIn(
42
42
 
43
43
  callbacks.onSignInStart();
44
44
 
45
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
46
+
45
47
  try {
46
- // Add timeout protection
47
- const timeoutPromise = new Promise<never>((_, reject) =>
48
- setTimeout(() => reject(new Error("Anonymous sign-in timeout")), timeout)
49
- );
48
+ // Add timeout protection with proper cleanup
49
+ const timeoutPromise = new Promise<never>((_, reject) => {
50
+ timeoutId = setTimeout(() => reject(new Error("Anonymous sign-in timeout")), timeout);
51
+ });
50
52
 
51
53
  // Race between sign-in and timeout
52
54
  await Promise.race([
@@ -58,6 +60,11 @@ async function attemptAnonymousSignIn(
58
60
  } catch (error) {
59
61
  const signInError = error instanceof Error ? error : new Error("Unknown sign-in error");
60
62
  callbacks.onSignInFailure(signInError);
63
+ } finally {
64
+ // Always clear timeout to prevent timer leaks
65
+ if (timeoutId !== undefined) {
66
+ clearTimeout(timeoutId);
67
+ }
61
68
  }
62
69
  }
63
70
 
@@ -5,34 +5,37 @@
5
5
 
6
6
  import type { Auth, User } from "firebase/auth";
7
7
  import type { AuthActions } from "../../../types/auth-store.types";
8
- import { completeInitialization } from "./listenerState.util";
9
8
  import { handleAnonymousMode } from "./anonymousHandler";
10
9
  import { safeCallbackSync } from "../safeCallback";
11
10
 
12
- type Store = AuthActions & { isAnonymous: boolean };
11
+ type StoreActions = AuthActions;
12
+ type GetIsAnonymous = () => boolean;
13
13
 
14
14
  /**
15
15
  * Handle auth state change from Firebase
16
16
  */
17
17
  export function handleAuthStateChange(
18
18
  user: User | null,
19
- store: Store,
19
+ store: StoreActions,
20
20
  auth: Auth,
21
21
  autoAnonymousSignIn: boolean,
22
- onAuthStateChange?: (user: User | null) => void | Promise<void>
22
+ onAuthStateChange?: (user: User | null) => void | Promise<void>,
23
+ getIsAnonymous?: GetIsAnonymous
23
24
  ): void {
24
25
  try {
25
26
  if (!user && autoAnonymousSignIn) {
27
+ // Don't call completeInitialization here - handleAnonymousMode
28
+ // will set initialized/loading when anonymous sign-in completes or fails.
26
29
  void handleAnonymousMode(store, auth);
27
- completeInitialization();
28
30
  return;
29
31
  }
30
32
 
31
33
  store.setFirebaseUser(user);
32
34
  store.setInitialized(true);
33
35
 
34
- // Handle conversion from anonymous
35
- if (user && !user.isAnonymous && store.isAnonymous) {
36
+ // Handle conversion from anonymous - read fresh state, not stale snapshot
37
+ const currentIsAnonymous = getIsAnonymous ? getIsAnonymous() : false;
38
+ if (user && !user.isAnonymous && currentIsAnonymous) {
36
39
  store.setIsAnonymous(false);
37
40
  }
38
41
 
@@ -6,12 +6,10 @@
6
6
  import type { AuthActions } from "../../../types/auth-store.types";
7
7
  import { completeInitialization } from "./listenerState.util";
8
8
 
9
- type Store = AuthActions & { isAnonymous: boolean };
10
-
11
9
  /**
12
10
  * Handle case where Firebase auth is not available
13
11
  */
14
- export function handleNoFirebaseAuth(store: Store): () => void {
12
+ export function handleNoFirebaseAuth(store: AuthActions): () => void {
15
13
  completeInitialization();
16
14
  store.setLoading(false);
17
15
  store.setInitialized(true);
@@ -10,16 +10,18 @@ import { getAuthService } from "../../services/AuthService";
10
10
  import { completeInitialization, setUnsubscribe } from "./listenerState.util";
11
11
  import { handleAuthStateChange } from "./authListenerStateHandler";
12
12
 
13
- type Store = AuthActions & { isAnonymous: boolean };
13
+ type StoreActions = AuthActions;
14
14
 
15
15
  /**
16
16
  * Setup Firebase auth listener with timeout protection
17
+ * @param getIsAnonymous - Function to read fresh isAnonymous state (avoids stale snapshots)
17
18
  */
18
19
  export function setupAuthListener(
19
20
  auth: Auth,
20
- store: Store,
21
+ store: StoreActions,
21
22
  autoAnonymousSignIn: boolean,
22
- onAuthStateChange?: (user: User | null) => void | Promise<void>
23
+ onAuthStateChange?: (user: User | null) => void | Promise<void>,
24
+ getIsAnonymous?: () => boolean
23
25
  ): void {
24
26
  const service = getAuthService();
25
27
 
@@ -46,7 +48,7 @@ export function setupAuthListener(
46
48
  hasTriggered = true;
47
49
  clearTimeout(timeout);
48
50
  }
49
- handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange);
51
+ handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange, getIsAnonymous);
50
52
  });
51
53
 
52
54
  setUnsubscribe(unsubscribe);
@@ -98,7 +98,11 @@ export function createAuthInitModule(
98
98
 
99
99
  // Call custom callback if provided
100
100
  if (onUserConverted) {
101
- await onUserConverted(anonymousId, authenticatedId);
101
+ try {
102
+ await onUserConverted(anonymousId, authenticatedId);
103
+ } catch (error) {
104
+ console.error('[AuthInitModule] onUserConverted callback failed:', error instanceof Error ? error.message : String(error));
105
+ }
102
106
  }
103
107
  },
104
108
  });
@@ -33,10 +33,14 @@ export function useRegisterFormSubmit(
33
33
  const handleSignUp = useCallback(async () => {
34
34
  clearFormErrors();
35
35
 
36
+ // Sanitize once, use for both validation and sign-up
37
+ const sanitizedEmail = sanitizeEmail(fields.email);
38
+ const sanitizedName = sanitizeName(fields.displayName) || undefined;
39
+
36
40
  const validation = validateRegisterForm(
37
41
  {
38
- displayName: sanitizeName(fields.displayName) || undefined,
39
- email: sanitizeEmail(fields.email),
42
+ displayName: sanitizedName,
43
+ email: sanitizedEmail,
40
44
  password: fields.password,
41
45
  confirmPassword: fields.confirmPassword,
42
46
  },
@@ -50,7 +54,7 @@ export function useRegisterFormSubmit(
50
54
  }
51
55
 
52
56
  try {
53
- await signUp(sanitizeEmail(fields.email), fields.password, sanitizeName(fields.displayName) || undefined);
57
+ await signUp(sanitizedEmail, fields.password, sanitizedName);
54
58
 
55
59
  if (translations) {
56
60
  alertService.success(translations.successTitle, translations.signUpSuccess);
@@ -3,7 +3,7 @@
3
3
  * React hook for authentication state management
4
4
  */
5
5
 
6
- import { useCallback } from "react";
6
+ import { useCallback, useRef } from "react";
7
7
  import { useAuthStore } from "../stores/authStore";
8
8
  import {
9
9
  selectUser,
@@ -11,7 +11,6 @@ import {
11
11
  selectError,
12
12
  selectSetLoading,
13
13
  selectSetError,
14
- selectSetIsAnonymous,
15
14
  selectIsAuthenticated,
16
15
  selectHasFirebaseUser,
17
16
  selectUserId,
@@ -57,21 +56,30 @@ export function useAuth(): UseAuthResult {
57
56
  const isAuthReady = useAuthStore(selectIsAuthReady);
58
57
  const setLoading = useAuthStore(selectSetLoading);
59
58
  const setError = useAuthStore(selectSetError);
60
- const setIsAnonymous = useAuthStore(selectSetIsAnonymous);
61
59
 
62
60
  const signInMutation = useSignInMutation();
63
61
  const signUpMutation = useSignUpMutation();
64
62
  const signOutMutation = useSignOutMutation();
65
63
  const anonymousModeMutation = useAnonymousModeMutation();
66
64
 
65
+ // Store mutateAsync in refs to avoid recreating callbacks on every render.
66
+ // useMutation returns a new object each render, but mutateAsync is stable.
67
+ const signUpMutateRef = useRef(signUpMutation.mutateAsync);
68
+ signUpMutateRef.current = signUpMutation.mutateAsync;
69
+ const signInMutateRef = useRef(signInMutation.mutateAsync);
70
+ signInMutateRef.current = signInMutation.mutateAsync;
71
+ const signOutMutateRef = useRef(signOutMutation.mutateAsync);
72
+ signOutMutateRef.current = signOutMutation.mutateAsync;
73
+ const anonymousMutateRef = useRef(anonymousModeMutation.mutateAsync);
74
+ anonymousMutateRef.current = anonymousModeMutation.mutateAsync;
75
+
67
76
  const signUp = useCallback(
68
77
  async (email: string, password: string, displayName?: string) => {
69
78
  try {
70
79
  setLoading(true);
71
80
  setError(null);
72
- await signUpMutation.mutateAsync({ email, password, displayName });
73
- // Only clear anonymous flag after successful signup
74
- setIsAnonymous(false);
81
+ await signUpMutateRef.current({ email, password, displayName });
82
+ // isAnonymous is automatically derived from firebaseUser by the auth listener
75
83
  } catch (err: unknown) {
76
84
  setError(err instanceof Error ? err.message : "Sign up failed");
77
85
  throw err;
@@ -79,7 +87,7 @@ export function useAuth(): UseAuthResult {
79
87
  setLoading(false);
80
88
  }
81
89
  },
82
- [setIsAnonymous, setLoading, setError, signUpMutation]
90
+ [setLoading, setError]
83
91
  );
84
92
 
85
93
  const signIn = useCallback(
@@ -87,9 +95,8 @@ export function useAuth(): UseAuthResult {
87
95
  try {
88
96
  setLoading(true);
89
97
  setError(null);
90
- await signInMutation.mutateAsync({ email, password });
91
- // Only clear anonymous flag after successful signin
92
- setIsAnonymous(false);
98
+ await signInMutateRef.current({ email, password });
99
+ // isAnonymous is automatically derived from firebaseUser by the auth listener
93
100
  } catch (err: unknown) {
94
101
  setError(err instanceof Error ? err.message : "Sign in failed");
95
102
  throw err;
@@ -97,37 +104,35 @@ export function useAuth(): UseAuthResult {
97
104
  setLoading(false);
98
105
  }
99
106
  },
100
- [setIsAnonymous, setLoading, setError, signInMutation]
107
+ [setLoading, setError]
101
108
  );
102
109
 
103
110
  const signOut = useCallback(async () => {
104
111
  try {
105
112
  setLoading(true);
106
113
  setError(null);
107
- await signOutMutation.mutateAsync();
114
+ await signOutMutateRef.current();
108
115
  } catch (err: unknown) {
109
116
  setError(err instanceof Error ? err.message : "Sign out failed");
110
117
  throw err;
111
118
  } finally {
112
119
  setLoading(false);
113
120
  }
114
- }, [setLoading, setError, signOutMutation]);
121
+ }, [setLoading, setError]);
115
122
 
116
123
  const continueAnonymously = useCallback(async () => {
117
124
  try {
118
125
  setLoading(true);
119
126
  setError(null);
120
- await anonymousModeMutation.mutateAsync();
121
- // Only set anonymous flag after successful mutation
122
- setIsAnonymous(true);
127
+ await anonymousMutateRef.current();
128
+ // isAnonymous is automatically derived from firebaseUser by the auth listener
123
129
  } catch (err: unknown) {
124
- // Don't set anonymous flag on error - let user try again or choose another option
125
130
  setError(err instanceof Error ? err.message : "Failed to continue anonymously");
126
131
  throw err;
127
132
  } finally {
128
133
  setLoading(false);
129
134
  }
130
- }, [setIsAnonymous, setLoading, setError, anonymousModeMutation]);
135
+ }, [setLoading, setError]);
131
136
 
132
137
  return {
133
138
  user, userId, userType, loading, isAuthReady, isAnonymous, isAuthenticated, hasFirebaseUser, error,
@@ -44,17 +44,13 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
44
44
  setEmailError(null);
45
45
  setPasswordError(null);
46
46
  setLocalError(null);
47
- }, []);
47
+ }, [setLocalError]);
48
48
 
49
49
  const { fields, updateField } = useFormFields(
50
50
  { email: "", password: "" },
51
51
  { clearLocalError }
52
52
  );
53
53
 
54
- const clearErrors = useCallback(() => {
55
- clearFieldErrorsState();
56
- }, [clearFieldErrorsState]);
57
-
58
54
  const handleEmailChange = useCallback(
59
55
  (text: string) => {
60
56
  updateField("email", text);
@@ -72,10 +68,13 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
72
68
  );
73
69
 
74
70
  const handleSignIn = useCallback(async () => {
75
- clearErrors();
71
+ clearFieldErrorsState();
72
+
73
+ // Sanitize once, use for both validation and sign-in
74
+ const sanitizedEmail = sanitizeEmail(fields.email);
76
75
 
77
76
  const validation = validateLoginForm(
78
- { email: sanitizeEmail(fields.email), password: fields.password },
77
+ { email: sanitizedEmail, password: fields.password },
79
78
  getErrorMessage
80
79
  );
81
80
 
@@ -88,7 +87,7 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
88
87
  }
89
88
 
90
89
  try {
91
- await signIn(sanitizeEmail(fields.email), fields.password);
90
+ await signIn(sanitizedEmail, fields.password);
92
91
 
93
92
  if (translations) {
94
93
  alertService.success(
@@ -99,7 +98,7 @@ export function useLoginForm(config?: UseLoginFormConfig): UseLoginFormResult {
99
98
  } catch (err: unknown) {
100
99
  setLocalError(handleAuthError(err));
101
100
  }
102
- }, [fields, signIn, translations, handleAuthError, getErrorMessage, clearErrors]);
101
+ }, [fields, signIn, translations, handleAuthError, getErrorMessage, clearFieldErrorsState, setLocalError]);
103
102
 
104
103
  const handleContinueAnonymously = useCallback(async () => {
105
104
  try {
@@ -48,8 +48,12 @@ export function initializeAuthListener(
48
48
  return handleNoFirebaseAuth(store);
49
49
  }
50
50
 
51
+ // Pass a getter function for isAnonymous to avoid stale snapshot reads.
52
+ // store.getState() returns a snapshot, but isAnonymous can change over time.
53
+ const getIsAnonymous = () => useAuthStore.getState().isAnonymous;
54
+
51
55
  // Setup the listener
52
- setupAuthListener(auth, store, autoAnonymousSignIn, onAuthStateChange);
56
+ setupAuthListener(auth, store, autoAnonymousSignIn, onAuthStateChange, getIsAnonymous);
53
57
  completeListenerSetup();
54
58
 
55
59
  // Return cleanup function
@@ -17,15 +17,19 @@ import type {
17
17
  ProfileFormValues,
18
18
  FormValidationError,
19
19
  } from "./formValidation.types";
20
- import { sanitizeEmail, sanitizeName } from "../../../../infrastructure/utils/validation/sanitization";
20
+ import { sanitizeName } from "../../../../infrastructure/utils/validation/sanitization";
21
21
 
22
+ /**
23
+ * Validate login form values.
24
+ * IMPORTANT: Callers must sanitize email before passing to this function.
25
+ */
22
26
  export function validateLoginForm(
23
27
  values: LoginFormValues,
24
28
  getErrorMessage: (key: string) => string
25
29
  ): FormValidationResult {
26
30
  const errors: FormValidationError[] = [];
27
31
 
28
- const emailResult = validateEmail(sanitizeEmail(values.email));
32
+ const emailResult = validateEmail(values.email);
29
33
  if (!emailResult.isValid && emailResult.error) {
30
34
  errors.push({ field: "email", message: getErrorMessage(emailResult.error) });
31
35
  }
@@ -38,6 +42,10 @@ export function validateLoginForm(
38
42
  return { isValid: errors.length === 0, errors };
39
43
  }
40
44
 
45
+ /**
46
+ * Validate register form values.
47
+ * IMPORTANT: Callers must sanitize email before passing to this function.
48
+ */
41
49
  export function validateRegisterForm(
42
50
  values: RegisterFormValues,
43
51
  getErrorMessage: (key: string) => string,
@@ -45,7 +53,7 @@ export function validateRegisterForm(
45
53
  ): FormValidationResult {
46
54
  const errors: FormValidationError[] = [];
47
55
 
48
- const emailResult = validateEmail(sanitizeEmail(values.email));
56
+ const emailResult = validateEmail(values.email);
49
57
  if (!emailResult.isValid && emailResult.error) {
50
58
  errors.push({ field: "email", message: getErrorMessage(emailResult.error) });
51
59
  }
@@ -63,6 +71,10 @@ export function validateRegisterForm(
63
71
  return { isValid: errors.length === 0, errors };
64
72
  }
65
73
 
74
+ /**
75
+ * Validate profile form values.
76
+ * Email should be pre-sanitized by caller if provided.
77
+ */
66
78
  export function validateProfileForm(
67
79
  values: ProfileFormValues,
68
80
  getErrorMessage: (key: string) => string
@@ -77,7 +89,7 @@ export function validateProfileForm(
77
89
  }
78
90
 
79
91
  if (values.email) {
80
- const emailResult = validateEmail(sanitizeEmail(values.email));
92
+ const emailResult = validateEmail(values.email);
81
93
  if (!emailResult.isValid && emailResult.error) {
82
94
  errors.push({ field: "email", message: getErrorMessage(emailResult.error) });
83
95
  }