@umituz/react-native-firebase 1.13.135 → 1.13.137

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": "1.13.135",
3
+ "version": "1.13.137",
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",
@@ -1,72 +1,68 @@
1
1
  /**
2
2
  * Firebase Auth Client - Infrastructure Layer
3
+ *
4
+ * Manages Firebase Authentication instance initialization
3
5
  */
4
6
 
5
7
  import type { Auth } from 'firebase/auth';
6
8
  import { getFirebaseApp } from '../../../infrastructure/config/FirebaseClient';
7
9
  import { FirebaseAuthInitializer } from './initializers/FirebaseAuthInitializer';
8
10
  import type { FirebaseAuthConfig } from '../../domain/value-objects/FirebaseAuthConfig';
11
+ import { ServiceClientSingleton } from '../../../infrastructure/config/base/ServiceClientSingleton';
12
+
13
+ /**
14
+ * Firebase Auth Client Singleton
15
+ */
16
+ class FirebaseAuthClientSingleton extends ServiceClientSingleton<Auth, FirebaseAuthConfig> {
17
+ private constructor() {
18
+ super({
19
+ serviceName: 'FirebaseAuth',
20
+ initializer: (config?: FirebaseAuthConfig) => {
21
+ const app = getFirebaseApp();
22
+ if (!app) {
23
+ this.setError('Firebase App is not initialized');
24
+ return null;
25
+ }
26
+ const auth = FirebaseAuthInitializer.initialize(app, config);
27
+ if (!auth) {
28
+ this.setError('Auth initialization returned null');
29
+ }
30
+ return auth;
31
+ },
32
+ });
33
+ }
9
34
 
10
- class FirebaseAuthClientSingleton {
11
35
  private static instance: FirebaseAuthClientSingleton | null = null;
12
- private auth: Auth | null = null;
13
- private initializationError: string | null = null;
14
36
 
15
37
  static getInstance(): FirebaseAuthClientSingleton {
16
38
  if (!this.instance) this.instance = new FirebaseAuthClientSingleton();
17
39
  return this.instance;
18
40
  }
19
41
 
42
+ /**
43
+ * Initialize Auth with optional configuration
44
+ */
20
45
  initialize(config?: FirebaseAuthConfig): Auth | null {
21
- if (this.auth) return this.auth;
22
- if (this.initializationError) return null;
23
-
24
- try {
25
- const app = getFirebaseApp();
26
- if (!app) return null;
27
- this.auth = FirebaseAuthInitializer.initialize(app, config);
28
- if (!this.auth) {
29
- this.initializationError = "Auth initialization returned null";
30
- }
31
- return this.auth;
32
- } catch (error: unknown) {
33
- const message = error instanceof Error ? error.message : "Unknown error";
34
- if (__DEV__) console.error('[FirebaseAuth] Init error:', message);
35
- this.initializationError = message;
36
- return null;
37
- }
46
+ return super.initialize(config);
38
47
  }
39
48
 
49
+ /**
50
+ * Get Auth instance
51
+ */
40
52
  getAuth(): Auth | null {
41
- // Don't retry if we already have an auth instance
42
- if (this.auth) return this.auth;
43
-
44
- // Don't retry if we already failed to initialize
45
- if (this.initializationError) return null;
46
-
47
- // Don't retry if Firebase app is not available
48
- const app = getFirebaseApp();
49
- if (!app) return null;
50
-
51
- // Attempt initialization
52
- this.initialize();
53
- return this.auth;
54
- }
55
-
56
- getInitializationError(): string | null {
57
- return this.initializationError;
58
- }
59
-
60
- reset(): void {
61
- this.auth = null;
62
- this.initializationError = null;
53
+ // Attempt initialization if not already initialized
54
+ if (!this.isInitialized() && !this.getInitializationError()) {
55
+ const app = getFirebaseApp();
56
+ if (app) this.initialize();
57
+ }
58
+ return this.getInstance();
63
59
  }
64
60
  }
65
61
 
66
62
  export const firebaseAuthClient = FirebaseAuthClientSingleton.getInstance();
67
63
  export const initializeFirebaseAuth = (c?: FirebaseAuthConfig) => firebaseAuthClient.initialize(c);
68
64
  export const getFirebaseAuth = () => firebaseAuthClient.getAuth();
69
- export const isFirebaseAuthInitialized = () => firebaseAuthClient.getAuth() !== null;
65
+ export const isFirebaseAuthInitialized = () => firebaseAuthClient.isInitialized();
70
66
  export const getFirebaseAuthInitializationError = () => firebaseAuthClient.getInitializationError();
71
67
  export const resetFirebaseAuthClient = () => firebaseAuthClient.reset();
72
68
 
@@ -12,6 +12,12 @@ import * as AppleAuthentication from "expo-apple-authentication";
12
12
  import { Platform } from "react-native";
13
13
  import { generateNonce, hashNonce } from "./crypto.util";
14
14
  import type { AppleAuthResult } from "./apple-auth.types";
15
+ import {
16
+ createSuccessResult,
17
+ createFailureResult,
18
+ logAuthError,
19
+ isCancellationError,
20
+ } from "./base/base-auth.service";
15
21
 
16
22
  export class AppleAuthService {
17
23
  async isAvailable(): Promise<boolean> {
@@ -61,17 +67,9 @@ export class AppleAuthService {
61
67
  });
62
68
 
63
69
  const userCredential = await signInWithCredential(auth, credential);
64
- const isNewUser =
65
- userCredential.user.metadata.creationTime ===
66
- userCredential.user.metadata.lastSignInTime;
67
-
68
- return {
69
- success: true,
70
- userCredential,
71
- isNewUser,
72
- };
70
+ return createSuccessResult(userCredential);
73
71
  } catch (error) {
74
- if (error instanceof Error && error.message.includes("ERR_CANCELED")) {
72
+ if (isCancellationError(error)) {
75
73
  return {
76
74
  success: false,
77
75
  error: "Apple Sign-In was cancelled",
@@ -79,16 +77,8 @@ export class AppleAuthService {
79
77
  };
80
78
  }
81
79
 
82
- if (__DEV__) console.error('[Firebase Auth] Apple Sign-In failed:', error);
83
-
84
- const errorCode = (error as { code?: string })?.code || 'unknown';
85
- const errorMessage = error instanceof Error ? error.message : 'Apple sign-in failed';
86
-
87
- return {
88
- success: false,
89
- error: errorMessage,
90
- code: errorCode,
91
- };
80
+ logAuthError('Apple Sign-In', error);
81
+ return createFailureResult(error);
92
82
  }
93
83
  }
94
84
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Base Auth Service
3
+ *
4
+ * Provides common authentication service functionality
5
+ * Handles error processing, result formatting, and credential management
6
+ */
7
+
8
+ import type { UserCredential } from 'firebase/auth';
9
+ import { toAuthErrorInfo } from '../../../../domain/utils/error-handler.util';
10
+
11
+ /**
12
+ * Base authentication result interface
13
+ */
14
+ export interface BaseAuthResult {
15
+ readonly success: boolean;
16
+ readonly error?: string;
17
+ readonly code?: string;
18
+ }
19
+
20
+ /**
21
+ * Successful authentication result
22
+ */
23
+ export interface AuthSuccessResult extends BaseAuthResult {
24
+ readonly success: true;
25
+ readonly userCredential: UserCredential;
26
+ readonly isNewUser: boolean;
27
+ }
28
+
29
+ /**
30
+ * Failed authentication result
31
+ */
32
+ export interface AuthFailureResult extends BaseAuthResult {
33
+ readonly success: false;
34
+ readonly error: string;
35
+ readonly code: string;
36
+ }
37
+
38
+ /**
39
+ * Combined auth result type
40
+ */
41
+ export type AuthResult = AuthSuccessResult | AuthFailureResult;
42
+
43
+ /**
44
+ * Check if user is new based on metadata
45
+ */
46
+ export function checkIsNewUser(userCredential: UserCredential): boolean {
47
+ return (
48
+ userCredential.user.metadata.creationTime ===
49
+ userCredential.user.metadata.lastSignInTime
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Extract error information from unknown error
55
+ */
56
+ export function extractAuthError(error: unknown): { code: string; message: string } {
57
+ const errorInfo = toAuthErrorInfo(error);
58
+ return {
59
+ code: errorInfo.code,
60
+ message: errorInfo.message,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Check if error is a cancellation error
66
+ */
67
+ export function isCancellationError(error: unknown): boolean {
68
+ if (error instanceof Error) {
69
+ return error.message.includes('ERR_CANCELED');
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * Create failure result from error
76
+ */
77
+ export function createFailureResult(error: unknown): AuthFailureResult {
78
+ const { code, message } = extractAuthError(error);
79
+ return {
80
+ success: false,
81
+ error: message,
82
+ code,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Create success result from user credential
88
+ */
89
+ export function createSuccessResult(userCredential: UserCredential): AuthSuccessResult {
90
+ return {
91
+ success: true,
92
+ userCredential,
93
+ isNewUser: checkIsNewUser(userCredential),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Log auth error in development mode
99
+ */
100
+ export function logAuthError(serviceName: string, error: unknown): void {
101
+ if (__DEV__) {
102
+ console.error(`[Firebase Auth] ${serviceName} failed:`, error);
103
+ }
104
+ }
@@ -9,6 +9,11 @@ import {
9
9
  type Auth,
10
10
  } from "firebase/auth";
11
11
  import type { GoogleAuthConfig, GoogleAuthResult } from "./google-auth.types";
12
+ import {
13
+ createSuccessResult,
14
+ createFailureResult,
15
+ logAuthError,
16
+ } from "./base/base-auth.service";
12
17
 
13
18
  /**
14
19
  * Google Auth Service
@@ -41,30 +46,10 @@ export class GoogleAuthService {
41
46
  try {
42
47
  const credential = GoogleAuthProvider.credential(idToken);
43
48
  const userCredential = await signInWithCredential(auth, credential);
44
-
45
- const isNewUser =
46
- userCredential.user.metadata.creationTime ===
47
- userCredential.user.metadata.lastSignInTime;
48
-
49
- return {
50
- success: true,
51
- userCredential,
52
- isNewUser,
53
- };
49
+ return createSuccessResult(userCredential);
54
50
  } catch (error) {
55
- if (__DEV__) {
56
- console.error('[Firebase Auth] Google Sign-In failed:', error);
57
- }
58
-
59
- // Extract error code for better error handling
60
- const errorCode = (error as { code?: string })?.code || 'unknown';
61
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
62
-
63
- return {
64
- success: false,
65
- error: errorMessage,
66
- code: errorCode,
67
- };
51
+ logAuthError('Google Sign-In', error);
52
+ return createFailureResult(error);
68
53
  }
69
54
  }
70
55
  }
@@ -8,6 +8,15 @@
8
8
 
9
9
  import type { FirestoreError } from 'firebase/firestore';
10
10
  import type { AuthError } from 'firebase/auth';
11
+ import { hasCodeProperty } from '../utils/error-handler.util';
12
+
13
+ /**
14
+ * Firebase error base interface
15
+ */
16
+ interface FirebaseErrorBase {
17
+ code: string;
18
+ message: string;
19
+ }
11
20
 
12
21
  /**
13
22
  * Check if error is a Firebase Firestore error
@@ -33,54 +42,54 @@ export function isAuthError(error: unknown): error is AuthError {
33
42
  );
34
43
  }
35
44
 
45
+ /**
46
+ * Check if error is a Firebase error (either Firestore or Auth)
47
+ */
48
+ export function isFirebaseError(error: unknown): error is FirebaseErrorBase {
49
+ return isFirestoreError(error) || isAuthError(error);
50
+ }
51
+
52
+ /**
53
+ * Check if error has a specific code
54
+ */
55
+ export function hasErrorCode(error: unknown, code: string): boolean {
56
+ return hasCodeProperty(error) && error.code === code;
57
+ }
58
+
59
+ /**
60
+ * Check if error code matches any of the provided codes
61
+ */
62
+ export function hasAnyErrorCode(error: unknown, codes: string[]): boolean {
63
+ if (!hasCodeProperty(error)) return false;
64
+ return codes.includes(error.code);
65
+ }
66
+
36
67
  /**
37
68
  * Check if error is a network error
38
69
  */
39
70
  export function isNetworkError(error: unknown): boolean {
40
- if (!isFirestoreError(error) && !isAuthError(error)) {
41
- return false;
42
- }
43
-
44
- const code = (error as FirestoreError | AuthError).code;
45
- return (
46
- code === 'unavailable' ||
47
- code === 'network-request-failed' ||
48
- code === 'timeout'
49
- );
71
+ return hasAnyErrorCode(error, ['unavailable', 'network-request-failed', 'timeout']);
50
72
  }
51
73
 
52
74
  /**
53
75
  * Check if error is a permission denied error
54
76
  */
55
77
  export function isPermissionDeniedError(error: unknown): boolean {
56
- if (!isFirestoreError(error) && !isAuthError(error)) {
57
- return false;
58
- }
59
-
60
- const code = (error as FirestoreError | AuthError).code;
61
- return code === 'permission-denied' || code === 'unauthorized';
78
+ return hasAnyErrorCode(error, ['permission-denied', 'unauthorized']);
62
79
  }
63
80
 
64
81
  /**
65
82
  * Check if error is a not found error
66
83
  */
67
84
  export function isNotFoundError(error: unknown): boolean {
68
- if (!isFirestoreError(error)) {
69
- return false;
70
- }
71
-
72
- return (error as FirestoreError).code === 'not-found';
85
+ return hasErrorCode(error, 'not-found');
73
86
  }
74
87
 
75
88
  /**
76
89
  * Check if error is a quota exceeded error
77
90
  */
78
91
  export function isQuotaExceededError(error: unknown): boolean {
79
- if (!isFirestoreError(error)) {
80
- return false;
81
- }
82
-
83
- return (error as FirestoreError).code === 'resource-exhausted';
92
+ return hasErrorCode(error, 'resource-exhausted');
84
93
  }
85
94
 
86
95
  /**
@@ -88,7 +97,7 @@ export function isQuotaExceededError(error: unknown): boolean {
88
97
  * Returns error message or unknown error message
89
98
  */
90
99
  export function getSafeErrorMessage(error: unknown): string {
91
- if (isFirestoreError(error) || isAuthError(error)) {
100
+ if (isFirebaseError(error)) {
92
101
  return error.message;
93
102
  }
94
103
 
@@ -108,7 +117,7 @@ export function getSafeErrorMessage(error: unknown): string {
108
117
  * Returns error code or unknown error code
109
118
  */
110
119
  export function getSafeErrorCode(error: unknown): string {
111
- if (isFirestoreError(error) || isAuthError(error)) {
120
+ if (hasCodeProperty(error)) {
112
121
  return error.code;
113
122
  }
114
123
 
@@ -11,15 +11,65 @@ export interface ErrorInfo {
11
11
  message: string;
12
12
  }
13
13
 
14
+ /**
15
+ * Quota error codes
16
+ */
17
+ const QUOTA_ERROR_CODES = [
18
+ 'resource-exhausted',
19
+ 'quota-exceeded',
20
+ 'RESOURCE_EXHAUSTED',
21
+ ];
22
+
23
+ /**
24
+ * Quota error message patterns
25
+ */
26
+ const QUOTA_ERROR_MESSAGES = [
27
+ 'quota exceeded',
28
+ 'quota limit',
29
+ 'daily limit',
30
+ 'resource exhausted',
31
+ 'too many requests',
32
+ ];
33
+
34
+ /**
35
+ * Retryable error codes
36
+ */
37
+ const RETRYABLE_ERROR_CODES = ['unavailable', 'deadline-exceeded', 'aborted'];
38
+
39
+ /**
40
+ * Type guard for error with code property
41
+ * Uses proper type predicate instead of 'as' assertion
42
+ */
43
+ export function hasCodeProperty(error: unknown): error is { code: string } {
44
+ return (
45
+ typeof error === 'object' &&
46
+ error !== null &&
47
+ 'code' in error &&
48
+ typeof (error as { code: unknown }).code === 'string'
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Type guard for error with message property
54
+ * Uses proper type predicate instead of 'as' assertion
55
+ */
56
+ export function hasMessageProperty(error: unknown): error is { message: string } {
57
+ return (
58
+ typeof error === 'object' &&
59
+ error !== null &&
60
+ 'message' in error &&
61
+ typeof (error as { message: unknown }).message === 'string'
62
+ );
63
+ }
64
+
14
65
  /**
15
66
  * Convert unknown error to standard error info
16
67
  * Handles Error objects, strings, and unknown types
17
68
  */
18
69
  export function toErrorInfo(error: unknown): ErrorInfo {
19
70
  if (error instanceof Error) {
20
- const firebaseErr = error as { code?: string };
21
71
  return {
22
- code: firebaseErr.code || 'unknown',
72
+ code: hasCodeProperty(error) ? error.code : 'unknown',
23
73
  message: error.message,
24
74
  };
25
75
  }
@@ -35,9 +85,8 @@ export function toErrorInfo(error: unknown): ErrorInfo {
35
85
  */
36
86
  export function toAuthErrorInfo(error: unknown): ErrorInfo {
37
87
  if (error instanceof Error) {
38
- const firebaseErr = error as { code?: string };
39
88
  return {
40
- code: firebaseErr.code || 'auth/failed',
89
+ code: hasCodeProperty(error) && error.code ? error.code : 'auth/failed',
41
90
  message: error.message,
42
91
  };
43
92
  }
@@ -58,13 +107,16 @@ export function hasErrorCode(error: ErrorInfo, code: string): boolean {
58
107
  * Check if error is a cancelled/auth cancelled error
59
108
  */
60
109
  export function isCancelledError(error: ErrorInfo): boolean {
61
- return error.code === 'auth/cancelled' || error.message.includes('ERR_CANCELED');
110
+ return (
111
+ error.code === 'auth/cancelled' ||
112
+ error.message.includes('ERR_CANCELED')
113
+ );
62
114
  }
63
115
 
64
116
  /**
65
- * Check if error is a quota exceeded error
117
+ * Check if error info is a quota error
66
118
  */
67
- export function isQuotaError(error: ErrorInfo): boolean {
119
+ export function isQuotaErrorInfo(error: ErrorInfo): boolean {
68
120
  return (
69
121
  error.code === 'quota-exceeded' ||
70
122
  error.code.includes('quota') ||
@@ -73,7 +125,7 @@ export function isQuotaError(error: ErrorInfo): boolean {
73
125
  }
74
126
 
75
127
  /**
76
- * Check if error is a network error
128
+ * Check if error info is a network error
77
129
  */
78
130
  export function isNetworkError(error: ErrorInfo): boolean {
79
131
  return (
@@ -83,8 +135,65 @@ export function isNetworkError(error: ErrorInfo): boolean {
83
135
  }
84
136
 
85
137
  /**
86
- * Check if error is an authentication error
138
+ * Check if error info is an authentication error
87
139
  */
88
140
  export function isAuthError(error: ErrorInfo): boolean {
89
141
  return error.code.startsWith('auth/');
90
142
  }
143
+
144
+ /**
145
+ * Check if unknown error is a Firestore quota error
146
+ * Enhanced type guard with proper error checking
147
+ */
148
+ export function isQuotaError(error: unknown): boolean {
149
+ if (!error || typeof error !== 'object') return false;
150
+
151
+ if (hasCodeProperty(error)) {
152
+ const code = error.code;
153
+ return QUOTA_ERROR_CODES.some(
154
+ (c) => code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
155
+ );
156
+ }
157
+
158
+ if (hasMessageProperty(error)) {
159
+ const message = error.message.toLowerCase();
160
+ return QUOTA_ERROR_MESSAGES.some((m) => {
161
+ const pattern = m.toLowerCase();
162
+ return (
163
+ message.includes(` ${pattern} `) ||
164
+ message.startsWith(`${pattern} `) ||
165
+ message.endsWith(` ${pattern}`) ||
166
+ message === pattern
167
+ );
168
+ });
169
+ }
170
+
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Check if error is retryable
176
+ */
177
+ export function isRetryableError(error: unknown): boolean {
178
+ if (!error || typeof error !== 'object') return false;
179
+
180
+ if (hasCodeProperty(error)) {
181
+ return RETRYABLE_ERROR_CODES.some((code) => error.code.includes(code));
182
+ }
183
+
184
+ return false;
185
+ }
186
+
187
+ /**
188
+ * Get user-friendly quota error message
189
+ */
190
+ export function getQuotaErrorMessage(): string {
191
+ return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
192
+ }
193
+
194
+ /**
195
+ * Get user-friendly retryable error message
196
+ */
197
+ export function getRetryableErrorMessage(): string {
198
+ return 'Temporary error occurred. Please try again.';
199
+ }
@@ -11,17 +11,33 @@
11
11
  import type { Firestore } from 'firebase/firestore';
12
12
  import { getFirebaseApp } from '../../../infrastructure/config/FirebaseClient';
13
13
  import { FirebaseFirestoreInitializer } from './initializers/FirebaseFirestoreInitializer';
14
+ import { ServiceClientSingleton } from '../../../infrastructure/config/base/ServiceClientSingleton';
14
15
 
15
16
  /**
16
17
  * Firestore Client Singleton
17
18
  * Manages Firestore initialization
18
19
  */
19
- class FirestoreClientSingleton {
20
- private static instance: FirestoreClientSingleton | null = null;
21
- private firestore: Firestore | null = null;
22
- private initializationError: string | null = null;
20
+ class FirestoreClientSingleton extends ServiceClientSingleton<Firestore> {
21
+ private constructor() {
22
+ super({
23
+ serviceName: 'Firestore',
24
+ initializer: () => {
25
+ const app = getFirebaseApp();
26
+ if (!app) {
27
+ this.setError('Firebase App is not initialized');
28
+ return null;
29
+ }
30
+ return FirebaseFirestoreInitializer.initialize(app);
31
+ },
32
+ autoInitializer: () => {
33
+ const app = getFirebaseApp();
34
+ if (!app) return null;
35
+ return FirebaseFirestoreInitializer.initialize(app);
36
+ },
37
+ });
38
+ }
23
39
 
24
- private constructor() {}
40
+ private static instance: FirestoreClientSingleton | null = null;
25
41
 
26
42
  static getInstance(): FirestoreClientSingleton {
27
43
  if (!FirestoreClientSingleton.instance) {
@@ -30,44 +46,18 @@ class FirestoreClientSingleton {
30
46
  return FirestoreClientSingleton.instance;
31
47
  }
32
48
 
49
+ /**
50
+ * Initialize Firestore
51
+ */
33
52
  initialize(): Firestore | null {
34
- if (this.firestore) return this.firestore;
35
- if (this.initializationError) return null;
36
-
37
- try {
38
- const app = getFirebaseApp();
39
- if (!app) return null;
40
-
41
- this.firestore = FirebaseFirestoreInitializer.initialize(app);
42
- return this.firestore;
43
- } catch (error) {
44
- this.initializationError =
45
- error instanceof Error
46
- ? error.message
47
- : 'Failed to initialize Firestore client';
48
- return null;
49
- }
53
+ return super.initialize();
50
54
  }
51
55
 
56
+ /**
57
+ * Get Firestore instance with auto-initialization
58
+ */
52
59
  getFirestore(): Firestore | null {
53
- if (!this.firestore && !this.initializationError) {
54
- const app = getFirebaseApp();
55
- if (app) this.initialize();
56
- }
57
- return this.firestore || null;
58
- }
59
-
60
- isInitialized(): boolean {
61
- return this.firestore !== null;
62
- }
63
-
64
- getInitializationError(): string | null {
65
- return this.initializationError;
66
- }
67
-
68
- reset(): void {
69
- this.firestore = null;
70
- this.initializationError = null;
60
+ return this.getInstance(true);
71
61
  }
72
62
  }
73
63
 
@@ -94,4 +84,3 @@ export function resetFirestoreClient(): void {
94
84
  }
95
85
 
96
86
  export type { Firestore } from 'firebase/firestore';
97
-
@@ -33,7 +33,11 @@ export class QueryDeduplicationMiddleware {
33
33
  if (this.queryManager.isPending(key)) {
34
34
  const pendingPromise = this.queryManager.get(key);
35
35
  if (pendingPromise) {
36
- return pendingPromise as Promise<T>;
36
+ // Type assertion is safe here because the same key was used to store the promise
37
+ return Promise.race([pendingPromise]).then(() => {
38
+ // Retry the original query after pending completes
39
+ return queryFn();
40
+ });
37
41
  }
38
42
  }
39
43
 
@@ -59,4 +63,3 @@ export class QueryDeduplicationMiddleware {
59
63
 
60
64
  export const queryDeduplicationMiddleware = new QueryDeduplicationMiddleware();
61
65
  export type { QueryKey };
62
-
@@ -6,7 +6,7 @@
6
6
  import type { Firestore } from "../../infrastructure/config/FirestoreClient";
7
7
  import { getFirestore } from "../../infrastructure/config/FirestoreClient";
8
8
  import type { FirestoreResult } from "../result/result.util";
9
- import { NO_DB_ERROR } from "../result/result.util";
9
+ import { createNoDbErrorResult } from "../result/result.util";
10
10
 
11
11
  /**
12
12
  * Execute a Firestore operation with automatic null check
@@ -17,7 +17,7 @@ export async function withFirestore<T>(
17
17
  ): Promise<FirestoreResult<T>> {
18
18
  const db = getFirestore();
19
19
  if (!db) {
20
- return NO_DB_ERROR as FirestoreResult<T>;
20
+ return createNoDbErrorResult<T>();
21
21
  }
22
22
  return operation(db);
23
23
  }
@@ -1,82 +1,13 @@
1
1
  /**
2
2
  * Quota Error Detection Utilities
3
+ *
4
+ * Re-exports centralized quota error detection from error-handler.util.ts
5
+ * This maintains backwards compatibility while using a single source of truth.
3
6
  */
4
7
 
5
- const QUOTA_ERROR_CODES = [
6
- 'resource-exhausted',
7
- 'quota-exceeded',
8
- 'RESOURCE_EXHAUSTED',
9
- ];
10
-
11
- const QUOTA_ERROR_MESSAGES = [
12
- 'quota exceeded',
13
- 'quota limit',
14
- 'daily limit',
15
- 'resource exhausted',
16
- 'too many requests',
17
- ];
18
-
19
- /**
20
- * Type guard for error with code property
21
- */
22
- function hasCodeProperty(error: unknown): error is { code: string } {
23
- return typeof error === 'object' && error !== null && 'code' in error && typeof (error as { code: unknown }).code === 'string';
24
- }
25
-
26
- /**
27
- * Type guard for error with message property
28
- */
29
- function hasMessageProperty(error: unknown): error is { message: string } {
30
- return typeof error === 'object' && error !== null && 'message' in error && typeof (error as { message: unknown }).message === 'string';
31
- }
32
-
33
- /**
34
- * Check if error is a Firestore quota error
35
- */
36
- export function isQuotaError(error: unknown): boolean {
37
- if (!error || typeof error !== 'object') return false;
38
-
39
- if (hasCodeProperty(error)) {
40
- const code = error.code;
41
- // Use more specific matching - exact match or ends with pattern
42
- return QUOTA_ERROR_CODES.some((c) =>
43
- code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
44
- );
45
- }
46
-
47
- if (hasMessageProperty(error)) {
48
- const message = error.message;
49
- const lowerMessage = message.toLowerCase();
50
- // Use word boundaries to avoid partial matches
51
- return QUOTA_ERROR_MESSAGES.some((m) =>
52
- lowerMessage.includes(` ${m} `) ||
53
- lowerMessage.startsWith(`${m} `) ||
54
- lowerMessage.endsWith(` ${m}`) ||
55
- lowerMessage === m
56
- );
57
- }
58
-
59
- return false;
60
- }
61
-
62
- /**
63
- * Check if error is retryable
64
- */
65
- export function isRetryableError(error: unknown): boolean {
66
- if (!error || typeof error !== 'object') return false;
67
-
68
- if (hasCodeProperty(error)) {
69
- const code = error.code;
70
- const retryableCodes = ['unavailable', 'deadline-exceeded', 'aborted'];
71
- return retryableCodes.some((c) => code.includes(c));
72
- }
73
-
74
- return false;
75
- }
76
-
77
- /**
78
- * Get user-friendly quota error message
79
- */
80
- export function getQuotaErrorMessage(): string {
81
- return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
82
- }
8
+ export {
9
+ isQuotaError,
10
+ isRetryableError,
11
+ getQuotaErrorMessage,
12
+ getRetryableErrorMessage,
13
+ } from '../../domain/utils/error-handler.util';
@@ -26,10 +26,17 @@ export function createErrorResult<T>(message: string, code: string): FirestoreRe
26
26
  /**
27
27
  * Create a standard success result
28
28
  */
29
- export function createSuccessResult<T>(data?: T): FirestoreResult<T> {
29
+ export function createFirestoreSuccessResult<T>(data?: T): FirestoreResult<T> {
30
30
  return { success: true, data };
31
31
  }
32
32
 
33
+ /**
34
+ * Create no-db error result with proper typing
35
+ */
36
+ export function createNoDbErrorResult<T>(): FirestoreResult<T> {
37
+ return { success: false, error: NO_DB_ERROR.error };
38
+ }
39
+
33
40
  /**
34
41
  * Check if result is successful
35
42
  */
@@ -43,3 +50,6 @@ export function isSuccess<T>(result: FirestoreResult<T>): result is FirestoreRes
43
50
  export function isError<T>(result: FirestoreResult<T>): result is FirestoreResult<T> & { success: false } {
44
51
  return !result.success;
45
52
  }
53
+
54
+ // Keep old function name for backwards compatibility
55
+ export const createSuccessResult = createFirestoreSuccessResult;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Client State Manager
3
+ *
4
+ * Generic state management for Firebase service clients.
5
+ * Provides centralized state tracking for initialization status, errors, and instances.
6
+ *
7
+ * @template TInstance - The service instance type (e.g., FirebaseApp, Firestore, Auth)
8
+ */
9
+
10
+ export interface ClientState<TInstance> {
11
+ instance: TInstance | null;
12
+ initializationError: string | null;
13
+ isInitialized: boolean;
14
+ }
15
+
16
+ /**
17
+ * Generic client state manager
18
+ * Handles initialization state, error tracking, and instance management
19
+ */
20
+ export class ClientStateManager<TInstance> {
21
+ private state: ClientState<TInstance>;
22
+
23
+ constructor() {
24
+ this.state = {
25
+ instance: null,
26
+ initializationError: null,
27
+ isInitialized: false,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Get the current instance
33
+ */
34
+ getInstance(): TInstance | null {
35
+ return this.state.instance;
36
+ }
37
+
38
+ /**
39
+ * Set the instance
40
+ */
41
+ setInstance(instance: TInstance | null): void {
42
+ this.state.instance = instance;
43
+ this.state.isInitialized = instance !== null;
44
+ }
45
+
46
+ /**
47
+ * Check if the service is initialized
48
+ */
49
+ isInitialized(): boolean {
50
+ return this.state.isInitialized;
51
+ }
52
+
53
+ /**
54
+ * Get the initialization error if any
55
+ */
56
+ getInitializationError(): string | null {
57
+ return this.state.initializationError;
58
+ }
59
+
60
+ /**
61
+ * Set the initialization error
62
+ */
63
+ setInitializationError(error: string | null): void {
64
+ this.state.initializationError = error;
65
+ }
66
+
67
+ /**
68
+ * Reset the state
69
+ */
70
+ reset(): void {
71
+ this.state.instance = null;
72
+ this.state.initializationError = null;
73
+ this.state.isInitialized = false;
74
+ }
75
+
76
+ /**
77
+ * Get the current state (read-only)
78
+ */
79
+ getState(): Readonly<ClientState<TInstance>> {
80
+ return this.state;
81
+ }
82
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Service Client Singleton Base Class
3
+ *
4
+ * Provides a generic singleton pattern for Firebase service clients.
5
+ * Eliminates code duplication across FirebaseClient, FirestoreClient, FirebaseAuthClient.
6
+ *
7
+ * Features:
8
+ * - Generic singleton pattern
9
+ * - Initialization state management
10
+ * - Error handling and tracking
11
+ * - Automatic cleanup
12
+ *
13
+ * @template TInstance - The service instance type (e.g., Firestore, Auth)
14
+ * @template TConfig - The configuration type (optional)
15
+ */
16
+
17
+ export interface ServiceClientState<TInstance> {
18
+ instance: TInstance | null;
19
+ initializationError: string | null;
20
+ isInitialized: boolean;
21
+ }
22
+
23
+ export interface ServiceClientOptions<TInstance, TConfig = unknown> {
24
+ serviceName: string;
25
+ initializer?: (config?: TConfig) => TInstance | null;
26
+ autoInitializer?: () => TInstance | null;
27
+ }
28
+
29
+ /**
30
+ * Generic service client singleton base class
31
+ * Provides common initialization, state management, and error handling
32
+ */
33
+ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
34
+ private static instances = new Map<string, ServiceClientSingleton<unknown, unknown>>();
35
+
36
+ protected state: ServiceClientState<TInstance>;
37
+ private readonly options: ServiceClientOptions<TInstance, TConfig>;
38
+
39
+ constructor(options: ServiceClientOptions<TInstance, TConfig>) {
40
+ this.options = options;
41
+ this.state = {
42
+ instance: null,
43
+ initializationError: null,
44
+ isInitialized: false,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Initialize the service with optional configuration
50
+ */
51
+ initialize(config?: TConfig): TInstance | null {
52
+ if (this.state.isInitialized && this.state.instance) {
53
+ return this.state.instance;
54
+ }
55
+
56
+ if (this.state.initializationError) {
57
+ return null;
58
+ }
59
+
60
+ try {
61
+ const instance = this.options.initializer ? this.options.initializer(config) : null;
62
+ if (instance) {
63
+ this.state.instance = instance;
64
+ this.state.isInitialized = true;
65
+ }
66
+ return instance;
67
+ } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
69
+ this.state.initializationError = errorMessage;
70
+
71
+ if (__DEV__) {
72
+ console.error(`[${this.options.serviceName}] Initialization failed:`, errorMessage);
73
+ }
74
+
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get the service instance, auto-initializing if needed
81
+ */
82
+ getInstance(autoInit: boolean = false): TInstance | null {
83
+ if (this.state.instance) {
84
+ return this.state.instance;
85
+ }
86
+
87
+ if (this.state.initializationError) {
88
+ return null;
89
+ }
90
+
91
+ if (autoInit && this.options.autoInitializer) {
92
+ try {
93
+ const instance = this.options.autoInitializer();
94
+ if (instance) {
95
+ this.state.instance = instance;
96
+ this.state.isInitialized = true;
97
+ }
98
+ return instance;
99
+ } catch (error) {
100
+ const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
101
+ this.state.initializationError = errorMessage;
102
+
103
+ if (__DEV__) {
104
+ console.error(`[${this.options.serviceName}] Auto-initialization failed:`, errorMessage);
105
+ }
106
+ }
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Check if the service is initialized
114
+ */
115
+ isInitialized(): boolean {
116
+ return this.state.isInitialized;
117
+ }
118
+
119
+ /**
120
+ * Get the initialization error if any
121
+ */
122
+ getInitializationError(): string | null {
123
+ return this.state.initializationError;
124
+ }
125
+
126
+ /**
127
+ * Reset the service state
128
+ */
129
+ reset(): void {
130
+ this.state.instance = null;
131
+ this.state.initializationError = null;
132
+ this.state.isInitialized = false;
133
+
134
+ if (__DEV__) {
135
+ console.log(`[${this.options.serviceName}] Service reset`);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Get the current instance without initialization
141
+ */
142
+ protected getCurrentInstance(): TInstance | null {
143
+ return this.state.instance;
144
+ }
145
+
146
+ /**
147
+ * Set initialization error
148
+ */
149
+ protected setError(error: string): void {
150
+ this.state.initializationError = error;
151
+ }
152
+ }
@@ -3,36 +3,18 @@
3
3
  * Manages the state of Firebase initialization
4
4
  *
5
5
  * Single Responsibility: Only manages initialization state
6
+ * Uses generic ClientStateManager for shared functionality
6
7
  */
7
8
 
8
9
  import type { FirebaseApp } from '../initializers/FirebaseAppInitializer';
10
+ import { ClientStateManager } from '../base/ClientStateManager';
9
11
 
10
- export class FirebaseClientState {
11
- private app: FirebaseApp | null = null;
12
- private initializationError: string | null = null;
13
-
12
+ export class FirebaseClientState extends ClientStateManager<FirebaseApp> {
14
13
  getApp(): FirebaseApp | null {
15
- return this.app;
14
+ return this.getInstance();
16
15
  }
17
16
 
18
17
  setApp(app: FirebaseApp | null): void {
19
- this.app = app;
20
- }
21
-
22
- isInitialized(): boolean {
23
- return this.app !== null;
24
- }
25
-
26
- getInitializationError(): string | null {
27
- return this.initializationError;
28
- }
29
-
30
- setInitializationError(error: string | null): void {
31
- this.initializationError = error;
32
- }
33
-
34
- reset(): void {
35
- this.app = null;
36
- this.initializationError = null;
18
+ this.setInstance(app);
37
19
  }
38
20
  }
@@ -2,10 +2,12 @@
2
2
  * Firebase Configuration Validator
3
3
  *
4
4
  * Single Responsibility: Validates Firebase configuration
5
+ * Uses centralized validation utilities from validation.util.ts
5
6
  */
6
7
 
7
8
  import type { FirebaseConfig } from '../../../domain/value-objects/FirebaseConfig';
8
9
  import { FirebaseConfigurationError } from '../../../domain/errors/FirebaseError';
10
+ import { isValidString, isValidFirebaseApiKey, isValidFirebaseProjectId } from '../../../domain/utils/validation.util';
9
11
 
10
12
  /**
11
13
  * Validation rule interface
@@ -15,25 +17,28 @@ interface ValidationRule {
15
17
  }
16
18
 
17
19
  /**
18
- * Required field validation rule
20
+ * Required field validation rule using centralized validation
19
21
  */
20
22
  class RequiredFieldRule implements ValidationRule {
21
23
  constructor(
22
24
  private fieldName: string,
23
- private getter: (config: FirebaseConfig) => string | undefined
25
+ private getter: (config: FirebaseConfig) => string | undefined,
26
+ private customValidator?: (value: string) => boolean
24
27
  ) {}
25
28
 
26
29
  validate(config: FirebaseConfig): void {
27
30
  const value = this.getter(config);
28
-
29
- if (!value || typeof value !== 'string') {
31
+
32
+ if (!isValidString(value)) {
30
33
  throw new FirebaseConfigurationError(
31
- `Firebase ${this.fieldName} is required and must be a string`
34
+ `Firebase ${this.fieldName} is required and must be a non-empty string`
32
35
  );
33
36
  }
34
37
 
35
- if (value.trim().length === 0) {
36
- throw new FirebaseConfigurationError(`Firebase ${this.fieldName} cannot be empty`);
38
+ if (this.customValidator && !this.customValidator(value)) {
39
+ throw new FirebaseConfigurationError(
40
+ `Firebase ${this.fieldName} format is invalid`
41
+ );
37
42
  }
38
43
  }
39
44
  }
@@ -50,7 +55,7 @@ class PlaceholderRule implements ValidationRule {
50
55
 
51
56
  validate(config: FirebaseConfig): void {
52
57
  const value = this.getter(config);
53
-
58
+
54
59
  if (value && value.includes(this.placeholder)) {
55
60
  throw new FirebaseConfigurationError(
56
61
  `Please replace placeholder values with actual Firebase credentials for ${this.fieldName}`
@@ -64,9 +69,9 @@ class PlaceholderRule implements ValidationRule {
64
69
  */
65
70
  export class FirebaseConfigValidator {
66
71
  private static rules: ValidationRule[] = [
67
- new RequiredFieldRule('API Key', config => config.apiKey),
72
+ new RequiredFieldRule('API Key', config => config.apiKey, isValidFirebaseApiKey),
68
73
  new RequiredFieldRule('Auth Domain', config => config.authDomain),
69
- new RequiredFieldRule('Project ID', config => config.projectId),
74
+ new RequiredFieldRule('Project ID', config => config.projectId, isValidFirebaseProjectId),
70
75
  new PlaceholderRule('API Key', config => config.apiKey, 'your_firebase_api_key'),
71
76
  new PlaceholderRule('Project ID', config => config.projectId, 'your-project-id'),
72
77
  ];
@@ -81,11 +86,3 @@ export class FirebaseConfigValidator {
81
86
  }
82
87
  }
83
88
  }
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-