@umituz/react-native-firebase 2.4.82 → 2.4.84

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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/src/domains/auth/domain/utils/user-metadata.util.ts +6 -2
  3. package/src/domains/auth/domain/utils/user-validation.util.ts +1 -1
  4. package/src/domains/auth/infrastructure/services/apple-auth.service.ts +3 -3
  5. package/src/domains/auth/infrastructure/services/crypto.util.ts +10 -1
  6. package/src/domains/auth/infrastructure/services/password.service.ts +16 -1
  7. package/src/domains/auth/infrastructure/services/user-document-builder.util.ts +8 -9
  8. package/src/domains/auth/infrastructure/services/user-document.service.ts +3 -3
  9. package/src/domains/auth/infrastructure/services/utils/auth-result-converter.util.ts +2 -2
  10. package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +12 -6
  11. package/src/domains/auth/presentation/hooks/useSocialAuth.ts +5 -7
  12. package/src/domains/firestore/index.ts +8 -9
  13. package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +2 -3
  14. package/src/domains/firestore/infrastructure/services/RequestLoggerService.ts +61 -23
  15. package/src/domains/firestore/presentation/hooks/useFirestoreSnapshot.ts +13 -5
  16. package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +5 -7
  17. package/src/domains/firestore/utils/pagination.helper.ts +5 -5
  18. package/src/domains/firestore/utils/query/filters.util.ts +8 -23
  19. package/src/shared/domain/utils/error-handlers/error-checkers.ts +6 -3
  20. package/src/domains/auth/infrastructure/services/base/base-auth.service.ts +0 -15
  21. package/src/domains/firestore/presentation/index.ts +0 -2
  22. package/src/shared/domain/utils/id-generator.util.ts +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-firebase",
3
- "version": "2.4.82",
3
+ "version": "2.4.84",
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",
@@ -67,6 +67,10 @@ export function isNewUser(input: UserCredential | User | UserMetadata): boolean
67
67
  }
68
68
 
69
69
  // Convert to timestamps for reliable comparison
70
- // If creation time === last sign in time, this is the first sign-in
71
- return new Date(creationTime).getTime() === new Date(lastSignInTime).getTime();
70
+ // If creation time === last sign in time (within 2 seconds tolerance), this is the first sign-in
71
+ // Tolerance handles Firebase timestamp inconsistencies across different auth providers
72
+ const creationTimestamp = new Date(creationTime).getTime();
73
+ const lastSignInTimestamp = new Date(lastSignInTime).getTime();
74
+ const TOLERANCE_MS = 2000; // 2 second tolerance for timestamp inconsistencies
75
+ return Math.abs(creationTimestamp - lastSignInTimestamp) <= TOLERANCE_MS;
72
76
  }
