@umituz/react-native-firebase 1.13.140 → 1.13.141

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/auth/infrastructure/config/FirebaseAuthClient.ts +11 -2
  3. package/src/auth/infrastructure/services/account-deletion.service.ts +40 -20
  4. package/src/auth/infrastructure/services/anonymous-auth.service.ts +7 -11
  5. package/src/auth/infrastructure/services/apple-auth.service.ts +44 -18
  6. package/src/auth/infrastructure/services/base/base-auth.service.ts +9 -40
  7. package/src/auth/infrastructure/services/firestore-utils.service.ts +2 -2
  8. package/src/auth/infrastructure/services/google-auth.service.ts +27 -23
  9. package/src/auth/infrastructure/services/password.service.ts +6 -21
  10. package/src/auth/infrastructure/services/reauthentication.service.ts +19 -29
  11. package/src/auth/presentation/hooks/shared/hook-utils.util.ts +222 -0
  12. package/src/auth/presentation/hooks/useSocialAuth.ts +10 -1
  13. package/src/domain/utils/async-executor.util.ts +176 -0
  14. package/src/domain/utils/credential.util.ts +102 -0
  15. package/src/domain/utils/index.ts +101 -0
  16. package/src/domain/utils/result.util.ts +129 -0
  17. package/src/domain/utils/service-config.util.ts +99 -0
  18. package/src/domain/utils/validation.util.ts +78 -0
  19. package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +3 -5
  20. package/src/firestore/infrastructure/repositories/BaseQueryRepository.ts +3 -3
  21. package/src/firestore/utils/dateUtils.ts +21 -5
  22. package/src/firestore/utils/deduplication/pending-query-manager.util.ts +3 -7
  23. package/src/firestore/utils/deduplication/query-key-generator.util.ts +12 -3
  24. package/src/firestore/utils/deduplication/timer-manager.util.ts +8 -2
  25. package/src/infrastructure/config/FirebaseClient.ts +36 -4
  26. package/src/init/createFirebaseInitModule.ts +18 -3
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Result Utility
3
+ * Unified result type for all operations across auth and firestore modules
4
+ * Provides type-safe success/failure handling
5
+ */
6
+
7
+ /**
8
+ * Standard error information structure
9
+ */
10
+ export interface ErrorInfo {
11
+ code: string;
12
+ message: string;
13
+ }
14
+
15
+ /**
16
+ * Standard result type for operations
17
+ * Success contains data, failure contains error info
18
+ */
19
+ export interface Result<T = void> {
20
+ readonly success: boolean;
21
+ readonly data?: T;
22
+ readonly error?: ErrorInfo;
23
+ }
24
+
25
+ /**
26
+ * Success result type guard
27
+ */
28
+ export type SuccessResult<T = void> = Result<T> & { readonly success: true; readonly data: T; readonly error?: never };
29
+
30
+ /**
31
+ * Failure result type guard
32
+ */
33
+ export type FailureResult = Result & { readonly success: false; readonly data?: never; readonly error: ErrorInfo };
34
+
35
+ /**
36
+ * Create a success result with optional data
37
+ */
38
+ export function successResult<T = void>(data?: T): SuccessResult<T> {
39
+ return { success: true, data: data as T };
40
+ }
41
+
42
+ /**
43
+ * Create a failure result with error information
44
+ */
45
+ export function failureResult(error: ErrorInfo): FailureResult {
46
+ return { success: false, error };
47
+ }
48
+
49
+ /**
50
+ * Create a failure result from error code and message
51
+ */
52
+ export function failureResultFrom(code: string, message: string): FailureResult {
53
+ return { success: false, error: { code, message } };
54
+ }
55
+
56
+ /**
57
+ * Create a failure result from an unknown error
58
+ */
59
+ export function failureResultFromError(error: unknown, defaultCode = 'operation/failed'): FailureResult {
60
+ if (error instanceof Error) {
61
+ return {
62
+ success: false,
63
+ error: {
64
+ code: (error as { code?: string }).code ?? defaultCode,
65
+ message: error.message,
66
+ },
67
+ };
68
+ }
69
+ return {
70
+ success: false,
71
+ error: {
72
+ code: defaultCode,
73
+ message: typeof error === 'string' ? error : 'Unknown error occurred',
74
+ },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Check if result is successful
80
+ */
81
+ export function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
82
+ return result.success === true && result.error === undefined;
83
+ }
84
+
85
+ /**
86
+ * Check if result is a failure
87
+ */
88
+ export function isFailure<T>(result: Result<T>): result is FailureResult {
89
+ return result.success === false;
90
+ }
91
+
92
+ /**
93
+ * Get data from result or return default
94
+ */
95
+ export function getDataOrDefault<T>(result: Result<T>, defaultValue: T): T {
96
+ return isSuccess(result) ? (result.data ?? defaultValue) : defaultValue;
97
+ }
98
+
99
+ /**
100
+ * Map success result data to another type
101
+ */
102
+ export function mapResult<T, U>(
103
+ result: Result<T>,
104
+ mapper: (data: T) => U
105
+ ): Result<U> {
106
+ if (isSuccess(result) && result.data !== undefined) {
107
+ return successResult(mapper(result.data));
108
+ }
109
+ // Return a new failure result to avoid type conflicts
110
+ if (isFailure(result)) {
111
+ return { success: false, error: result.error };
112
+ }
113
+ return successResult();
114
+ }
115
+
116
+ /**
117
+ * Chain multiple results, stopping at first failure
118
+ */
119
+ export async function chainResults<T>(
120
+ ...operations: (() => Promise<Result<T>>)[]
121
+ ): Promise<Result<T>> {
122
+ for (const operation of operations) {
123
+ const result = await operation();
124
+ if (isFailure(result)) {
125
+ return result;
126
+ }
127
+ }
128
+ return successResult();
129
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Configurable Service Base Class
3
+ * Provides common service configuration pattern
4
+ * Eliminates code duplication across services
5
+ */
6
+
7
+ /**
8
+ * Configuration state management
9
+ */
10
+ export interface ConfigState<TConfig> {
11
+ config: TConfig | null;
12
+ initialized: boolean;
13
+ }
14
+
15
+ /**
16
+ * Configurable service interface
17
+ */
18
+ export interface IConfigurableService<TConfig> {
19
+ configure(config: TConfig): void;
20
+ isConfigured(): boolean;
21
+ getConfig(): TConfig | null;
22
+ reset(): void;
23
+ }
24
+
25
+ /**
26
+ * Base class for configurable services
27
+ * Provides configuration management pattern
28
+ */
29
+ export class ConfigurableService<TConfig = unknown> implements IConfigurableService<TConfig> {
30
+ protected configState: ConfigState<TConfig>;
31
+ private customValidator?: (config: TConfig) => boolean;
32
+
33
+ constructor(validator?: (config: TConfig) => boolean) {
34
+ this.configState = {
35
+ config: null,
36
+ initialized: false,
37
+ };
38
+ this.customValidator = validator;
39
+ }
40
+
41
+ /**
42
+ * Configure the service
43
+ */
44
+ configure(config: TConfig): void {
45
+ this.configState.config = config;
46
+ this.configState.initialized = this.isValidConfig(config);
47
+ }
48
+
49
+ /**
50
+ * Check if service is configured
51
+ */
52
+ isConfigured(): boolean {
53
+ return this.configState.initialized && this.configState.config !== null;
54
+ }
55
+
56
+ /**
57
+ * Get current configuration
58
+ */
59
+ getConfig(): TConfig | null {
60
+ return this.configState.config;
61
+ }
62
+
63
+ /**
64
+ * Reset configuration
65
+ */
66
+ reset(): void {
67
+ this.configState.config = null;
68
+ this.configState.initialized = false;
69
+ }
70
+
71
+ /**
72
+ * Validate configuration - can be overridden by custom validator
73
+ */
74
+ protected isValidConfig(config: TConfig): boolean {
75
+ if (this.customValidator) {
76
+ return this.customValidator(config);
77
+ }
78
+ return true;
79
+ }
80
+
81
+ /**
82
+ * Get required configuration or throw error
83
+ */
84
+ protected requireConfig(): TConfig {
85
+ if (!this.configState.config) {
86
+ throw new Error('Service is not configured');
87
+ }
88
+ return this.configState.config;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Create a configurable service instance
94
+ */
95
+ export function createConfigurableService<TConfig>(
96
+ validator?: (config: TConfig) => boolean
97
+ ): ConfigurableService<TConfig> {
98
+ return new ConfigurableService<TConfig>(validator);
99
+ }
@@ -131,3 +131,81 @@ export function isPositive(value: number): boolean {
131
131
  export function isNonNegative(value: number): boolean {
132
132
  return typeof value === 'number' && value >= 0;
133
133
  }
134
+
135
+ /**
136
+ * Validate password strength
137
+ * At least 8 characters, containing uppercase, lowercase, and number
138
+ */
139
+ export function isStrongPassword(password: string): boolean {
140
+ if (!isValidString(password) || password.length < 8) {
141
+ return false;
142
+ }
143
+ const hasUpperCase = /[A-Z]/.test(password);
144
+ const hasLowerCase = /[a-z]/.test(password);
145
+ const hasNumber = /[0-9]/.test(password);
146
+ return hasUpperCase && hasLowerCase && hasNumber;
147
+ }
148
+
149
+ /**
150
+ * Validate username format
151
+ * Alphanumeric, underscores, and hyphens, 3-20 characters
152
+ */
153
+ export function isValidUsername(username: string): boolean {
154
+ if (!isValidString(username)) {
155
+ return false;
156
+ }
157
+ const pattern = /^[a-zA-Z0-9_-]{3,20}$/;
158
+ return pattern.test(username);
159
+ }
160
+
161
+ /**
162
+ * Validate phone number format (basic check)
163
+ */
164
+ export function isValidPhoneNumber(phone: string): boolean {
165
+ if (!isValidString(phone)) {
166
+ return false;
167
+ }
168
+ const cleaned = phone.replace(/\s+/g, '').replace(/[-+()]/g, '');
169
+ return /^[0-9]{10,15}$/.test(cleaned);
170
+ }
171
+
172
+ /**
173
+ * Validate object has required properties
174
+ */
175
+ export function hasRequiredProperties<T extends Record<string, unknown>>(
176
+ obj: unknown,
177
+ requiredProps: (keyof T)[]
178
+ ): obj is T {
179
+ if (typeof obj !== 'object' || obj === null) {
180
+ return false;
181
+ }
182
+ return requiredProps.every((prop) => prop in obj);
183
+ }
184
+
185
+ /**
186
+ * Validate all items in array match predicate
187
+ */
188
+ export function allMatch<T>(items: unknown[], predicate: (item: unknown) => item is T): boolean {
189
+ return Array.isArray(items) && items.every(predicate);
190
+ }
191
+
192
+ /**
193
+ * Validate at least one item in array matches predicate
194
+ */
195
+ export function anyMatch<T>(items: unknown[], predicate: (item: unknown) => item is T): boolean {
196
+ return Array.isArray(items) && items.some(predicate);
197
+ }
198
+
199
+ /**
200
+ * Create a validator that combines multiple validators
201
+ */
202
+ export function combineValidators(...validators: ((value: string) => boolean)[]): (value: string) => boolean {
203
+ return (value: string) => validators.every((validator) => validator(value));
204
+ }
205
+
206
+ /**
207
+ * Create a validator that checks if value matches one of validators
208
+ */
209
+ export function anyValidator(...validators: ((value: string) => boolean)[]): (value: string) => boolean {
210
+ return (value: string) => validators.some((validator) => validator(value));
211
+ }
@@ -30,14 +30,12 @@ export class QueryDeduplicationMiddleware {
30
30
  ): Promise<T> {
31
31
  const key = generateQueryKey(queryKey);
32
32
 
33
+ // Check if query is already pending
33
34
  if (this.queryManager.isPending(key)) {
34
35
  const pendingPromise = this.queryManager.get(key);
35
36
  if (pendingPromise) {
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
+ // Return the existing pending promise instead of executing again
38
+ return pendingPromise as Promise<T>;
41
39
  }
42
40
  }
43
41
 
@@ -29,9 +29,9 @@ export abstract class BaseQueryRepository extends BaseRepository {
29
29
  cached: boolean = false,
30
30
  uniqueKey?: string
31
31
  ): Promise<T> {
32
- // FIX: query.toString() returns "[object Object]" which breaks deduplication
33
- // We must rely on the caller providing a uniqueKey or fallback to a collection-based key (less efficient but safe)
34
- const safeKey = uniqueKey || `${collection}_generic_query_${Date.now()}`;
32
+ // Use provided uniqueKey, or generate a stable key based on collection
33
+ // IMPORTANT: Don't use Date.now() as it defeats deduplication!
34
+ const safeKey = uniqueKey || `${collection}_query`;
35
35
 
36
36
  const queryKey = {
37
37
  collection,
@@ -1,14 +1,30 @@
1
1
  import { Timestamp } from 'firebase/firestore';
2
2
 
3
+ /**
4
+ * Validate ISO 8601 date string format
5
+ */
6
+ function isValidISODate(isoString: string): boolean {
7
+ // Check if it matches ISO 8601 format (basic check)
8
+ const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?([+-]\d{2}:\d{2})?$/;
9
+ if (!isoPattern.test(isoString)) {
10
+ return false;
11
+ }
12
+
13
+ // Verify it's a valid date
14
+ const date = new Date(isoString);
15
+ return !isNaN(date.getTime());
16
+ }
17
+
3
18
  /**
4
19
  * Convert ISO string to Firestore Timestamp
20
+ * @throws Error if isoString is not a valid ISO 8601 date
5
21
  */
6
22
  export function isoToTimestamp(isoString: string): Timestamp {
7
- const date = new Date(isoString);
8
- if (isNaN(date.getTime())) {
9
- throw new Error(`Invalid ISO date string: ${isoString}`);
10
- }
11
- return Timestamp.fromDate(date);
23
+ if (!isValidISODate(isoString)) {
24
+ throw new Error(`Invalid ISO date string: ${isoString}`);
25
+ }
26
+ const date = new Date(isoString);
27
+ return Timestamp.fromDate(date);
12
28
  }
13
29
 
14
30
  /**
@@ -44,13 +44,9 @@ export class PendingQueryManager {
44
44
  * Add query to pending list with guaranteed cleanup
45
45
  */
46
46
  add(key: string, promise: Promise<unknown>): void {
47
- const wrappedPromise = promise
48
- .catch((error) => {
49
- throw error;
50
- })
51
- .finally(() => {
52
- this.pendingQueries.delete(key);
53
- });
47
+ const wrappedPromise = promise.finally(() => {
48
+ this.pendingQueries.delete(key);
49
+ });
54
50
 
55
51
  this.pendingQueries.set(key, {
56
52
  promise: wrappedPromise,
@@ -10,15 +10,24 @@ export interface QueryKey {
10
10
  orderBy?: string;
11
11
  }
12
12
 
13
+ /**
14
+ * Escape special characters in query key components
15
+ * Prevents key collisions when filter strings contain separator characters
16
+ */
17
+ function escapeKeyComponent(component: string): string {
18
+ return component.replace(/%/g, '%25').replace(/\|/g, '%7C');
19
+ }
20
+
13
21
  /**
14
22
  * Generate a unique key from query parameters
23
+ * Uses URL encoding to prevent collisions from separator characters
15
24
  */
16
25
  export function generateQueryKey(key: QueryKey): string {
17
26
  const parts = [
18
- key.collection,
19
- key.filters,
27
+ escapeKeyComponent(key.collection),
28
+ escapeKeyComponent(key.filters),
20
29
  key.limit?.toString() || '',
21
- key.orderBy || '',
30
+ escapeKeyComponent(key.orderBy || ''),
22
31
  ];
23
32
  return parts.join('|');
24
33
  }
@@ -18,17 +18,23 @@ export class TimerManager {
18
18
 
19
19
  /**
20
20
  * Start the cleanup timer
21
+ * Idempotent: safe to call multiple times
21
22
  */
22
23
  start(): void {
24
+ // Clear existing timer if running (prevents duplicate timers)
23
25
  if (this.timer) {
24
- clearInterval(this.timer);
26
+ this.stop();
25
27
  }
26
28
 
27
29
  this.timer = setInterval(() => {
28
30
  try {
29
31
  this.options.onCleanup();
30
- } catch {
32
+ } catch (error) {
31
33
  // Silently handle cleanup errors to prevent timer from causing issues
34
+ // Log error in development for debugging
35
+ if (process.env.NODE_ENV === 'development') {
36
+ console.error('TimerManager cleanup error:', error);
37
+ }
32
38
  }
33
39
  }, this.options.cleanupIntervalMs);
34
40
  }
@@ -45,6 +45,7 @@ export interface ServiceInitializationResult {
45
45
  class FirebaseClientSingleton implements IFirebaseClient {
46
46
  private static instance: FirebaseClientSingleton | null = null;
47
47
  private state: FirebaseClientState;
48
+ private lastError: string | null = null;
48
49
 
49
50
  private constructor() {
50
51
  this.state = new FirebaseClientState();
@@ -58,23 +59,54 @@ class FirebaseClientSingleton implements IFirebaseClient {
58
59
  }
59
60
 
60
61
  initialize(config: FirebaseConfig): FirebaseApp | null {
61
- return FirebaseInitializationOrchestrator.initialize(config);
62
+ try {
63
+ const result = FirebaseInitializationOrchestrator.initialize(config);
64
+ // Sync state with orchestrator result
65
+ this.state.setInstance(result);
66
+ this.lastError = null;
67
+ return result;
68
+ } catch (error) {
69
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
70
+ this.lastError = errorMessage;
71
+ this.state.setInitializationError(errorMessage);
72
+ return null;
73
+ }
62
74
  }
63
75
 
64
76
  getApp(): FirebaseApp | null {
65
- return FirebaseInitializationOrchestrator.autoInitialize();
77
+ // Check local state first
78
+ const localApp = this.state.getApp();
79
+ if (localApp) return localApp;
80
+
81
+ // Try to get from orchestrator
82
+ const result = FirebaseInitializationOrchestrator.autoInitialize();
83
+ if (result) {
84
+ this.state.setInstance(result);
85
+ }
86
+ return result;
66
87
  }
67
88
 
68
89
  isInitialized(): boolean {
69
- return this.state.isInitialized();
90
+ // Check both local state and orchestrator for consistency
91
+ if (this.state.isInitialized()) return true;
92
+
93
+ // Check if Firebase has any apps initialized
94
+ return FirebaseInitializationOrchestrator.autoInitialize() !== null;
70
95
  }
71
96
 
72
97
  getInitializationError(): string | null {
73
- return this.state.getInitializationError();
98
+ // Check local state first
99
+ const localError = this.state.getInitializationError();
100
+ if (localError) return localError;
101
+ // Return last error
102
+ return this.lastError;
74
103
  }
75
104
 
76
105
  reset(): void {
106
+ // Reset local state
77
107
  this.state.reset();
108
+ this.lastError = null;
109
+ // Note: We don't reset Firebase apps as they might be in use
78
110
  }
79
111
  }
80
112
 
@@ -46,12 +46,27 @@ export function createFirebaseInitModule(
46
46
  critical,
47
47
  init: async () => {
48
48
  try {
49
- await initializeAllFirebaseServices(undefined, {
49
+ const result = await initializeAllFirebaseServices(undefined, {
50
50
  authInitializer: authInitializer ?? (() => Promise.resolve()),
51
51
  });
52
+
53
+ // Check if initialization was successful
54
+ if (!result.app) {
55
+ console.error('[Firebase] Initialization failed: Firebase app not initialized');
56
+ return false;
57
+ }
58
+
59
+ // Check if auth initialization failed
60
+ if (result.auth === false && result.authError) {
61
+ console.error(`[Firebase] Auth initialization failed: ${result.authError}`);
62
+ // Auth failure is not critical for the app to function
63
+ // Log the error but don't fail the entire initialization
64
+ }
65
+
52
66
  return true;
53
- } catch {
54
- // Return false to indicate failure, let the app initializer handle it
67
+ } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
69
+ console.error(`[Firebase] Initialization failed: ${errorMessage}`);
55
70
  return false;
56
71
  }
57
72
  },