@umituz/react-native-auth 3.6.86 → 3.6.88

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/infrastructure/services/AnonymousModeService.ts +5 -2
  3. package/src/infrastructure/services/AuthService.ts +13 -1
  4. package/src/infrastructure/services/UserDocument.types.ts +60 -0
  5. package/src/infrastructure/services/UserDocumentService.ts +85 -0
  6. package/src/infrastructure/utils/AuthValidation.ts +8 -4
  7. package/src/infrastructure/utils/UserMapper.ts +7 -3
  8. package/src/infrastructure/utils/authStateHandler.ts +7 -0
  9. package/src/infrastructure/utils/error/errorCodeMapping.constants.ts +12 -109
  10. package/src/infrastructure/utils/error/mappings/actionCodeErrorMappings.ts +26 -0
  11. package/src/infrastructure/utils/error/mappings/authErrorMappings.ts +69 -0
  12. package/src/infrastructure/utils/error/mappings/configErrorMappings.ts +31 -0
  13. package/src/infrastructure/utils/error/mappings/errorMapping.types.ts +12 -0
  14. package/src/infrastructure/utils/error/mappings/networkErrorMappings.ts +22 -0
  15. package/src/infrastructure/utils/listener/anonymousHandler.ts +31 -0
  16. package/src/infrastructure/utils/listener/authStateHandler.ts +59 -0
  17. package/src/infrastructure/utils/listener/cleanupHandlers.ts +46 -0
  18. package/src/infrastructure/utils/listener/initializationHandlers.ts +26 -0
  19. package/src/infrastructure/utils/listener/listenerLifecycle.util.ts +19 -161
  20. package/src/infrastructure/utils/listener/listenerState.util.ts +21 -3
  21. package/src/infrastructure/utils/listener/setupListener.ts +63 -0
  22. package/src/infrastructure/utils/userDocumentBuilder.util.ts +110 -0
  23. package/src/presentation/hooks/registerForm/registerFormHandlers.ts +59 -0
  24. package/src/presentation/hooks/registerForm/registerFormSubmit.ts +64 -0
  25. package/src/presentation/hooks/registerForm/useRegisterForm.types.ts +39 -0
  26. package/src/presentation/hooks/useProfileEdit.ts +7 -4
  27. package/src/presentation/hooks/useRegisterForm.ts +31 -109
  28. package/src/presentation/utils/form/formValidation.util.ts +23 -114
  29. package/src/presentation/utils/form/useFormField.hook.ts +2 -2
  30. package/src/presentation/utils/form/validation/formValidation.hook.ts +30 -0
  31. package/src/presentation/utils/form/validation/formValidation.types.ts +31 -0
  32. package/src/presentation/utils/form/validation/formValidation.utils.ts +17 -0
  33. package/src/presentation/utils/form/validation/formValidators.ts +86 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-auth",
