@umituz/react-native-firebase 1.13.118 → 1.13.120

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.118",
3
+ "version": "1.13.120",
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",
package/src/auth/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  getCurrentUserId,
41
41
  getCurrentUser,
42
42
  getCurrentUserIdFromGlobal,
43
+ getCurrentUserFromGlobal,
43
44
  isCurrentUserAuthenticated,
44
45
  isCurrentUserAnonymous,
45
46
  verifyUserId,
@@ -38,7 +38,18 @@ class FirebaseAuthClientSingleton {
38
38
  }
39
39
 
40
40
  getAuth(): Auth | null {
41
- if (!this.auth && !this.initializationError && getFirebaseApp()) this.initialize();
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();
42
53
  return this.auth;
43
54
  }
44
55
 
@@ -31,6 +31,7 @@ export class AppleAuthService {
31
31
  return {
32
32
  success: false,
33
33
  error: "Apple Sign-In is not available on this device",
34
+ code: "unavailable",
34
35
  };
35
36
  }
36
37
 
@@ -46,7 +47,11 @@ export class AppleAuthService {
46
47
  });
47
48
 
48
49
  if (!appleCredential.identityToken) {
49
- return { success: false, error: "No identity token received" };
50
+ return {
51
+ success: false,
52
+ error: "No identity token received",
53
+ code: "no_token"
54
+ };
50
55
  }
51
56
 
52
57
  const provider = new OAuthProvider("apple.com");
@@ -67,12 +72,22 @@ export class AppleAuthService {
67
72
  };
68
73
  } catch (error) {
69
74
  if (error instanceof Error && error.message.includes("ERR_CANCELED")) {
70
- return { success: false, error: "Apple Sign-In was cancelled" };
75
+ return {
76
+ success: false,
77
+ error: "Apple Sign-In was cancelled",
78
+ code: "canceled"
79
+ };
71
80
  }
81
+
72
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
+
73
87
  return {
74
88
  success: false,
75
- error: error instanceof Error ? error.message : "Apple sign-in failed",
89
+ error: errorMessage,
90
+ code: errorCode,
76
91
  };
77
92
  }
78
93
  }
@@ -3,14 +3,29 @@
3
3
  * Type definitions for Apple authentication
4
4
  */
5
5
 
