@umituz/react-native-firebase 2.4.4 → 2.4.6

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.6",
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> {
@@ -67,16 +67,33 @@ export async function deleteCurrentUser(
67
67
  if (typeof __DEV__ !== "undefined" && __DEV__) {
68
68
  console.log("[deleteCurrentUser] attemptReauth result:", reauth);
69
69
  }
70
- if (reauth) return reauth;
70
+ if (reauth) {
71
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
72
+ console.log("[deleteCurrentUser] Reauth returned result, returning:", reauth);
73
+ }
74
+ return reauth;
75
+ }
76
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
77
+ console.log("[deleteCurrentUser] Reauth returned null, continuing to deleteUser");
78
+ }
71
79
  }
72
80
 
73
81
  try {
82
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
83
+ console.log("[deleteCurrentUser] Calling deleteUser");
84
+ }
74
85
  await deleteUser(user);
86
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
87
+ console.log("[deleteCurrentUser] deleteUser successful");
88
+ }
75
89
  return successResult();
76
90
  } 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";
91
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
92
+ console.error("[deleteCurrentUser] deleteUser failed:", error);
93
+ }
94
+ const errorInfo = toAuthErrorInfo(error);
95
+ const code = errorInfo.code;
96
+ const message = errorInfo.message;
80
97
 
81
98
  const hasCredentials = !!(options.password || options.googleIdToken);
82
99
  const shouldReauth = options.autoReauthenticate === true || hasCredentials;
@@ -149,43 +166,69 @@ async function attemptReauth(user: User, options: AccountDeletionOptions): Promi
149
166
  console.log("[attemptReauth] onPasswordRequired returned:", pwd ? "password received" : "null/cancelled");
150
167
  }
151
168
  if (!pwd) {
169
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
170
+ console.log("[attemptReauth] Password was null/cancelled, returning error");
171
+ }
152
172
  return {
153
173
  success: false,
154
174
  error: { code: "auth/password-reauth-cancelled", message: "Password reauth cancelled" },
155
175
  requiresReauth: true
156
176
  };
157
177
  }
178
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
179
+ console.log("[attemptReauth] Password received, setting password variable");
180
+ }
158
181
  password = pwd;
159
182
  }
160
183
  if (!password) {
184
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
185
+ console.log("[attemptReauth] No password available after callback, returning error");
186
+ }
161
187
  return {
162
188
  success: false,
163
189
  error: { code: "auth/password-reauth", message: "Password required" },
164
190
  requiresReauth: true
165
191
  };
166
192
  }
193
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
194
+ console.log("[attemptReauth] Calling reauthenticateWithPassword");
195
+ }
167
196
  res = await reauthenticateWithPassword(user, password);
197
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
198
+ console.log("[attemptReauth] reauthenticateWithPassword result:", res);
199
+ }
168
200
  } else {
169
201
  return null;
170
202
  }
171
203
 
172
204
  if (res.success) {
205
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
206
+ console.log("[attemptReauth] Reauthentication successful, calling deleteUser");
207
+ }
173
208
  try {
174
- // After reauthentication, get fresh user reference from auth
175
209
  const auth = getFirebaseAuth();
176
210
  const currentUser = auth?.currentUser || user;
177
211
  await deleteUser(currentUser);
212
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
213
+ console.log("[attemptReauth] deleteUser successful after reauth");
214
+ }
178
215
  return successResult();
179
216
  } catch (err: unknown) {
180
- const authErr = err instanceof Error ? (err as { code?: string; message?: string }) : null;
217
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
218
+ console.error("[attemptReauth] deleteUser failed after reauth:", err);
219
+ }
220
+ const errorInfo = toAuthErrorInfo(err);
181
221
  return {
182
222
  success: false,
183
- error: { code: authErr?.code ?? "auth/failed", message: authErr?.message ?? "Deletion failed" },
223
+ error: { code: errorInfo.code, message: errorInfo.message },
184
224
  requiresReauth: false
185
225
  };
186
226
  }
187
227
  }
188
228
 
229
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
230
+ console.log("[attemptReauth] Reauthentication failed, returning error");
231
+ }
189
232
  return {
190
233
  success: false,
191
234
  error: {
@@ -209,12 +252,11 @@ export async function deleteUserAccount(user: User | null): Promise<AccountDelet
209
252
  await deleteUser(user);
210
253
  return successResult();
211
254
  } catch (error: unknown) {
212
- const authErr = error instanceof Error ? (error as { code?: string; message?: string }) : null;
213
- const code = authErr?.code ?? "auth/failed";
255
+ const errorInfo = toAuthErrorInfo(error);
214
256
  return {
215
257
  success: false,
216
- error: { code, message: authErr?.message ?? "Deletion failed" },
217
- requiresReauth: code === "auth/requires-recent-login"
258
+ error: { code: errorInfo.code, message: errorInfo.message },
259
+ requiresReauth: errorInfo.code === "auth/requires-recent-login"
218
260
  };
219
261
  }
220
262
  }
@@ -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'),