3
- "version": "3.6.86",
3
+ "version": "3.6.88",
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",
@@ -51,8 +51,11 @@ export class AnonymousModeService {
51
51
  if (provider?.getCurrentUser()) {
52
52
  try {
53
53
  await provider.signOut();
54
- } catch {
55
- // Silently fail - sign out errors are handled elsewhere
54
+ } catch (error) {
55
+ // Log error but don't throw - allow anonymous mode to be enabled
56
+ // even if sign-out fails (user might be in inconsistent state but
57
+ // subsequent auth operations will resolve it)
58
+ console.warn("[AnonymousModeService] Sign-out failed during anonymous mode enable:", error);
56
59
  }
57
60
  }
58
61
 
@@ -33,12 +33,24 @@ export class AuthService {
33
33
  return this.repository;
34
34
  }
35
35
 
36
+ private isFirebaseAuth(obj: IAuthProvider | Auth): obj is Auth {
37
+ return (
38
+ typeof obj === "object" &&
39
+ obj !== null &&
40
+ "currentUser" in obj &&
41
+ "onAuthStateChanged" in obj &&
42
+ "signInWithEmailAndPassword" in obj &&
43
+ "createUserWithEmailAndPassword" in obj &&
44
+ typeof obj.onAuthStateChanged === "function"
45
+ );
46
+ }
47
+
36
48
  async initialize(providerOrAuth: IAuthProvider | Auth): Promise<void> {
37
49
  if (this.initialized) return;
38
50
 
39
51
  let provider: IAuthProvider;
40
52
 
41
- if ("currentUser" in providerOrAuth) {
53
+ if (this.isFirebaseAuth(providerOrAuth)) {
42
54
  const firebaseProvider = new FirebaseAuthProvider(providerOrAuth);
43
55
  await firebaseProvider.initialize();
44
56
  provider = firebaseProvider;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * User Document Types and Configuration
3
+ */
4
+
5
+ /**
6
+ * Minimal user interface for document creation
7
+ * Compatible with both Firebase User and AuthUser
8
+ */
9
+ export interface UserDocumentUser {
10
+ uid: string;
11
+ displayName?: string | null;
12
+ email?: string | null;
13
+ photoURL?: string | null;
14
+ isAnonymous?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Configuration for user document service
19
+ */
20
+ export interface UserDocumentConfig {
21
+ /** Firestore collection name (default: "users") */
22
+ collectionName?: string;
23
+ /** Additional fields to store with user document */
24
+ extraFields?: Record<string, unknown>;
25
+ /** Callback to collect device/app info */
26
+ collectExtras?: () => Promise<UserDocumentExtras>;
27
+ }
28
+
29
+ /**
30
+ * User document extras from device/app
31
+ */
32
+ export interface UserDocumentExtras {
33
+ [key: string]: string | number | boolean | null | undefined;
34
+ deviceId?: string;
35
+ persistentDeviceId?: string;
36
+ nativeDeviceId?: string;
37
+ platform?: string;
38
+ deviceModel?: string;
39
+ deviceBrand?: string;
40
+ deviceName?: string;
41
+ deviceType?: number | string;
42
+ deviceYearClass?: number | string;
43
+ isDevice?: boolean;
44
+ osName?: string;
45
+ osVersion?: string;
46
+ osBuildId?: string;
47
+ totalMemory?: number | string;
48
+ appVersion?: string;
49
+ buildNumber?: string;
50
+ locale?: string;
51
+ region?: string;
52
+ timezone?: string;
53
+ screenWidth?: number;
54
+ screenHeight?: number;
55
+ screenScale?: number;
56
+ fontScale?: number;
57
+ isLandscape?: boolean;
58
+ previousAnonymousUserId?: string;
59
+ signUpMethod?: string;
60
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * User Document Service
3
+ * Generic service for creating/updating user documents in Firestore
4
+ */
5
+
6
+ import { doc, getDoc, setDoc, serverTimestamp } from "firebase/firestore";
7
+ import type { User } from "firebase/auth";
8
+ import { getFirestore } from "@umituz/react-native-firebase";
9
+ import type {
10
+ UserDocumentUser,
11
+ UserDocumentConfig,
12
+ UserDocumentExtras,
13
+ } from "./UserDocument.types";
14
+ import {
15
+ getSignUpMethod,
16
+ buildBaseData,
17
+ buildCreateData,
18
+ buildUpdateData,
19
+ } from "../utils/userDocumentBuilder.util";
20
+
21
+ export type {
22
+ UserDocumentUser,
23
+ UserDocumentConfig,
24
+ UserDocumentExtras,
25
+ } from "./UserDocument.types";
26
+
27
+ declare const __DEV__: boolean;
28
+
29
+ let userDocumentConfig: UserDocumentConfig = {};
30
+
31
+ export function configureUserDocumentService(config: UserDocumentConfig): void {
32
+ userDocumentConfig = { ...config };
33
+ }
34
+
35
+ export async function ensureUserDocument(
36
+ user: UserDocumentUser | User,
37
+ extras?: UserDocumentExtras,
38
+ ): Promise<boolean> {
39
+ const db = getFirestore();
40
+ if (!db || !user.uid) return false;
41
+
42
+ try {
43
+ let allExtras = extras || {};
44
+ if (userDocumentConfig.collectExtras) {
45
+ const collectedExtras = await userDocumentConfig.collectExtras();
46
+ allExtras = { ...collectedExtras, ...allExtras };
47
+ }
48
+
49
+ if (!allExtras.signUpMethod) allExtras.signUpMethod = getSignUpMethod(user);
50
+
51
+ const collectionName = userDocumentConfig.collectionName || "users";
52
+ const userRef = doc(db, collectionName, user.uid);
53
+ const userDoc = await getDoc(userRef);
54
+ const baseData = buildBaseData(user, allExtras);
55
+
56
+ const docData = !userDoc.exists()
57
+ ? buildCreateData(baseData, userDocumentConfig.extraFields, allExtras)
58
+ : buildUpdateData(baseData, allExtras);
59
+
60
+ await setDoc(userRef, docData, { merge: true });
61
+ return true;
62
+ } catch (error) {
63
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
64
+ console.error("[UserDocumentService] Failed:", error);
65
+ }
66
+ return false;
67
+ }
68
+ }
69
+
70
+ export async function markUserDeleted(userId: string): Promise<boolean> {
71
+ const db = getFirestore();
72
+ if (!db || !userId) return false;
73
+
74
+ try {
75
+ const userRef = doc(db, userDocumentConfig.collectionName || "users", userId);
76
+ await setDoc(userRef, {
77
+ isDeleted: true,
78
+ deletedAt: serverTimestamp(),
79
+ updatedAt: serverTimestamp(),
80
+ }, { merge: true });
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
@@ -16,7 +16,11 @@ export interface ValidationConfig {
16
16
  }
17
17
 
18
18
  export const DEFAULT_VAL_CONFIG: ValidationConfig = {
19
- emailRegex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
19
+ // More robust email validation:
20
+ // - Local part: alphanumeric, dots (not consecutive), hyphens, underscores, plus
21
+ // - Domain: alphanumeric and hyphens
22
+ // - TLD: at least 2 characters
23
+ emailRegex: /^[a-zA-Z0-9]([a-zA-Z0-9._+-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/,
20
24
  displayNameMinLength: 2,
21
25
  };
22
26
 
@@ -30,7 +34,7 @@ export function validateEmail(
30
34
  }
31
35
 
32
36
  export function validatePasswordForLogin(password: string): ValidationResult {
33
- if (!password || password.length === 0) return { isValid: false, error: "auth.validation.passwordRequired" };
37
+ if (!password || password.trim().length === 0) return { isValid: false, error: "auth.validation.passwordRequired" };
34
38
  return { isValid: true };
35
39
  }
36
40
 
@@ -38,7 +42,7 @@ export function validatePasswordForRegister(
38
42
  password: string,
39
43
  config: PasswordConfig,
40
44
  ): PasswordStrengthResult {
41
- if (!password) {
45
+ if (!password || password.trim().length === 0) {
42
46
  return { isValid: false, error: "auth.validation.passwordRequired", requirements: { hasMinLength: false } };
43
47
  }
44
48
 
@@ -52,7 +56,7 @@ export function validatePasswordForRegister(
52
56
  }
53
57
 
54
58
  export function validatePasswordConfirmation(password: string, confirm: string): ValidationResult {
55
- if (!confirm) return { isValid: false, error: "auth.validation.confirmPasswordRequired" };
59
+ if (!confirm || confirm.trim().length === 0) return { isValid: false, error: "auth.validation.confirmPasswordRequired" };
56
60
  if (password !== confirm) return { isValid: false, error: "auth.validation.passwordsDoNotMatch" };
57
61
  return { isValid: true };
58
62
  }
@@ -35,14 +35,18 @@ export function extractProvider(user: FirebaseUserLike): AuthProviderType {
35
35
  // Filter out null providers and find the first valid one
36
36
  const validProviders = providerData.filter((p): p is ProviderData => p != null);
37
37
 
38
+ if (validProviders.length === 0) {
39
+ return "unknown";
40
+ }
41
+
38
42
  const googleProvider = validProviders.find((p) => p.providerId === "google.com");
39
- if (googleProvider && googleProvider.providerId) return "google.com";
43
+ if (googleProvider) return "google.com";
40
44
 
41
45
  const appleProvider = validProviders.find((p) => p.providerId === "apple.com");
42
- if (appleProvider && appleProvider.providerId) return "apple.com";
46
+ if (appleProvider) return "apple.com";
43
47
 
44
48
  const passwordProvider = validProviders.find((p) => p.providerId === "password");
45
- if (passwordProvider && passwordProvider.providerId) return "password";
49
+ if (passwordProvider) return "password";
46
50
 
47
51
  return "unknown";
48
52
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { User } from "firebase/auth";
7
+ import { ensureUserDocument } from "../services/UserDocumentService";
7
8
  import { detectConversion, type ConversionState } from "./authConversionDetector";
8
9
 
9
10
  export interface AuthStateHandlerOptions {
@@ -40,6 +41,12 @@ export function createAuthStateHandler(
40
41
  }
41
42
  }
42
43
 
44
+ const extras = conversion.isConversion && state.current.previousUserId
45
+ ? { previousAnonymousUserId: state.current.previousUserId }
46
+ : undefined;
47
+
48
+ await ensureUserDocument(user, extras);
49
+
43
50
  state.current = {
44
51
  previousUserId: currentUserId,
45
52
  wasAnonymous: isCurrentlyAnonymous,
@@ -3,118 +3,21 @@
3
3
  * Centralized configuration for mapping Firebase error codes to domain errors
4
4
  */
5
5
 
6
- import {
7
- AuthError,
8
- AuthConfigurationError,
9
- AuthEmailAlreadyInUseError,
10
- AuthInvalidEmailError,
11
- AuthWeakPasswordError,
12
- AuthUserNotFoundError,
13
- AuthWrongPasswordError,
14
- AuthNetworkError,
15
- } from "../../../domain/errors/AuthError";
6
+ import type { ErrorMapping } from "./mappings/errorMapping.types";
7
+ import { AUTH_ERROR_MAPPINGS } from "./mappings/authErrorMappings";
8
+ import { CONFIG_ERROR_MAPPINGS } from "./mappings/configErrorMappings";
9
+ import { NETWORK_ERROR_MAPPINGS } from "./mappings/networkErrorMappings";
10
+ import { ACTION_CODE_ERROR_MAPPINGS } from "./mappings/actionCodeErrorMappings";
16
11
 
17
- export type ErrorConstructor = new (message?: string) => Error;
18
- export type ErrorFactory = () => Error;
19
-
20
- export interface ErrorMapping {
21
- type: "class" | "factory";
22
- create: ErrorConstructor | ErrorFactory;
23
- }
12
+ // Re-export types for backward compatibility
13
+ export type { ErrorConstructor, ErrorFactory, ErrorMapping } from "./mappings/errorMapping.types";
24
14
 
25
15
  /**
26
- * Firebase error code to domain error mapping
16
+ * Combined Firebase error code to domain error mapping
27
17
  */
28
18
  export const ERROR_CODE_MAP: Record<string, ErrorMapping> = {
29
- "auth/email-already-in-use": {
30
- type: "class",
31
- create: AuthEmailAlreadyInUseError,
32
- },
33
- "auth/invalid-email": {
34
- type: "class",
35
- create: AuthInvalidEmailError,
36
- },
37
- "auth/weak-password": {
38
- type: "class",
39
- create: AuthWeakPasswordError,
40
- },
41
- "auth/user-disabled": {
42
- type: "factory",
43
- create: () => new AuthError(
44
- "Your account has been disabled. Please contact support.",
45
- "AUTH_USER_DISABLED"
46
- ),
47
- },
48
- "auth/user-not-found": {
49
- type: "class",
50
- create: AuthUserNotFoundError,
51
- },
52
- "auth/wrong-password": {
53
- type: "class",
54
- create: AuthWrongPasswordError,
55
- },
56
- "auth/invalid-credential": {
57
- type: "factory",
58
- create: () => new AuthError(
59
- "Invalid email or password. Please check your credentials.",
60
- "AUTH_INVALID_CREDENTIAL"
61
- ),
62
- },
63
- "auth/invalid-login-credentials": {
64
- type: "factory",
65
- create: () => new AuthError(
66
- "Invalid email or password. Please check your credentials.",
67
- "AUTH_INVALID_CREDENTIAL"
68
- ),
69
- },
70
- "auth/network-request-failed": {
71
- type: "class",
72
- create: AuthNetworkError,
73
- },
74
- "auth/too-many-requests": {
75
- type: "factory",
76
- create: () => new AuthError(
77
- "Too many failed attempts. Please wait a few minutes and try again.",
78
- "AUTH_TOO_MANY_REQUESTS"
79
- ),
80
- },
81
- "auth/configuration-not-found": {
82
- type: "factory",
83
- create: () => new AuthConfigurationError(
84
- "Authentication is not properly configured. Please contact support."
85
- ),
86
- },
87
- "auth/app-not-authorized": {
88
- type: "factory",
89
- create: () => new AuthConfigurationError(
90
- "Authentication is not properly configured. Please contact support."
91
- ),
92
- },
93
- "auth/operation-not-allowed": {
94
- type: "factory",
95
- create: () => new AuthConfigurationError(
96
- "Email/password authentication is not enabled. Please contact support."
97
- ),
98
- },
99
- "auth/requires-recent-login": {
100
- type: "factory",
101
- create: () => new AuthError(
102
- "Please sign in again to complete this action.",
103
- "AUTH_REQUIRES_RECENT_LOGIN"
104
- ),
105
- },
106
- "auth/expired-action-code": {
107
- type: "factory",
108
- create: () => new AuthError(
109
- "This link has expired. Please request a new one.",
110
- "AUTH_EXPIRED_ACTION_CODE"
111
- ),
112
- },
113
- "auth/invalid-action-code": {
114
- type: "factory",
115
- create: () => new AuthError(
116
- "This link is invalid. Please request a new one.",
117
- "AUTH_INVALID_ACTION_CODE"
118
- ),
119
- },
19
+ ...AUTH_ERROR_MAPPINGS,
20
+ ...CONFIG_ERROR_MAPPINGS,
21
+ ...NETWORK_ERROR_MAPPINGS,
22
+ ...ACTION_CODE_ERROR_MAPPINGS,
120
23
  };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Action Code Error Mappings
3
+ * Email verification, password reset link errors
4
+ */
5
+
6
+ import { AuthError } from "../../../../domain/errors/AuthError";
7
+ import type { ErrorMapping } from "./errorMapping.types";
8
+
9
+ export const ACTION_CODE_ERROR_MAPPINGS: Record<string, ErrorMapping> = {
10
+ "auth/expired-action-code": {
11
+ type: "factory",
12
+ create: () =>
13
+ new AuthError(
14
+ "This link has expired. Please request a new one.",
15
+ "AUTH_EXPIRED_ACTION_CODE"
16
+ ),
17
+ },
18
+ "auth/invalid-action-code": {
19
+ type: "factory",
20
+ create: () =>
21
+ new AuthError(
22
+ "This link is invalid. Please request a new one.",
23
+ "AUTH_INVALID_ACTION_CODE"
24
+ ),
25
+ },
26
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Authentication Error Mappings
3
+ * Email, password, and credential related errors
4
+ */
5
+
6
+ import {
7
+ AuthError,
8
+ AuthEmailAlreadyInUseError,
9
+ AuthInvalidEmailError,
10
+ AuthWeakPasswordError,
11
+ AuthUserNotFoundError,
12
+ AuthWrongPasswordError,
13
+ } from "../../../../domain/errors/AuthError";
14
+ import type { ErrorMapping } from "./errorMapping.types";
15
+
16
+ export const AUTH_ERROR_MAPPINGS: Record<string, ErrorMapping> = {
17
+ "auth/email-already-in-use": {
18
+ type: "class",
19
+ create: AuthEmailAlreadyInUseError,
20
+ },
21
+ "auth/invalid-email": {
22
+ type: "class",
23
+ create: AuthInvalidEmailError,
24
+ },
25
+ "auth/weak-password": {
26
+ type: "class",
27
+ create: AuthWeakPasswordError,
28
+ },
29
+ "auth/user-disabled": {
30
+ type: "factory",
31
+ create: () =>
32
+ new AuthError(
33
+ "Your account has been disabled. Please contact support.",
34
+ "AUTH_USER_DISABLED"
35
+ ),
36
+ },
37
+ "auth/user-not-found": {
38
+ type: "class",
39
+ create: AuthUserNotFoundError,
40
+ },
41
+ "auth/wrong-password": {
42
+ type: "class",
43
+ create: AuthWrongPasswordError,
44
+ },
45
+ "auth/invalid-credential": {
46
+ type: "factory",
47
+ create: () =>
48
+ new AuthError(
49
+ "Invalid email or password. Please check your credentials.",
50
+ "AUTH_INVALID_CREDENTIAL"
51
+ ),
52
+ },
53
+ "auth/invalid-login-credentials": {
54
+ type: "factory",
55
+ create: () =>
56
+ new AuthError(
57
+ "Invalid email or password. Please check your credentials.",
58
+ "AUTH_INVALID_CREDENTIAL"
59
+ ),
60
+ },
61
+ "auth/requires-recent-login": {
62
+ type: "factory",
63
+ create: () =>
64
+ new AuthError(
65
+ "Please sign in again to complete this action.",
66
+ "AUTH_REQUIRES_RECENT_LOGIN"
67
+ ),
68
+ },
69
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Configuration Error Mappings
3
+ * Firebase configuration and setup errors
4
+ */
5
+
6
+ import { AuthConfigurationError } from "../../../../domain/errors/AuthError";
7
+ import type { ErrorMapping } from "./errorMapping.types";
8
+
9
+ export const CONFIG_ERROR_MAPPINGS: Record<string, ErrorMapping> = {
10
+ "auth/configuration-not-found": {
11
+ type: "factory",
12
+ create: () =>
13
+ new AuthConfigurationError(
14
+ "Authentication is not properly configured. Please contact support."
15
+ ),
16
+ },
17
+ "auth/app-not-authorized": {
18
+ type: "factory",
19
+ create: () =>
20
+ new AuthConfigurationError(
21
+ "Authentication is not properly configured. Please contact support."
22
+ ),
23
+ },
24
+ "auth/operation-not-allowed": {
25
+ type: "factory",
26
+ create: () =>
27
+ new AuthConfigurationError(
28
+ "Email/password authentication is not enabled. Please contact support."
29
+ ),
30
+ },
31
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Error Mapping Types
3
+ * Shared types for error code mappings
4
+ */
5
+
6
+ export type ErrorConstructor = new (message?: string) => Error;
7
+ export type ErrorFactory = () => Error;
8
+
9
+ export interface ErrorMapping {
10
+ type: "class" | "factory";
11
+ create: ErrorConstructor | ErrorFactory;
12
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Network Error Mappings
3
+ * Network and rate limit errors
4
+ */
5
+
6
+ import { AuthError, AuthNetworkError } from "../../../../domain/errors/AuthError";
7
+ import type { ErrorMapping } from "./errorMapping.types";
8
+
9
+ export const NETWORK_ERROR_MAPPINGS: Record<string, ErrorMapping> = {
10
+ "auth/network-request-failed": {
11
+ type: "class",
12
+ create: AuthNetworkError,
13
+ },
14
+ "auth/too-many-requests": {
15
+ type: "factory",
16
+ create: () =>
17
+ new AuthError(
18
+ "Too many failed attempts. Please wait a few minutes and try again.",
19
+ "AUTH_TOO_MANY_REQUESTS"
20
+ ),
21
+ },
22
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Anonymous Mode Handler
3
+ * Handles anonymous sign-in flow
4
+ */
5
+
6
+ import type { Auth } from "firebase/auth";
7
+ import type { AuthActions } from "../../../types/auth-store.types";
8
+ import { createAnonymousSignInHandler } from "./anonymousSignInHandler";
9
+ import {
10
+ startAnonymousSignIn,
11
+ completeAnonymousSignIn,
12
+ } from "./listenerState.util";
13
+
14
+ type Store = AuthActions & { isAnonymous: boolean };
15
+
16
+ /**
17
+ * Handle anonymous mode sign-in
18
+ */
19
+ export async function handleAnonymousMode(store: Store, auth: Auth): Promise<void> {
20
+ if (!startAnonymousSignIn()) {
21
+ return; // Already signing in
22
+ }
23
+
24
+ const handleAnonymousSignIn = createAnonymousSignInHandler(auth, store);
25
+
26
+ try {
27
+ await handleAnonymousSignIn();
28
+ } finally {
29
+ completeAnonymousSignIn();
30
+ }
31
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Auth State Change Handler
3
+ * Processes Firebase auth state changes and updates store
4
+ */
5
+
6
+ import type { Auth, User } from "firebase/auth";
7
+ import type { AuthActions } from "../../../types/auth-store.types";
8
+ import { completeInitialization } from "./listenerState.util";
9
+ import { handleAnonymousMode } from "./anonymousHandler";
10
+ import {
11
+ createOrUpdateUserDocument,
12
+ getFirestoreInstance,
13
+ } from "../../repositories/UserDocumentRepository";
14
+
15
+ type Store = AuthActions & { isAnonymous: boolean };
16
+
17
+ /**
18
+ * Handle auth state change from Firebase
19
+ */
20
+ export function handleAuthStateChange(
21
+ user: User | null,
22
+ store: Store,
23
+ auth: Auth,
24
+ autoAnonymousSignIn: boolean,
25
+ onAuthStateChange?: (user: User | null) => void
26
+ ): void {
27
+ try {
28
+ if (!user && autoAnonymousSignIn) {
29
+ // Start anonymous sign-in without blocking
30
+ void handleAnonymousMode(store, auth);
31
+ store.setFirebaseUser(null);
32
+ completeInitialization();
33
+ return;
34
+ }
35
+
36
+ store.setFirebaseUser(user);
37
+ store.setInitialized(true);
38
+
39
+ // Create or update Firestore user document (best practice)
40
+ if (user) {
41
+ createOrUpdateUserDocument(getFirestoreInstance(), user).catch((error) => {
42
+ console.error("[AuthListener] Failed to create/update user document:", error);
43
+ // Don't throw - Firestore failures shouldn't block auth state updates
44
+ });
45
+ }
46
+
47
+ // Handle conversion from anonymous
48
+ if (user && !user.isAnonymous && store.isAnonymous) {
49
+ store.setIsAnonymous(false);
50
+ }
51
+
52
+ onAuthStateChange?.(user);
53
+ } catch (error) {
54
+ console.error("[AuthListener] Error handling auth state change:", error);
55
+ // Ensure we don't leave the app in a bad state
56
+ store.setInitialized(true);
57
+ store.setLoading(false);
58
+ }
59
+ }