@umituz/react-native-firebase 1.13.136 → 1.13.138

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.136",
3
+ "version": "1.13.138",
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/type-guards.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
 
@@ -3,6 +3,8 @@
3
3
  * Centralized error handling utilities for Firebase operations
4
4
  */
5
5
 
6
+ import { hasCodeProperty, hasMessageProperty, hasCodeAndMessageProperties } from './type-guards.util';
7
+
6
8
  /**
7
9
  * Standard error structure with code and message
8
10
  */
@@ -11,15 +13,39 @@ export interface ErrorInfo {
11
13
  message: string;
12
14
  }
13
15
 
16
+ /**
17
+ * Quota error codes
18
+ */
19
+ const QUOTA_ERROR_CODES = [
20
+ 'resource-exhausted',
21
+ 'quota-exceeded',
22
+ 'RESOURCE_EXHAUSTED',
23
+ ];
24
+
25
+ /**
26
+ * Quota error message patterns
27
+ */
28
+ const QUOTA_ERROR_MESSAGES = [
29
+ 'quota exceeded',
30
+ 'quota limit',
31
+ 'daily limit',
32
+ 'resource exhausted',
33
+ 'too many requests',
34
+ ];
35
+
36
+ /**
37
+ * Retryable error codes
38
+ */
39
+ const RETRYABLE_ERROR_CODES = ['unavailable', 'deadline-exceeded', 'aborted'];
40
+
14
41
  /**
15
42
  * Convert unknown error to standard error info
16
43
  * Handles Error objects, strings, and unknown types
17
44
  */