@@ -42,7 +42,7 @@ export function validateUserUnchanged(
42
42
  const currentUserId = auth?.currentUser?.uid;
43
43
 
44
44
  if (currentUserId !== originalUserId) {
45
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
45
+ if (__DEV__) {
46
46
  console.log(
47
47
  `[validateUserUnchanged] User changed during operation. Original: ${originalUserId}, Current: ${currentUserId || 'none'}`
48
48
  );
@@ -14,8 +14,8 @@ import { Platform } from "react-native";
14
14
  import { generateNonce, hashNonce } from "./crypto.util";
15
15
  import type { AppleAuthResult } from "./apple-auth.types";
16
16
  import {
17
- isCancellationError,
18
- } from "./base/base-auth.service";
17
+ isCancelledError,
18
+ } from "../../../../shared/domain/utils/error-handlers/error-checkers";
19
19
  import { executeAuthOperation, type Result } from "../../../../shared/domain/utils";
20
20
  import { isNewUser as checkIsNewUser } from "../../domain/utils/user-metadata.util";
21
21
  import { convertToOAuthResult } from "./utils/auth-result-converter.util";
@@ -121,7 +121,7 @@ export class AppleAuthService {
121
121
  try {
122
122
  return await this.signIn(auth);
123
123
  } catch (error) {
124
- if (isCancellationError(error)) {
124
+ if (isCancelledError(error)) {
125
125
  return {
126
126
  success: false,
127
127
  error: "Apple Sign-In was cancelled",
@@ -9,7 +9,16 @@ const NONCE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456
9
9
 
10
10
  export async function generateNonce(length: number = 32): Promise<string> {
11
11
  const bytes = await Crypto.getRandomBytesAsync(length);
12
- return Array.from(bytes).map(b => NONCE_CHARS.charAt(b % NONCE_CHARS.length)).join("");
12
+ const charsLength = NONCE_CHARS.length;
13
+ const result: string[] = [];
14
+
15
+ // Optimized: Single pass with direct character access
16
+ for (let i = 0; i < length; i++) {
17
+ const charIndex = bytes[i]! % charsLength; // Non-null assertion: index is always valid
18
+ result.push(NONCE_CHARS[charIndex]!); // Non-null assertion: charIndex is always within bounds
19
+ }
20
+
21
+ return result.join('');
13
22
  }
14
23
 
15
24
  export async function hashNonce(nonce: string): Promise<string> {
@@ -4,7 +4,21 @@
4
4
  */
5
5
 
6
6
  import { updatePassword, type User } from 'firebase/auth';
7
- import { executeAuthOperation, type Result } from '../../../../shared/domain/utils';
7
+ import { executeAuthOperation, type Result, ERROR_MESSAGES } from '../../../../shared/domain/utils';
8
+
9
+ /**
10
+ * Validate password meets Firebase minimum requirements
11
+ * Firebase requires minimum 6 characters
12
+ */
13
+ function validatePassword(password: string): void {
14
+ if (typeof password !== 'string') {
15
+ throw new Error(ERROR_MESSAGES.AUTH.INVALID_PASSWORD);
16
+ }
17
+ const trimmed = password.trim();
18
+ if (trimmed.length < 6) {
19
+ throw new Error(ERROR_MESSAGES.AUTH.WEAK_PASSWORD);
20
+ }
21
+ }
8
22
 
9
23
  /**
10
24
  * Update the current user's password
@@ -12,6 +26,7 @@ import { executeAuthOperation, type Result } from '../../../../shared/domain/uti
12
26
  */
13
27
  export async function updateUserPassword(user: User, newPassword: string): Promise<Result<void>> {
14
28
  return executeAuthOperation(async () => {
29
+ validatePassword(newPassword);
15
30
  await updatePassword(user, newPassword);
16
31
  });
17
32
  }
@@ -17,7 +17,7 @@ function hasProviderData(user: unknown): user is { providerData: { providerId: s
17
17
  typeof user === 'object' &&
18
18
  user !== null &&
19
19
  'providerData' in user &&
20
- Array.isArray((user as Record<string, unknown>).providerData)
20
+ Array.isArray((user as { providerData: unknown }).providerData)
21
21
  );
22
22
  }
23
23
 
@@ -54,15 +54,14 @@ export function buildBaseData(
54
54
  };
55
55
 
56
56
  if (extras) {
57
- const internalFields = ["signUpMethod", "previousAnonymousUserId"];
58
- Object.keys(extras).forEach((key) => {
59
- if (!internalFields.includes(key)) {
60
- const val = extras[key];
61
- if (val !== undefined) {
62
- data[key] = val;
63
- }
57
+ const internalFields = new Set(["signUpMethod", "previousAnonymousUserId"]);
58
+ // Optimized: Single pass through extras, avoid multiple lookups
59
+ for (const [key, val] of Object.entries(extras)) {
60
+ // Skip internal fields (handled separately)
61
+ if (val !== undefined && !internalFields.has(key)) {
62
+ data[key] = val;
64
63
  }
65
- });
64
+ }
66
65
  }
67
66
 
68
67
  return data;
@@ -58,14 +58,14 @@ export async function ensureUserDocument(
58
58
  const historyEntry = buildLoginHistoryEntry(user, allExtras);
59
59
  const historyRef = collection(db, collectionName, user.uid, "loginHistory");
60
60
  addDoc(historyRef, historyEntry).catch((err) => {
61
- if (typeof __DEV__ !== "undefined" && __DEV__) {
61
+ if (__DEV__) {
62
62
  console.warn("[UserDocumentService] Failed to write login history:", err);
63
63
  }
64
64
  });
65
65
 
66
66
  return true;
67
67
  } catch (error) {
68
- if (typeof __DEV__ !== "undefined" && __DEV__) {
68
+ if (__DEV__) {
69
69
  console.error("[UserDocumentService] Failed:", error);
70
70
  }
71
71
  return false;
@@ -85,7 +85,7 @@ export async function markUserDeleted(userId: string): Promise<boolean> {
85
85
  }, { merge: true });
86
86
  return true;
87
87
  } catch (error) {
88
- if (typeof __DEV__ !== "undefined" && __DEV__) {
88
+ if (__DEV__) {
89
89
  console.error("[UserDocumentService] Failed to mark user deleted:", error);
90
90
  }
91
91
  return false;
@@ -62,11 +62,11 @@ export function convertToOAuthResult(
62
62
  success: true,
63
63
  userCredential: result.data.userCredential,
64
64
  isNewUser: result.data.isNewUser,
65
- } as OAuthAuthSuccessResult;
65
+ };
66
66
  }
67
67
  return {
68
68
  success: false,
69
69
  error: result.error?.message ?? defaultErrorMessage,
70
70
  code: result.error?.code,
71
- } as OAuthAuthErrorResult;
71
+ };
72
72
  }
@@ -4,7 +4,7 @@
4
4
  * This hook is optional and requires expo-auth-session to be installed
5
5
  */
6
6
 
7
- import { useState, useCallback, useEffect, useRef } from "react";
7
+ import { useState, useCallback, useEffect, useRef, useMemo } from "react";
8
8
  import { googleOAuthService } from "../../infrastructure/services/google-oauth.service";
9
9
  import { getFirebaseAuth } from "../../infrastructure/config/FirebaseAuthClient";
10
10
  import type { GoogleOAuthConfig } from "../../infrastructure/services/google-oauth.service";
@@ -55,8 +55,9 @@ export function useGoogleOAuth(config?: GoogleOAuthConfig): UseGoogleOAuthResult
55
55
  const [isLoading, setIsLoading] = useState(false);
56
56
  const [googleError, setGoogleError] = useState<string | null>(null);
57
57
 
58
- const googleAvailable = googleOAuthService.isAvailable();
59
- const googleConfigured = googleOAuthService.isConfigured(config);
58
+ // Memoize service checks to avoid repeated calls on every render
59
+ const googleAvailable = useMemo(() => googleOAuthService.isAvailable(), []);
60
+ const googleConfigured = useMemo(() => googleOAuthService.isConfigured(config), [config]);
60
61
 
61
62
  // Keep config in a ref so the response useEffect doesn't re-run when config object reference changes
62
63
  const configRef = useRef(config);
@@ -86,7 +87,7 @@ export function useGoogleOAuth(config?: GoogleOAuthConfig): UseGoogleOAuthResult
86
87
  const auth = getFirebaseAuth();
87
88
  if (!auth) {
88
89
  setGoogleError("Firebase Auth not initialized");
89
- setIsLoading(false); // FIX: Reset loading state before early return
90
+ setIsLoading(false);
90
91
  return;
91
92
  }
92
93
 
@@ -104,11 +105,16 @@ export function useGoogleOAuth(config?: GoogleOAuthConfig): UseGoogleOAuthResult
104
105
  }
105
106
  } else if (response.type === "error") {
106
107
  setGoogleError("Google authentication failed");
107
- setIsLoading(false);
108
108
  }
109
109
  };
110
110
 
111
- handleResponse();
111
+ // Call async function and catch errors separately
112
+ handleResponse().catch((err) => {
113
+ // Errors are already handled in handleResponse
114
+ if (__DEV__) {
115
+ console.error('[useGoogleOAuth] Unexpected error in handleResponse:', err);
116
+ }
117
+ });
112
118
  }, [response, googleAvailable]); // config read via ref to prevent re-running on reference changes
113
119
 
114
120
  const signInWithGoogle = useCallback(async (): Promise<SocialAuthResult> => {
@@ -35,18 +35,16 @@ export function useSocialAuth(config?: SocialAuthConfig): UseSocialAuthResult {
35
35
  const [appleAvailable, setAppleAvailable] = useState(false);
36
36
 
37
37
  // Stabilize config objects to prevent unnecessary re-renders and effect re-runs
38
- const googleConfig = useMemo(() => config?.google, [
39
- config?.google?.webClientId,
40
- config?.google?.iosClientId,
41
- config?.google?.androidClientId,
42
- ]);
38
+ // Use full object as dependency instead of individual properties
39
+ const googleConfig = useMemo(() => config?.google, [config?.google]);
43
40
  const appleEnabled = useMemo(() => config?.apple?.enabled, [config?.apple?.enabled]);
44
41
 
45
- const googleConfigured = !!(
42
+ // Memoize configured check to avoid recalculation on every render
43
+ const googleConfigured = useMemo(() => !!(
46
44
  googleConfig?.webClientId ||
47
45
  googleConfig?.iosClientId ||
48
46
  googleConfig?.androidClientId
49
- );
47
+ ), [googleConfig]);
50
48
 
51
49
  useEffect(() => {
52
50
  if (googleConfig) {
@@ -141,15 +141,14 @@ export {
141
141
  } from './utils/validation/date-validator.util';
142
142
 
143
143
  // Presentation — TanStack Query integration
144
- export {
145
- useFirestoreQuery,
146
- useFirestoreMutation,
147
- useFirestoreSnapshot,
148
- createFirestoreKeys,
149
- type UseFirestoreQueryOptions,
150
- type UseFirestoreMutationOptions,
151
- type UseFirestoreSnapshotOptions,
152
- } from './presentation';
144
+ export { useFirestoreQuery } from './presentation/hooks/useFirestoreQuery';
145
+ export { useFirestoreMutation } from './presentation/hooks/useFirestoreMutation';
146
+ export { useFirestoreSnapshot } from './presentation/hooks/useFirestoreSnapshot';
147
+ export { createFirestoreKeys } from './presentation/query-keys/createFirestoreKeys';
148
+
149
+ export type { UseFirestoreQueryOptions } from './presentation/hooks/useFirestoreQuery';
150
+ export type { UseFirestoreMutationOptions } from './presentation/hooks/useFirestoreMutation';
151
+ export type { UseFirestoreSnapshotOptions } from './presentation/hooks/useFirestoreSnapshot';
153
152
 
154
153
  export { Timestamp } from 'firebase/firestore';
155
154
  export type {
@@ -51,9 +51,8 @@ export class QueryDeduplicationMiddleware {
51
51
  try {
52
52
  return await queryFn();
53
53
  } finally {
54
- // Cleanup after completion (success or error)
55
- // Note: PendingQueryManager also has cleanup via finally, but we keep
56
- // this for extra safety and immediate cleanup
54
+ // Immediate cleanup after completion (success or error)
55
+ // PendingQueryManager will also cleanup via its finally handler
57
56
  this.queryManager.remove(key);
58
57
  }
59
58
  })();
@@ -4,12 +4,23 @@
4
4
  */
5
5
 
6
6
  import type { RequestLog, RequestStats, RequestType } from '../../domain/entities/RequestLog';
7
- import { generateUniqueId } from '../../../../shared/domain/utils/id-generator.util';
7
+ import { generateUUID } from '@umituz/react-native-design-system/uuid';
8
+
9
+ /**
10
+ * Maximum number of logs to keep in memory
11
+ * Prevents unbounded memory growth
12
+ */
13
+ export const DEFAULT_MAX_LOGS = 1000;
8
14
 
9
15
  export class RequestLoggerService {
10
16
  private logs: RequestLog[] = [];
11
- private readonly MAX_LOGS = 1000;
17
+ private readonly maxLogs: number;
12
18
  private listeners: Set<(log: RequestLog) => void> = new Set();
19
+ private static readonly LISTENER_ERROR_PREFIX = '[RequestLoggerService] Listener error:';
20
+
21
+ constructor(maxLogs: number = DEFAULT_MAX_LOGS) {
22
+ this.maxLogs = maxLogs;
23
+ }
13
24
 
14
25
  /**
15
26
  * Log a request
@@ -17,13 +28,13 @@ export class RequestLoggerService {
17
28
  logRequest(log: Omit<RequestLog, 'id' | 'timestamp'>): void {
18
29
  const fullLog: RequestLog = {
19
30
  ...log,
20
- id: generateUniqueId(),
31
+ id: generateUUID(),
21
32
  timestamp: Date.now(),
22
33
  };
23
34
 
24
35
  this.logs.push(fullLog);
25
36
 
26
- if (this.logs.length > this.MAX_LOGS) {
37
+ if (this.logs.length > this.maxLogs) {
27
38
  this.logs.shift();
28
39
  }
29
40
 
@@ -39,40 +50,62 @@ export class RequestLoggerService {
39
50
 
40
51
  /**
41
52
  * Get logs by type
53
+ * Optimized: Return empty array early if no logs
42
54
  */
43
55
  getLogsByType(type: RequestType): RequestLog[] {
56
+ if (this.logs.length === 0) return [];
44
57
  return this.logs.filter((log) => log.type === type);
45
58
  }
46
59
 
47
60
  /**
48
61
  * Get request statistics
62
+ * Optimized: Single-pass calculation O(n) instead of O(7n)
49
63
  */
50
64
  getStats(): RequestStats {
51
- const totalRequests = this.logs.length;
52
- const readRequests = this.logs.filter((l) => l.type === 'read').length;
53
- const writeRequests = this.logs.filter((l) => l.type === 'write').length;
54
- const deleteRequests = this.logs.filter((l) => l.type === 'delete').length;
55
- const listenerRequests = this.logs.filter((l) => l.type === 'listener').length;
56
- const cachedRequests = this.logs.filter((l) => l.cached).length;
57
- const failedRequests = this.logs.filter((l) => !l.success).length;
58
-
59
- const durations = this.logs
60
- .map((l) => l.duration)
61
- .filter((d): d is number => d !== undefined);
62
- const averageDuration =
63
- durations.length > 0
64
- ? durations.reduce((sum, d) => sum + d, 0) / durations.length
65
- : 0;
65
+ let readRequests = 0;
66
+ let writeRequests = 0;
67
+ let deleteRequests = 0;
68
+ let listenerRequests = 0;
69
+ let cachedRequests = 0;
70
+ let failedRequests = 0;
71
+ let durationSum = 0;
72
+ let durationCount = 0;
73
+
74
+ // Single pass through logs for all statistics
75
+ for (const log of this.logs) {
76
+ switch (log.type) {
77
+ case 'read':
78
+ readRequests++;
79
+ break;
80
+ case 'write':
81
+ writeRequests++;
82
+ break;
83
+ case 'delete':
84
+ deleteRequests++;
85
+ break;
86
+ case 'listener':
87
+ listenerRequests++;
88
+ break;
89
+ }
90
+
91
+ if (log.cached) cachedRequests++;
92
+ if (!log.success) failedRequests++;
93
+
94
+ if (log.duration !== undefined) {
95
+ durationSum += log.duration;
96
+ durationCount++;
97
+ }
98
+ }
66
99
 
67
100
  return {
68
- totalRequests,
101
+ totalRequests: this.logs.length,
69
102
  readRequests,
70
103
  writeRequests,
71
104
  deleteRequests,
72
105
  listenerRequests,
73
106
  cachedRequests,
74
107
  failedRequests,
75
- averageDuration,
108
+ averageDuration: durationCount > 0 ? durationSum / durationCount : 0,
76
109
  };
77
110
  }
78
111
 
@@ -116,8 +149,13 @@ export class RequestLoggerService {
116
149
  this.listeners.forEach((listener) => {
117
150
  try {
118
151
  listener(log);
119
- } catch {
120
- // Silently ignore listener errors
152
+ } catch (error) {
153
+ // Log listener errors in development to help debugging
154
+ // In production, silently ignore to prevent crashing the app
155
+ if (__DEV__) {
156
+ const errorMessage = error instanceof Error ? error.message : String(error);
157
+ console.warn(`${RequestLoggerService.LISTENER_ERROR_PREFIX} ${errorMessage}`);
158
+ }
121
159
  }
122
160
  });
123
161
  }
@@ -47,6 +47,7 @@ export function useFirestoreSnapshot<TData>(
47
47
  const queryClient = useQueryClient();
48
48
  const unsubscribeRef = useRef<(() => void) | null>(null);
49
49
  const dataPromiseRef = useRef<{ resolve: (value: TData) => void; reject: (error: Error) => void } | null>(null);
50
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
50
51
 
51
52
  // Stabilize queryKey to prevent unnecessary listener re-subscriptions
52
53
  const stableKeyString = JSON.stringify(queryKey);
@@ -57,10 +58,14 @@ export function useFirestoreSnapshot<TData>(
57
58
 
58
59
  unsubscribeRef.current = subscribe((data) => {
59
60
  queryClient.setQueryData(stableQueryKey, data);
60
- // Resolve any pending promise from queryFn
61
+ // Resolve any pending promise from queryFn and clear timeout
61
62
  if (dataPromiseRef.current) {
62
63
  dataPromiseRef.current.resolve(data);
63
64
  dataPromiseRef.current = null;
65
+ if (timeoutRef.current) {
66
+ clearTimeout(timeoutRef.current);
67
+ timeoutRef.current = null;
68
+ }
64
69
  }
65
70
  });
66
71
 
@@ -72,6 +77,11 @@ export function useFirestoreSnapshot<TData>(
72
77
  dataPromiseRef.current.reject(new Error('Snapshot listener cleanup'));
73
78
  dataPromiseRef.current = null;
74
79
  }
80
+ // Clear timeout on cleanup
81
+ if (timeoutRef.current) {
82
+ clearTimeout(timeoutRef.current);
83
+ timeoutRef.current = null;
84
+ }
75
85
  };
76
86
  }, [enabled, queryClient, stableQueryKey, subscribe]);
77
87
 
@@ -89,9 +99,10 @@ export function useFirestoreSnapshot<TData>(
89
99
  dataPromiseRef.current = { resolve, reject };
90
100
 
91
101
  // Timeout to prevent infinite waiting (memory leak protection)
92
- const timeoutId = setTimeout(() => {
102
+ timeoutRef.current = setTimeout(() => {
93
103
  if (dataPromiseRef.current) {
94
104
  dataPromiseRef.current = null;
105
+ timeoutRef.current = null;
95
106
  if (initialData !== undefined) {
96
107
  resolve(initialData);
97
108
  } else {
@@ -99,9 +110,6 @@ export function useFirestoreSnapshot<TData>(
99
110
  }
100
111
  }
101
112
  }, 30000); // 30 second timeout
102
-
103
- // Clear timeout on promise resolution
104
- return () => clearTimeout(timeoutId);
105
113
  });
106
114
  },
107
115
  enabled,
@@ -46,17 +46,15 @@ export class PendingQueryManager {
46
46
  * Also attaches cleanup handlers to prevent memory leaks.
47
47
  */
48
48
  add(key: string, promise: Promise<unknown>): void {
49
- // Attach cleanup handlers to ensure promise is removed from map
49
+ // Attach cleanup handler to ensure query is removed from map
50
50
  // even if caller's finally block doesn't execute (e.g., unhandled rejection)
51
- const cleanupPromise = promise.finally(() => {
52
- // Small delay to allow immediate retry if needed
53
- setTimeout(() => {
54
- this.pendingQueries.delete(key);
55
- }, 100);
51
+ promise.finally(() => {
52
+ // Immediate cleanup - no delay needed for better performance
53
+ this.pendingQueries.delete(key);
56
54
  });
57
55
 
58
56
  this.pendingQueries.set(key, {
59
- promise: cleanupPromise,
57
+ promise,
60
58
  timestamp: Date.now(),
61
59
  });
62
60
  }
@@ -44,13 +44,13 @@ export class PaginationHelper<T> {
44
44
  const resultCount = getResultCount(items.length, pageLimit);
45
45
  const resultItems = safeSlice(items, 0, resultCount);
46
46
 
47
- // Safe access: check array is not empty before accessing last item
47
+ // Extract next cursor from last item
48
+ // Safe: resultItems is guaranteed to have at least one item when hasMoreValue is true
48
49
  let nextCursor: string | null = null;
49
50
  if (hasMoreValue && resultItems.length > 0) {
50
- const lastItem = resultItems[resultItems.length - 1];
51
- if (lastItem) {
52
- nextCursor = getCursor(lastItem);
53
- }
51
+ // Access is safe because we checked length > 0
52
+ const lastItem = resultItems[resultItems.length - 1]!;
53
+ nextCursor = getCursor(lastItem);
54
54
  }
55
55
 
56
56
  return {
@@ -11,26 +11,7 @@ import {
11
11
  type WhereFilterOp,
12
12
  type Query,
13
13
  } from "firebase/firestore";
14
-
15
- /**
16
- * Chunk array into smaller arrays (local copy for this module)
17
- * Inlined here to avoid circular dependencies
18
- */
19
- function chunkArray(array: readonly (string | number)[], chunkSize: number): (string | number)[][] {
20
- if (chunkSize <= 0) {
21
- throw new Error('chunkSize must be greater than 0');
22
- }
23
-
24
- const chunks: (string | number)[][] = [];
25
- const len = array.length;
26
-
27
- for (let i = 0; i < len; i += chunkSize) {
28
- const end = Math.min(i + chunkSize, len);
29
- chunks.push(array.slice(i, end));
30
- }
31
-
32
- return chunks;
33
- }
14
+ import { chunkArray } from "../../../../shared/domain/utils/calculation.util";
34
15
 
35
16
  export interface FieldFilter {
36
17
  field: string;
@@ -38,7 +19,11 @@ export interface FieldFilter {
38
19
  value: string | number | boolean | string[] | number[];
39
20
  }
40
21
 
41
- const MAX_IN_OPERATOR_VALUES = 10;
22
+ /**
23
+ * Maximum number of values in a single 'in' query
24
+ * Firestore limits 'in' queries to 10 values
25
+ */
26
+ export const MAX_IN_OPERATOR_VALUES = 10;
42
27
 
43
28
  /**
44
29
  * Apply field filter with 'in' operator and chunking support
@@ -53,8 +38,8 @@ export function applyFieldFilter(q: Query, filter: FieldFilter): Query {
53
38
  }
54
39
 
55
40
  // Split into chunks of 10 and use 'or' operator
56
- // Optimized: Uses local chunkArray utility
57
- const chunks = chunkArray(value, MAX_IN_OPERATOR_VALUES);
41
+ // Optimized: Uses centralized chunkArray utility
42
+ const chunks = chunkArray(value as readonly (string | number)[], MAX_IN_OPERATOR_VALUES);
58
43
  const orConditions = chunks.map((chunk) => where(field, "in", chunk));
59
44
  return query(q, or(...orConditions));
60
45
  }
@@ -33,10 +33,13 @@ const RETRYABLE_ERROR_CODES = ['unavailable', 'deadline-exceeded', 'aborted'];
33
33
  /**
34
34
  * Check if error is a cancelled/auth cancelled error
35
35
  */
36
- export function isCancelledError(error: { code: string; message: string }): boolean {
36
+ export function isCancelledError(error: unknown): boolean {
37
+ if (!error || typeof error !== 'object') return false;
38
+ if (!('code' in error) || !('message' in error)) return false;
39
+ const err = error as { code: string; message: string };
37
40
  return (
38
- error.code === 'auth/cancelled' ||
39
- error.message.includes('ERR_CANCELED')
41
+ err.code === 'auth/cancelled' ||
42
+ err.message.includes('ERR_CANCELED')
40
43
  );
41
44
  }
42
45
 
@@ -1,15 +0,0 @@
1
- /**
2
- * Base Auth Service
3
- *
4
- * Provides common authentication service functionality
5
- */
6
-
7
- /**
8
- * Check if error is a cancellation error
9
- */
10
- export function isCancellationError(error: unknown): boolean {
11
- if (error instanceof Error) {
12
- return error.message.includes('ERR_CANCELED');
13
- }
14
- return false;
15
- }
@@ -1,2 +0,0 @@
1
- export * from './hooks';
2
- export * from './query-keys';
@@ -1,17 +0,0 @@
1
- /**
2
- * ID Generator Utility
3
- * Centralized ID generation utilities for unique identifiers
4
- */
5
-
6
- import * as Crypto from "expo-crypto";
7
-
8
- /**
9
- * Generate a unique ID using timestamp and cryptographically secure random string
10
- * Format: timestamp-randomstring
11
- * @returns Unique identifier string
12
- */
13
- export function generateUniqueId(): string {
14
- const randomBytes = Crypto.getRandomBytes(8);
15
- const hex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, "0")).join("");
16
- return `${Date.now()}-${hex}`;
17
- }