@umituz/react-native-firebase 2.4.4 → 2.4.5

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-firebase",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "Unified Firebase package for React Native apps - Auth and Firestore services using Firebase JS SDK (no native modules).",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -13,7 +13,7 @@ import {
13
13
  reauthenticateWithPassword,
14
14
  reauthenticateWithGoogle,
15
15
  } from "./reauthentication.service";
16
- import { successResult, type Result } from "../../../../shared/domain/utils";
16
+ import { successResult, type Result, toAuthErrorInfo } from "../../../../shared/domain/utils";
17
17
  import type { AccountDeletionOptions } from "../../application/ports/reauthentication.types";
18
18
 
19
19
  export interface AccountDeletionResult extends Result<void> {
@@ -74,9 +74,9 @@ export async function deleteCurrentUser(
74
74
  await deleteUser(user);
75
75
  return successResult();
76
76
  } catch (error: unknown) {
77
- const authErr = error instanceof Error ? (error as { code?: string; message?: string }) : null;
78
- const code = authErr?.code ?? "auth/failed";
79
- const message = authErr?.message ?? "Account deletion failed";
77
+ const errorInfo = toAuthErrorInfo(error);
78
+ const code = errorInfo.code;
79
+ const message = errorInfo.message;
80
80
 
81
81
  const hasCredentials = !!(options.password || options.googleIdToken);
82
82
  const shouldReauth = options.autoReauthenticate === true || hasCredentials;
@@ -177,10 +177,10 @@ async function attemptReauth(user: User, options: AccountDeletionOptions): Promi
177
177
  await deleteUser(currentUser);
178
178
  return successResult();
179
179
  } catch (err: unknown) {
180
- const authErr = err instanceof Error ? (err as { code?: string; message?: string }) : null;
180
+ const errorInfo = toAuthErrorInfo(err);
181
181
  return {
182
182
  success: false,
183
- error: { code: authErr?.code ?? "auth/failed", message: authErr?.message ?? "Deletion failed" },
183
+ error: { code: errorInfo.code, message: errorInfo.message },
184
184
  requiresReauth: false
185
185
  };
186
186
  }
@@ -209,12 +209,11 @@ export async function deleteUserAccount(user: User | null): Promise<AccountDelet
209
209
  await deleteUser(user);
210
210
  return successResult();
211
211
  } catch (error: unknown) {
212
- const authErr = error instanceof Error ? (error as { code?: string; message?: string }) : null;
213
- const code = authErr?.code ?? "auth/failed";
212
+ const errorInfo = toAuthErrorInfo(error);
214
213
  return {
215
214
  success: false,
216
- error: { code, message: authErr?.message ?? "Deletion failed" },
217
- requiresReauth: code === "auth/requires-recent-login"
215
+ error: { code: errorInfo.code, message: errorInfo.message },
216
+ requiresReauth: errorInfo.code === "auth/requires-recent-login"
218
217
  };
219
218
  }
220
219
  }
@@ -13,7 +13,7 @@ import {
13
13
  import * as AppleAuthentication from "expo-apple-authentication";
14
14
  import { Platform } from "react-native";
15
15
  import { generateNonce, hashNonce } from "../../../auth/infrastructure/services/crypto.util";
16
- import { executeOperation, failureResultFrom } from "../../../../shared/domain/utils";
16
+ import { executeOperation, failureResultFrom, toAuthErrorInfo } from "../../../../shared/domain/utils";
17
17
  import { isCancelledError } from "../../../../shared/domain/utils/error-handler.util";
18
18
  import type {
19
19
  ReauthenticationResult,
@@ -98,15 +98,11 @@ export async function getAppleReauthCredential(): Promise<ReauthCredentialResult
98
98
  credential
99
99
  };
100
100
  } catch (error: unknown) {
101
- const err = error instanceof Error ? error : new Error(String(error));
102
- const errorInfo = {
103
- code: (err as { code?: string }).code ?? '',
104
- message: err.message
105
- };
106
- const code = isCancelledError(errorInfo) ? "auth/cancelled" : "auth/failed";
101
+ const errorInfo = toAuthErrorInfo(error);
102
+ const code = isCancelledError(errorInfo) ? "auth/cancelled" : errorInfo.code;
107
103
  return {
108
104
  success: false,
109
- error: { code, message: err.message }
105
+ error: { code, message: errorInfo.message }
110
106
  };
111
107
  }
112
108
  }