18
45
  export function toErrorInfo(error: unknown): ErrorInfo {
19
46
  if (error instanceof Error) {
20
- const firebaseErr = error as { code?: string };
21
47
  return {
22
- code: firebaseErr.code || 'unknown',
48
+ code: hasCodeProperty(error) ? error.code : 'unknown',
23
49
  message: error.message,
24
50
  };
25
51
  }
@@ -35,9 +61,8 @@ export function toErrorInfo(error: unknown): ErrorInfo {
35
61
  */
36
62
  export function toAuthErrorInfo(error: unknown): ErrorInfo {
37
63
  if (error instanceof Error) {
38
- const firebaseErr = error as { code?: string };
39
64
  return {
40
- code: firebaseErr.code || 'auth/failed',
65
+ code: hasCodeProperty(error) && error.code ? error.code : 'auth/failed',
41
66
  message: error.message,
42
67
  };
43
68
  }
@@ -58,13 +83,16 @@ export function hasErrorCode(error: ErrorInfo, code: string): boolean {
58
83
  * Check if error is a cancelled/auth cancelled error
59
84
  */
60
85
  export function isCancelledError(error: ErrorInfo): boolean {
61
- return error.code === 'auth/cancelled' || error.message.includes('ERR_CANCELED');
86
+ return (
87
+ error.code === 'auth/cancelled' ||
88
+ error.message.includes('ERR_CANCELED')
89
+ );
62
90
  }
63
91
 
64
92
  /**
65
- * Check if error is a quota exceeded error
93
+ * Check if error info is a quota error
66
94
  */
67
- export function isQuotaError(error: ErrorInfo): boolean {
95
+ export function isQuotaErrorInfo(error: ErrorInfo): boolean {
68
96
  return (
69
97
  error.code === 'quota-exceeded' ||
70
98
  error.code.includes('quota') ||
@@ -73,7 +101,7 @@ export function isQuotaError(error: ErrorInfo): boolean {
73
101
  }
74
102
 
75
103
  /**
76
- * Check if error is a network error
104
+ * Check if error info is a network error
77
105
  */
78
106
  export function isNetworkError(error: ErrorInfo): boolean {
79
107
  return (
@@ -83,8 +111,68 @@ export function isNetworkError(error: ErrorInfo): boolean {
83
111
  }
84
112
 
85
113
  /**
86
- * Check if error is an authentication error
114
+ * Check if error info is an authentication error
87
115
  */
88
116
  export function isAuthError(error: ErrorInfo): boolean {
89
117
  return error.code.startsWith('auth/');
90
118
  }
119
+
120
+ /**
121
+ * Check if unknown error is a Firestore quota error
122
+ * Enhanced type guard with proper error checking
123
+ */
124
+ export function isQuotaError(error: unknown): boolean {
125
+ if (!error || typeof error !== 'object') return false;
126
+
127
+ if (hasCodeProperty(error)) {
128
+ const code = error.code;
129
+ return QUOTA_ERROR_CODES.some(
130
+ (c) => code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
131
+ );
132
+ }
133
+
134
+ if (hasMessageProperty(error)) {
135
+ const message = error.message.toLowerCase();
136
+ return QUOTA_ERROR_MESSAGES.some((m) => {
137
+ const pattern = m.toLowerCase();
138
+ return (
139
+ message.includes(` ${pattern} `) ||
140
+ message.startsWith(`${pattern} `) ||
141
+ message.endsWith(` ${pattern}`) ||
142
+ message === pattern
143
+ );
144
+ });
145
+ }
146
+
147
+ return false;
148
+ }
149
+
150
+ /**
151
+ * Check if error is retryable
152
+ */
153
+ export function isRetryableError(error: unknown): boolean {
154
+ if (!error || typeof error !== 'object') return false;
155
+
156
+ if (hasCodeProperty(error)) {
157
+ return RETRYABLE_ERROR_CODES.some((code) => error.code.includes(code));
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ /**
164
+ * Get user-friendly quota error message
165
+ */
166
+ export function getQuotaErrorMessage(): string {
167
+ return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
168
+ }
169
+
170
+ /**
171
+ * Get user-friendly retryable error message
172
+ */
173
+ export function getRetryableErrorMessage(): string {
174
+ return 'Temporary error occurred. Please try again.';
175
+ }
176
+
177
+ // Re-export type guards for convenience
178
+ export { hasCodeProperty, hasMessageProperty, hasCodeAndMessageProperties };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Type Guard Utilities
3
+ *
4
+ * Common type guards for Firebase and JavaScript objects.
5
+ * Provides type-safe checking without using 'as' assertions.
6
+ */
7
+
8
+ /**
9
+ * Type guard for objects with a 'code' property of type string
10
+ * Commonly used for Firebase errors and other error objects
11
+ */
12
+ export function hasCodeProperty(error: unknown): error is { code: string } {
13
+ return (
14
+ typeof error === 'object' &&
15
+ error !== null &&
16
+ 'code' in error &&
17
+ typeof (error as { code: unknown }).code === 'string'
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Type guard for objects with a 'message' property of type string
23
+ * Commonly used for Error objects
24
+ */
25
+ export function hasMessageProperty(error: unknown): error is { message: string } {
26
+ return (
27
+ typeof error === 'object' &&
28
+ error !== null &&
29
+ 'message' in error &&
30
+ typeof (error as { message: unknown }).message === 'string'
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Type guard for objects with both 'code' and 'message' properties
36
+ * Commonly used for Firebase error objects
37
+ */
38
+ export function hasCodeAndMessageProperties(error: unknown): error is { code: string; message: string } {
39
+ return hasCodeProperty(error) && hasMessageProperty(error);
40
+ }
41
+
42
+ /**
43
+ * Type guard for objects with a 'name' property of type string
44
+ * Commonly used for Error objects
45
+ */
46
+ export function hasNameProperty(error: unknown): error is { name: string } {
47
+ return (
48
+ typeof error === 'object' &&
49
+ error !== null &&
50
+ 'name' in error &&
51
+ typeof (error as { name: unknown }).name === 'string'
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Type guard for objects with a 'stack' property of type string
57
+ * Commonly used for Error objects
58
+ */
59
+ export function hasStackProperty(error: unknown): error is { stack: string } {
60
+ return (
61
+ typeof error === 'object' &&
62
+ error !== null &&
63
+ 'stack' in error &&
64
+ typeof (error as { stack: unknown }).stack === 'string'
65
+ );
66
+ }
@@ -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;
@@ -9,7 +9,7 @@ import {
9
9
  type Transaction,
10
10
  } from "firebase/firestore";
11
11
  import { getFirestore } from "../../infrastructure/config/FirestoreClient";
12
- import type { Firestore } from "../../infrastructure/config/FirestoreClient";
12
+ import { hasCodeProperty } from "../../../domain/utils/type-guards.util";
13
13
 
14
14
  /**
15
15
  * Execute a transaction with automatic DB instance check.
@@ -23,10 +23,10 @@ export async function runTransaction<T>(
23
23
  throw new Error("[runTransaction] Firestore database is not initialized. Please ensure Firebase is properly initialized before running transactions.");
24
24
  }
25
25
  try {
26
- return await fbRunTransaction(db as Firestore, updateFunction);
26
+ return await fbRunTransaction(db, updateFunction);
27
27
  } catch (error) {
28
28
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
29
- const errorCode = error instanceof Error ? (error as { code?: string }).code : 'unknown';
29
+ const errorCode = hasCodeProperty(error) ? error.code : 'unknown';
30
30
 
31
31
  if (__DEV__) {
32
32
  console.error(`[runTransaction] Transaction failed (Code: ${errorCode}):`, errorMessage);
@@ -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
-