6
- export interface AppleAuthResult {
7
- success: boolean;
8
- user: User;
9
- credential: AppleAuthCredential;
10
- wasAlreadySignedIn: boolean;
11
- error?: string;
6
+ import type { UserCredential } from 'firebase/auth';
7
+
8
+ export interface AppleAuthConfig {
9
+ clientId?: string;
10
+ scope?: string;
11
+ redirectURI?: string;
12
+ state?: string;
13
+ }
14
+
15
+ export interface AppleAuthSuccessResult {
16
+ success: true;
17
+ userCredential: UserCredential;
18
+ isNewUser: boolean;
12
19
  }
13
20
 
21
+ export interface AppleAuthErrorResult {
22
+ success: false;
23
+ error: string;
24
+ code?: string;
25
+ }
26
+
27
+ export type AppleAuthResult = AppleAuthSuccessResult | AppleAuthErrorResult;
28
+
14
29
  export interface AppleAuthCredential {
15
30
  idToken: string;
16
31
  rawNonce: string;
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { User, Auth } from 'firebase/auth';
7
+ import { getFirebaseAuth } from '../config/FirebaseAuthClient';
7
8
 
8
9
  export interface AuthCheckResult {
9
10
  isAuthenticated: boolean;
@@ -67,16 +68,26 @@ export function getCurrentUser(auth: Auth): User | null {
67
68
  * Get current user ID from global auth instance
68
69
  */
69
70
  export function getCurrentUserIdFromGlobal(): string | null {
70
- // This would use the global auth instance
71
- return null;
71
+ const auth = getFirebaseAuth();
72
+
73
+ if (!auth || !auth.currentUser) {
74
+ return null;
75
+ }
76
+
77
+ return auth.currentUser.uid;
72
78
  }
73
79
 
74
80
  /**
75
81
  * Get current user from global auth instance
76
82
  */
77
83
  export function getCurrentUserFromGlobal(): User | null {
78
- // This would use the global auth instance
79
- return null;
84
+ const auth = getFirebaseAuth();
85
+
86
+ if (!auth) {
87
+ return null;
88
+ }
89
+
90
+ return auth.currentUser;
80
91
  }
81
92
 
82
93
  /**
@@ -61,7 +61,13 @@ export function shouldSkipFirestoreQuery(
61
61
 
62
62
  try {
63
63
  if (!auth) {
64
- return createResult(true, checkAuthState(null), "no_auth");
64
+ // Return a default result when no auth instance is available
65
+ return createResult(true, {
66
+ isAuthenticated: false,
67
+ isAnonymous: false,
68
+ currentUser: null,
69
+ userId: null,
70
+ }, "no_auth");
65
71
  }
66
72
 
67
73
  const authState = checkAuthState(auth);
@@ -83,7 +89,13 @@ export function shouldSkipFirestoreQuery(
83
89
  if (__DEV__) {
84
90
  console.error("[FirestoreUtils] Error checking query", error);
85
91
  }
86
- return createResult(true, checkAuthState(null), "invalid_options");
92
+ // Return a default result on error
93
+ return createResult(true, {
94
+ isAuthenticated: false,
95
+ isAnonymous: false,
96
+ currentUser: null,
97
+ userId: null,
98
+ }, "invalid_options");
87
99
  }
88
100
  }
89
101
 
@@ -55,9 +55,15 @@ export class GoogleAuthService {
55
55
  if (__DEV__) {
56
56
  console.error('[Firebase Auth] Google Sign-In failed:', error);
57
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
+
58
63
  return {
59
64
  success: false,
60
- error: error instanceof Error ? error.message : "Google sign-in failed",
65
+ error: errorMessage,
66
+ code: errorCode,
61
67
  };
62
68
  }
63
69
  }
@@ -3,6 +3,8 @@
3
3
  * Type definitions for Google authentication
4
4
  */
5
5
 
6
+ import type { UserCredential } from 'firebase/auth';
7
+
6
8
  export interface GoogleAuthConfig {
7
9
  clientId?: string;
8
10
  webClientId?: string;
@@ -10,13 +12,20 @@ export interface GoogleAuthConfig {
10
12
  androidClientId?: string;
11
13
  }
12
14
 
13
- export interface GoogleAuthResult {
14
- success: boolean;
15
- user: User;
16
- credential: GoogleAuthCredential;
17
- wasAlreadySignedIn: boolean;
15
+ export interface GoogleAuthSuccessResult {
16
+ success: true;
17
+ userCredential: UserCredential;
18
+ isNewUser: boolean;
19
+ }
20
+
21
+ export interface GoogleAuthErrorResult {
22
+ success: false;
23
+ error: string;
24
+ code?: string;
18
25
  }
19
26
 
27
+ export type GoogleAuthResult = GoogleAuthSuccessResult | GoogleAuthErrorResult;
28
+
20
29
  export interface GoogleAuthCredential {
21
30
  idToken: string;
22
31
  accessToken?: string;
@@ -3,25 +3,30 @@
3
3
  * Type definitions for reauthentication operations
4
4
  */
5
5
 
6
+ import type { AuthCredential } from 'firebase/auth';
7
+
6
8
  export interface ReauthenticationCredential {
7
9
  provider: 'password' | 'google.com' | 'apple.com';
8
- credential: unknown;
10
+ credential: AuthCredential;
9
11
  }
10
12
 
11
13
  export interface ReauthenticationResult {
12
14
  success: boolean;
13
15
  error?: {
14
- code?: string;
15
- message?: string;
16
+ code: string;
17
+ message: string;
16
18
  };
17
19
  }
18
20
 
19
- export type AuthProviderType = 'password' | 'google.com' | 'apple.com';
21
+ export type AuthProviderType = 'anonymous' | 'password' | 'google.com' | 'apple.com' | 'unknown';
20
22
 
21
23
  export interface ReauthCredentialResult {
22
24
  success: boolean;
23
- credential?: ReauthenticationCredential;
24
- error?: string;
25
+ credential?: AuthCredential;
26
+ error?: {
27
+ code: string;
28
+ message: string;
29
+ };
25
30
  }
26
31
 
27
32
  export interface AccountDeletionResult {
@@ -40,13 +40,15 @@ export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
40
40
  const state = get();
41
41
 
42
42
  // Atomic check: both state flag AND in-progress mutex
43
+ // This prevents multiple simultaneous calls from setting up listeners
43
44
  if (state.listenerSetup || unsubscribe || setupInProgress) {
44
45
  return;
45
46
  }
46
47
 
47
48
  // Set mutex immediately (synchronous, before any async operation)
49
+ // This ensures no other call can pass the check above
48
50
  setupInProgress = true;
49
- set({ listenerSetup: true });
51
+ set({ listenerSetup: true, loading: true });
50
52
 
51
53
  try {
52
54
  unsubscribe = onAuthStateChanged(auth, (currentUser: User | null) => {
@@ -59,10 +61,14 @@ export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
59
61
 
60
62
  // Listener setup complete - keep mutex locked until cleanup
61
63
  // (setupInProgress remains true to indicate active listener)
62
- } catch {
64
+ } catch (error) {
63
65
  // On error, release the mutex so retry is possible
64
66
  setupInProgress = false;
65
- set({ listenerSetup: false });
67
+ set({ listenerSetup: false, loading: false });
68
+ if (__DEV__) {
69
+ console.error('[Auth Store] Failed to setup auth listener:', error);
70
+ }
71
+ throw error; // Re-throw to allow caller to handle
66
72
  }
67
73
  },
68
74
 
@@ -37,7 +37,15 @@ export function useFirebaseAuth(): UseFirebaseAuthResult {
37
37
  }
38
38
 
39
39
  setupListener(auth);
40
- }, [setupListener]);
40
+
41
+ // Cleanup function - called when component unmounts
42
+ return () => {
43
+ // Note: We don't call cleanupListener here because the store manages
44
+ // the shared listener. Multiple components can use this hook simultaneously,
45
+ // and we want to keep the listener active until all components are unmounted.
46
+ // The store will handle cleanup when appropriate.
47
+ };
48
+ }, [setupListener]); // setupListener is stable from the store
41
49
 
42
50
  return {
43
51
  user,
@@ -94,17 +94,25 @@ export class QueryDeduplicationMiddleware {
94
94
  }
95
95
 
96
96
  /**
97
- * Add query to pending list
97
+ * Add query to pending list with guaranteed cleanup
98
98
  */
99
99
  private addPendingQuery(key: string, promise: Promise<unknown>): void {
100
+ // Wrap the promise to ensure cleanup happens regardless of outcome
101
+ const wrappedPromise = promise
102
+ .catch((error) => {
103
+ // Re-throw to maintain original promise behavior
104
+ throw error;
105
+ })
106
+ .finally(() => {
107
+ // Guaranteed cleanup - runs for both resolve and reject
108
+ this.pendingQueries.delete(key);
109
+ });
110
+
111
+ // Store the wrapped promise with the finally handler attached
100
112
  this.pendingQueries.set(key, {
101
- promise,
113
+ promise: wrappedPromise,
102
114
  timestamp: Date.now(),
103
115
  });
104
-
105
- promise.finally(() => {
106
- this.pendingQueries.delete(key);
107
- });
108
116
  }
109
117
 
110
118
  /**
@@ -36,27 +36,42 @@ export abstract class BasePaginatedRepository extends BaseQueryRepository {
36
36
  const fetchLimit = helper.getFetchLimit(pageLimit);
37
37
 
38
38
  const collectionRef = collection(db, collectionName);
39
- let q = query(
40
- collectionRef,
41
- orderBy(orderByField, orderDirection),
42
- limit(fetchLimit),
43
- );
44
-
39
+ let q: import("firebase/firestore").Query<DocumentData>;
45
40
  let cursorKey = 'start';
41
+
42
+ // Handle cursor-based pagination
46
43
  if (helper.hasCursor(params) && params?.cursor) {
47
44
  cursorKey = params.cursor;
48
- const cursorDoc = await getDoc(doc(db, collectionName, params.cursor));
49
- if (cursorDoc.exists()) {
50
- q = query(
51
- collectionRef,
52
- orderBy(orderByField, orderDirection),
53
- startAfter(cursorDoc),
54
- limit(fetchLimit),
55
- );
45
+
46
+ // Fetch cursor document first
47
+ const cursorDocRef = doc(db, collectionName, params.cursor);
48
+ const cursorDoc = await getDoc(cursorDocRef);
49
+
50
+ if (!cursorDoc.exists()) {
51
+ // Cursor document doesn't exist - return empty result
52
+ if (__DEV__) {
53
+ console.warn(`[BasePaginatedRepository] Cursor document not found: ${params.cursor}`);
54
+ }
55
+ return [];
56
56
  }
57
+
58
+ // Build query with startAfter using the cursor document
59
+ q = query(
60
+ collectionRef,
61
+ orderBy(orderByField, orderDirection),
62
+ startAfter(cursorDoc),
63
+ limit(fetchLimit),
64
+ );
65
+ } else {
66
+ // No cursor - build standard query
67
+ q = query(
68
+ collectionRef,
69
+ orderBy(orderByField, orderDirection),
70
+ limit(fetchLimit),
71
+ );
57
72
  }
58
73
 
59
- // Generate a unique key for deduplication
74
+ // Generate a unique key for deduplication (after cursor is resolved)
60
75
  const uniqueKey = `${collectionName}_list_${orderByField}_${orderDirection}_${fetchLimit}_${cursorKey}`;
61
76
 
62
77
  return this.executeQuery(
@@ -128,8 +128,25 @@ export class BaseRepository {
128
128
 
129
129
  /**
130
130
  * Destroy repository and cleanup resources
131
+ * Child classes can override onDestroy() to add custom cleanup logic
131
132
  */
132
133
  destroy(): void {
134
+ if (__DEV__) {
135
+ console.log(`[BaseRepository] Destroying repository for ${this.constructor.name}`);
136
+ }
137
+
138
+ // Call child class cleanup if implemented
139
+ this.onDestroy();
140
+
141
+ // Mark as destroyed
133
142
  this.isDestroyed = true;
134
143
  }
144
+
145
+ /**
146
+ * Cleanup hook for child classes
147
+ * Override this method to add custom cleanup logic (e.g., unsubscribe from listeners)
148
+ */
149
+ protected onDestroy(): void {
150
+ // Override in child classes if needed
151
+ }
135
152
  }
@@ -107,7 +107,13 @@ export async function runTransaction<T>(
107
107
  return await fbRunTransaction(db, updateFunction);
108
108
  } catch (error) {
109
109
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
110
- throw new Error(`[runTransaction] Transaction failed: ${errorMessage}`);
110
+ const errorCode = error instanceof Error ? (error as { code?: string }).code : 'unknown';
111
+
112
+ if (__DEV__) {
113
+ console.error(`[runTransaction] Transaction failed (Code: ${errorCode}):`, errorMessage);
114
+ }
115
+
116
+ throw new Error(`[runTransaction] Transaction failed: ${errorMessage} (Code: ${errorCode})`);
111
117
  }
112
118
  }
113
119
 
@@ -38,15 +38,22 @@ export function isQuotaError(error: unknown): boolean {
38
38
 
39
39
  if (hasCodeProperty(error)) {
40
40
  const code = error.code;
41
- if (QUOTA_ERROR_CODES.some((c) => code.includes(c))) {
42
- return true;
43
- }
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
+ );
44
45
  }
45
46
 
46
47
  if (hasMessageProperty(error)) {
47
48
  const message = error.message;
48
49
  const lowerMessage = message.toLowerCase();
49
- return QUOTA_ERROR_MESSAGES.some((m) => lowerMessage.includes(m));
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
+ );
50
57
  }
51
58
 
52
59
  return false;
package/src/index.ts CHANGED
@@ -59,24 +59,12 @@ export {
59
59
  initializeFirebaseAuth,
60
60
  } from "./auth/infrastructure/config/FirebaseAuthClient";
61
61
 
62
- export { anonymousAuthService } from "./auth/infrastructure/services/anonymous-auth.service";
63
- export { deleteCurrentUser } from "./auth/infrastructure/services/account-deletion.service";
64
- export { appleAuthService } from "./auth/infrastructure/services/apple-auth.service";
65
- export { googleAuthService } from "./auth/infrastructure/services/google-auth.service";
66
- export type { GoogleAuthConfig } from "./auth/infrastructure/services/google-auth.types";
67
- export { useAnonymousAuth } from "./auth/presentation/hooks/useAnonymousAuth";
68
- export type { UseAnonymousAuthResult } from "./auth/presentation/hooks/useAnonymousAuth";
69
-
70
62
  // Commonly Used Firestore Exports (for convenience)
71
63
  export {
72
64
  getFirestore,
73
65
  initializeFirestore,
74
66
  } from "./firestore/infrastructure/config/FirestoreClient";
75
67
 
76
- export { BaseRepository } from "./firestore/infrastructure/repositories/BaseRepository";
77
- export { FirestorePathResolver } from "./firestore/utils/path-resolver";
78
- export { PaginationHelper } from "./firestore/utils/pagination.helper";
79
-
80
68
  export { Timestamp } from "firebase/firestore";
81
69
  export type {
82
70
  Transaction,
@@ -100,20 +88,6 @@ export {
100
88
  } from "./firestore/utils/firestore-helper";
101
89
  export type { FirestoreResult, NoDbResult } from "./firestore/utils/firestore-helper";
102
90
 
103
- // Auth Hooks (commonly used)
104
- export { useSocialAuth } from "./auth/presentation/hooks/useSocialAuth";
105
- export type {
106
- SocialAuthConfig,
107
- SocialAuthResult,
108
- } from "./auth/presentation/hooks/useSocialAuth";
109
-
110
- export { updateUserPassword } from "./auth/infrastructure/services/password.service";
111
- export type { PasswordUpdateResult } from "./auth/infrastructure/services/password.service";
112
-
113
- export { reauthenticateWithPassword } from "./auth/infrastructure/services/reauthentication.service";
114
-
115
- export { getCurrentUserFromGlobal } from "./auth/infrastructure/services/auth-utils.service";
116
-
117
91
  // Init Module Factory
118
92
  export {
119
93
  createFirebaseInitModule,
@@ -35,7 +35,8 @@ export type { FirebaseApp, AuthInitializer, ServiceInitializationOptions };
35
35
  */
36
36
  export interface ServiceInitializationResult {
37
37
  app: FirebaseApp | null;
38
- auth: unknown;
38
+ auth: unknown; // Auth result from authInitializer callback (can be Auth, UserCredential, or any custom type)
39
+ authError?: string; // Error message if auth initialization failed
39
40
  }
40
41
 
41
42
  /**
@@ -18,19 +18,32 @@ export interface ServiceInitializationResult {
18
18
  auth: unknown;
19
19
  }
20
20
 
21
+ export interface ServiceInitializationResult {
22
+ auth: unknown;
23
+ authError?: string;
24
+ }
25
+
21
26
  export class FirebaseServiceInitializer {
22
27
  static async initializeServices(
23
28
  options?: ServiceInitializationOptions
24
29
  ): Promise<ServiceInitializationResult> {
25
30
  let auth: unknown = null;
31
+ let authError: string | undefined;
32
+
26
33
  if (options?.authInitializer) {
27
34
  try {
28
35
  auth = await options.authInitializer();
29
- } catch {
30
- // Silently fail, auth initialization is optional
36
+ } catch (error) {
37
+ // Auth initialization is optional but we should log the error
38
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
39
+ authError = errorMessage;
40
+
41
+ if (__DEV__) {
42
+ console.error('[FirebaseServiceInitializer] Auth initialization failed:', errorMessage);
43
+ }
31
44
  }
32
45
  }
33
46
 
34
- return { auth };
47
+ return { auth, authError };
35
48
  }
36
49
  }
@@ -51,6 +51,9 @@ export async function uploadBase64Image(
51
51
  const storageRef = ref(storage, storagePath);
52
52
 
53
53
  const response = await fetch(dataUrl);
54
+ if (!response.ok) {
55
+ throw new Error(`Failed to fetch base64 data: HTTP ${response.status} ${response.statusText}`);
56
+ }
54
57
  const blob = await response.blob();
55
58
 
56
59
  const metadata: UploadMetadata = {
@@ -82,6 +85,9 @@ export async function uploadFile(
82
85
  const storageRef = ref(storage, storagePath);
83
86
 
84
87
  const response = await fetch(uri);
88
+ if (!response.ok) {
89
+ throw new Error(`Failed to fetch file from URI: HTTP ${response.status} ${response.statusText}`);
90
+ }
85
91
  const blob = await response.blob();
86
92
 
87
93
  const contentType = options?.mimeType ?? "image/jpeg";