@@ -64,7 +64,8 @@ class FirebaseAuthClientSingleton extends ServiceClientSingleton<Auth, FirebaseA
64
64
  this.setError(errorMessage);
65
65
  }
66
66
  }
67
- return this.getInstance();
67
+ // Enable auto-initialization flag when getting instance
68
+ return this.getInstance(true);
68
69
  }
69
70
  }
70
71
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { signInAnonymously, type Auth, type User } from "firebase/auth";
7
7
  import { toAnonymousUser, type AnonymousUser } from "../../domain/entities/AnonymousUser";
8
+ import { ERROR_MESSAGES } from "../../../../shared/domain/utils/error-handlers/error-messages";
8
9
 
9
10
  export interface AnonymousAuthResult {
10
11
  readonly user: User;
@@ -18,7 +19,7 @@ export interface AnonymousAuthServiceInterface {
18
19
 
19
20
  export class AnonymousAuthService implements AnonymousAuthServiceInterface {
20
21
  async signInAnonymously(auth: Auth): Promise<AnonymousAuthResult> {
21
- if (!auth) throw new Error("Firebase Auth instance is required");
22
+ if (!auth) throw new Error(ERROR_MESSAGES.AUTH.NOT_INITIALIZED);
22
23
 
23
24
  const currentUser = auth.currentUser;
24
25
 
@@ -31,10 +32,15 @@ export class AnonymousAuthService implements AnonymousAuthServiceInterface {
31
32
  }
32
33
 
33
34
  if (currentUser && !currentUser.isAnonymous) {
34
- throw new Error("A non-anonymous user is already signed in. Sign out first before creating an anonymous session.");
35
+ throw new Error(ERROR_MESSAGES.AUTH.SIGN_OUT_REQUIRED);
35
36
  }
36
37
 
37
38
  const userCredential = await signInAnonymously(auth);
39
+
40
+ if (!userCredential.user.isAnonymous) {
41
+ throw new Error(ERROR_MESSAGES.AUTH.INVALID_USER);
42
+ }
43
+
38
44
  const anonymousUser = toAnonymousUser(userCredential.user);
39
45
  return {
40
46
  user: userCredential.user,
@@ -11,6 +11,7 @@ import {
11
11
  getCurrentUserId,
12
12
  getCurrentUser,
13
13
  } from './auth-utils.service';
14
+ import { ERROR_MESSAGES } from '../../../../shared/domain/utils/error-handlers/error-messages';
14
15
 
15
16
  /**
16
17
  * Auth Guard Service
@@ -24,22 +25,21 @@ export class AuthGuardService {
24
25
  async requireAuthenticatedUser(): Promise<string> {
25
26
  const auth = getFirebaseAuth();
26
27
  if (!auth) {
27
- throw new Error('Firebase Auth is not initialized');
28
+ throw new Error(ERROR_MESSAGES.AUTH.NOT_INITIALIZED);
28
29
  }
29
30
 
30
31
  const userId = getCurrentUserId(auth);
31
32
  if (!userId) {
32
- throw new Error('User must be authenticated to perform this action');
33
+ throw new Error(ERROR_MESSAGES.AUTH.NOT_AUTHENTICATED);
33
34
  }
34
35
 
35
36
  const currentUser = getCurrentUser(auth);
36
37
  if (!currentUser) {
37
- throw new Error('User must be authenticated to perform this action');
38
+ throw new Error(ERROR_MESSAGES.AUTH.NOT_AUTHENTICATED);
38
39
  }
39
40
 
40
- // Check if user is anonymous (guest)
41
41
  if (currentUser.isAnonymous) {
42
- throw new Error('Guest users cannot perform this action');
42
+ throw new Error(ERROR_MESSAGES.AUTH.NON_ANONYMOUS_ONLY);
43
43
  }
44
44
 
45
45
  return userId;
@@ -62,7 +62,11 @@ export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
62
62
  // Listener setup complete - keep mutex locked until cleanup
63
63
  // (setupInProgress remains true to indicate active listener)
64
64
  } catch (error) {
65
- // On error, release the mutex so retry is possible
65
+ // On error, clean up partially initialized listener and release the mutex
66
+ if (unsubscribe) {
67
+ unsubscribe();
68
+ unsubscribe = null;
69
+ }
66
70
  setupInProgress = false;
67
71
  set({ listenerSetup: false, loading: false });
68
72
  throw error; // Re-throw to allow caller to handle
@@ -12,6 +12,19 @@ import type { PaginatedResult, PaginationParams } from "../../types/pagination.t
12
12
  import { BaseQueryRepository } from "./BaseQueryRepository";
13
13
 
14
14
  export abstract class BasePaginatedRepository extends BaseQueryRepository {
15
+ /**
16
+ * Validate cursor format
17
+ * Cursors must be non-empty strings without path separators
18
+ */
19
+ private isValidCursor(cursor: string): boolean {
20
+ if (!cursor || typeof cursor !== 'string') return false;
21
+ // Check for invalid characters (path separators, null bytes)
22
+ if (cursor.includes('/') || cursor.includes('\\') || cursor.includes('\0')) return false;
23
+ // Check length (Firestore document IDs can't be longer than 1500 bytes)
24
+ if (cursor.length > 1500) return false;
25
+ return true;
26
+ }
27
+
15
28
  /**
16
29
  * Execute paginated query with cursor support
17
30
  *
@@ -47,6 +60,12 @@ export abstract class BasePaginatedRepository extends BaseQueryRepository {
47
60
  if (helper.hasCursor(params) && params?.cursor) {
48
61
  cursorKey = params.cursor;
49
62
 
63
+ // Validate cursor format to prevent Firestore errors
64
+ if (!this.isValidCursor(params.cursor)) {
65
+ // Invalid cursor format - return empty result
66
+ return [];
67
+ }
68
+
50
69
  // Fetch cursor document first
51
70
  const cursorDocRef = doc(db, collectionName, params.cursor);
52
71
  const cursorDoc = await getDoc(cursorDocRef);
@@ -11,6 +11,7 @@
11
11
  import type { Firestore, CollectionReference, DocumentReference, DocumentData } from 'firebase/firestore';
12
12
  import { getFirestore, collection, doc } from 'firebase/firestore';
13
13
  import { isQuotaError as checkQuotaError } from '../../utils/quota-error-detector.util';
14
+ import { ERROR_MESSAGES } from '../../../../shared/domain/utils/error-handlers/error-messages';
14
15
 
15
16
  export enum RepositoryState {
16
17
  ACTIVE = 'active',
@@ -75,19 +76,15 @@ export abstract class BaseRepository implements IPathResolver {
75
76
  operation: () => Promise<T>
76
77
  ): Promise<T> {
77
78
  if (this.state === RepositoryState.DESTROYED) {
78
- throw new Error('Repository has been destroyed');
79
+ throw new Error(ERROR_MESSAGES.REPOSITORY.DESTROYED);
79
80
  }
80
81
 
81
82
  try {
82
83
  return await operation();
83
84
  } catch (error) {
84
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
85
-
86
- // Check if this is a quota error
87
85
  if (checkQuotaError(error)) {
88
- throw new Error(`Firestore quota exceeded: ${errorMessage}`);
86
+ throw new Error(ERROR_MESSAGES.FIRESTORE.QUOTA_EXCEEDED);
89
87
  }
90
-
91
88
  throw error;
92
89
  }
93
90
  }
@@ -84,8 +84,8 @@ export function isQuotaError(error: unknown): boolean {
84
84
  if (!error || typeof error !== 'object') return false;
85
85
 
86
86
  if (hasCodeProperty(error)) {
87
- const code = error.code;
88
- return QUOTA_ERROR_CODES.some(
87
+ const code = error.code.toLowerCase();
88
+ return QUOTA_ERROR_CODES.map(c => c.toLowerCase()).some(
89
89
  (c) => code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
90
90
  );
91
91
  }
@@ -94,12 +94,10 @@ export function isQuotaError(error: unknown): boolean {
94
94
  const message = error.message.toLowerCase();
95
95
  return QUOTA_ERROR_MESSAGES.some((m) => {
96
96
  const pattern = m.toLowerCase();
97
- return (
98
- message.includes(` ${pattern} `) ||
99
- message.startsWith(`${pattern} `) ||
100
- message.endsWith(` ${pattern}`) ||
101
- message === pattern
102
- );
97
+ // More flexible matching: handle hyphens, underscores, and no spaces
98
+ const normalizedMessage = message.replace(/[-_\s]+/g, ' ');
99
+ const normalizedPattern = pattern.replace(/[-_\s]+/g, ' ');
100
+ return normalizedMessage.includes(normalizedPattern);
103
101
  });
104
102
  }
105
103
 
@@ -1,18 +1,33 @@
1
- /**
2
- * Error Messages
3
- * User-friendly error messages for common error types
4
- */
1
+ export const ERROR_MESSAGES = {
2
+ AUTH: {
3
+ NOT_INITIALIZED: 'Firebase Auth is not initialized',
4
+ NOT_AUTHENTICATED: 'User must be authenticated',
5
+ ANONYMOUS_ONLY: 'Anonymous users cannot perform this action',
6
+ NON_ANONYMOUS_ONLY: 'Guest users cannot perform this action',
7
+ SIGN_OUT_REQUIRED: 'Sign out first before performing this action',
8
+ NO_USER: 'No user is currently signed in',
9
+ INVALID_USER: 'Invalid user',
10
+ },
11
+ FIRESTORE: {
12
+ NOT_INITIALIZED: 'Firestore is not initialized',
13
+ QUOTA_EXCEEDED: 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.',
14
+ },
15
+ REPOSITORY: {
16
+ DESTROYED: 'Repository has been destroyed',
17
+ },
18
+ SERVICE: {
19
+ NOT_CONFIGURED: 'Service is not configured',
20
+ },
21
+ GENERAL: {
22
+ RETRYABLE: 'Temporary error occurred. Please try again.',
23
+ UNKNOWN: 'Unknown error occurred',
24
+ },
25
+ } as const;
5
26
 
6
- /**
7
- * Get user-friendly quota error message
8
- */
9
27
  export function getQuotaErrorMessage(): string {
10
- return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
28
+ return ERROR_MESSAGES.FIRESTORE.QUOTA_EXCEEDED;
11
29
  }
12
30
 
13
- /**
14
- * Get user-friendly retryable error message
15
- */
16
31
  export function getRetryableErrorMessage(): string {
17
- return 'Temporary error occurred. Please try again.';
32
+ return ERROR_MESSAGES.GENERAL.RETRYABLE;
18
33
  }
@@ -1,41 +1,38 @@
1
- /**
2
- * Batch Async Executors
3
- * Execute multiple operations in parallel or sequence
4
- */
5
-
6
1
  import type { Result, FailureResult } from '../result.util';
7
- import { failureResultFromError, successResult } from '../result.util';
2
+ import { failureResultFromError, successResult, isSuccess } from '../result.util';
8
3
 
9
- /**
10
- * Execute multiple operations in parallel
11
- * Returns success only if all operations succeed
12
- */
13
4
  export async function executeAll<T>(
14
5
  ...operations: (() => Promise<Result<T>>)[]
15
6
  ): Promise<Result<T[]>> {
16
7
  try {
17
8
  const results = await Promise.all(operations.map((op) => op()));
18
- const failures = results.filter((r) => !r.success);
19
- if (failures.length > 0) {
20
- return failures[0] as FailureResult;
9
+
10
+ for (const result of results) {
11
+ if (!result.success && result.error !== undefined) {
12
+ return result as FailureResult;
13
+ }
21
14
  }
22
- const data = results.map((r) => (r as { success: true; data: T }).data);
15
+
16
+ const data: T[] = [];
17
+ for (const result of results) {
18
+ if (isSuccess(result) && result.data !== undefined) {
19
+ data.push(result.data);
20
+ }
21
+ }
22
+
23
23
  return successResult(data);
24
24
  } catch (error) {
25
25
  return failureResultFromError(error);
26
26
  }
27
27
  }
28
28
 
29
- /**
30
- * Execute operations in sequence, stopping at first failure
31
- */
32
29
  export async function executeSequence<T>(
33
30
  ...operations: (() => Promise<Result<T>>)[]
34
31
  ): Promise<Result<void>> {
35
32
  for (const operation of operations) {
36
33
  const result = await operation();
37
- if (!result.success) {
38
- return result as Result<void>;
34
+ if (!result.success && result.error !== undefined) {
35
+ return { success: false, error: result.error };
39
36
  }
40
37
  }
41
38
  return successResult();
@@ -17,7 +17,7 @@ export function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
17
17
  * Check if result is a failure
18
18
  */
19
19
  export function isFailure<T>(result: Result<T>): result is FailureResult {
20
- return result.success === false;
20
+ return result.success === false && result.error !== undefined;
21
21
  }
22
22
 
23
23
  /**
@@ -1,8 +1,4 @@
1
- /**
2
- * Configurable Service Base Class
3
- * Provides common service configuration pattern
4
- * Eliminates code duplication across services
5
- */
1
+ import { ERROR_MESSAGES } from './error-handlers/error-messages';
6
2
 
7
3
  /**
8
4
  * Configuration state management
@@ -78,12 +74,9 @@ export class ConfigurableService<TConfig = unknown> implements IConfigurableServ
78
74
  return true;
79
75
  }
80
76
 
81
- /**
82
- * Get required configuration or throw error
83
- */
84
77
  protected requireConfig(): TConfig {
85
78
  if (!this.configState.config) {
86
- throw new Error('Service is not configured');
79
+ throw new Error(ERROR_MESSAGES.SERVICE.NOT_CONFIGURED);
87
80
  }
88
81
  return this.configState.config;
89
82
  }
@@ -96,7 +96,7 @@ export function loadFirebaseConfig(): FirebaseConfig | null {
96
96
 
97
97
  // Validate authDomain format (should be like "projectId.firebaseapp.com")
98
98
  if (!isValidFirebaseAuthDomain(authDomain)) {
99
- // Invalid format but not a critical error - continue
99
+ return null;
100
100
  }
101
101
 
102
102
  // Build type-safe FirebaseConfig object
@@ -33,6 +33,7 @@ export interface ServiceClientOptions<TInstance, TConfig = unknown> {
33
33
  export class ServiceClientSingleton<TInstance, TConfig = unknown> {
34
34
  protected state: ServiceClientState<TInstance>;
35
35
  private readonly options: ServiceClientOptions<TInstance, TConfig>;
36
+ private initInProgress = false;
36
37
 
37
38
  constructor(options: ServiceClientOptions<TInstance, TConfig>) {
38
39
  this.options = options;
@@ -55,6 +56,12 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
55
56
  return null;
56
57
  }
57
58
 
59
+ // Prevent concurrent initialization attempts
60
+ if (this.initInProgress) {
61
+ return null;
62
+ }
63
+
64
+ this.initInProgress = true;
58
65
  try {
59
66
  const instance = this.options.initializer ? this.options.initializer(config) : null;
60
67
  if (instance) {
@@ -66,6 +73,8 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
66
73
  const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
67
74
  this.state.initializationError = errorMessage;
68
75
  return null;
76
+ } finally {
77
+ this.initInProgress = false;
69
78
  }
70
79
  }
71
80
 
@@ -81,7 +90,13 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
81
90
  return null;
82
91
  }
83
92
 
93
+ // Prevent concurrent auto-initialization attempts
94
+ if (this.initInProgress) {
95
+ return null;
96
+ }
97
+
84
98
  if (autoInit && this.options.autoInitializer) {
99
+ this.initInProgress = true;
85
100
  try {
86
101
  const instance = this.options.autoInitializer();
87
102
  if (instance) {
@@ -92,6 +107,8 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
92
107
  } catch (error) {
93
108
  const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
94
109
  this.state.initializationError = errorMessage;
110
+ } finally {
111
+ this.initInProgress = false;
95
112
  }
96
113
  }
97
114
 
@@ -119,6 +136,7 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
119
136
  this.state.instance = null;
120
137
  this.state.initializationError = null;
121
138
  this.state.isInitialized = false;
139
+ this.initInProgress = false;
122
140
  }
123
141
 
124
142
  /**
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { FirebaseConfig } from '../../../domain/value-objects/FirebaseConfig';
9
9
  import { FirebaseConfigurationError } from '../../../domain/errors/FirebaseError';
10
- import { isValidString, isValidFirebaseApiKey, isValidFirebaseProjectId } from '../../../domain/utils/validation.util';
10
+ import { isValidString, isValidFirebaseApiKey, isValidFirebaseProjectId, isValidFirebaseAuthDomain } from '../../../domain/utils/validation.util';
11
11
 
12
12
  /**
13
13
  * Validation rule interface
@@ -70,7 +70,7 @@ class PlaceholderRule implements ValidationRule {
70
70
  export class FirebaseConfigValidator {
71
71
  private static rules: ValidationRule[] = [
72
72
  new RequiredFieldRule('API Key', config => config.apiKey, isValidFirebaseApiKey),
73
- new RequiredFieldRule('Auth Domain', config => config.authDomain),
73
+ new RequiredFieldRule('Auth Domain', config => config.authDomain, isValidFirebaseAuthDomain),
74
74
  new RequiredFieldRule('Project ID', config => config.projectId, isValidFirebaseProjectId),
75
75
  new PlaceholderRule('API Key', config => config.apiKey, 'your_firebase_api_key'),
76
76
  new PlaceholderRule('Project ID', config => config.projectId, 'your-project-id'),