cordova-plugin-firebasex-firestore 1.0.0

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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file FirebasexFirestorePlugin.h
3
+ * @brief Cordova plugin interface for Cloud Firestore on iOS.
4
+ *
5
+ * Provides CRUD operations, filtered collection queries, and real-time
6
+ * snapshot listeners with change tracking.
7
+ */
8
+ #import <Cordova/CDVPlugin.h>
9
+ @import FirebaseFirestore;
10
+
11
+ /**
12
+ * @brief Cordova plugin class for Cloud Firestore on iOS.
13
+ *
14
+ * @see https://firebase.google.com/docs/firestore
15
+ */
16
+ @interface FirebasexFirestorePlugin : CDVPlugin
17
+
18
+ /** Adds a document with auto-generated ID. @param command args[0]: document, args[1]: collection, args[2]: timestamp. */
19
+ - (void)addDocumentToFirestoreCollection:(CDVInvokedUrlCommand *)command;
20
+ /** Sets/overwrites a document by ID. @param command args[0]: documentId, args[1]: document, args[2]: collection, args[3]: timestamp. */
21
+ - (void)setDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command;
22
+ /** Updates fields of an existing document. @param command args[0]: documentId, args[1]: document, args[2]: collection, args[3]: timestamp. */
23
+ - (void)updateDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command;
24
+ /** Deletes a document by ID. @param command args[0]: documentId, args[1]: collection. */
25
+ - (void)deleteDocumentFromFirestoreCollection:(CDVInvokedUrlCommand *)command;
26
+ /** Checks if a document exists. @param command args[0]: documentId, args[1]: collection. */
27
+ - (void)documentExistsInFirestoreCollection:(CDVInvokedUrlCommand *)command;
28
+ /** Fetches a single document by ID. @param command args[0]: documentId, args[1]: collection. */
29
+ - (void)fetchDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command;
30
+ /** Fetches a collection with optional filters. @param command args[0]: collection, args[1]: filters array. */
31
+ - (void)fetchFirestoreCollection:(CDVInvokedUrlCommand *)command;
32
+ /** Listens for real-time changes on a document. @param command args[0]: documentId, args[1]: collection, args[2]: includeMetadata. */
33
+ - (void)listenToDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command;
34
+ /** Listens for real-time changes on a collection. @param command args[0]: collection, args[1]: filters, args[2]: includeMetadata. */
35
+ - (void)listenToFirestoreCollection:(CDVInvokedUrlCommand *)command;
36
+ /** Removes a snapshot listener by ID. @param command args[0]: listenerId. */
37
+ - (void)removeFirestoreListener:(CDVInvokedUrlCommand *)command;
38
+
39
+ @end
@@ -0,0 +1,575 @@
1
+ /**
2
+ * @file FirebasexFirestorePlugin.m
3
+ * @brief iOS implementation of the FirebaseX Cloud Firestore Cordova plugin.
4
+ */
5
+ #import "FirebasexFirestorePlugin.h"
6
+ #import "FirebasexCorePlugin.h"
7
+
8
+ @interface FirebasexFirestorePlugin ()
9
+ /** Cloud Firestore instance. */
10
+ @property(nonatomic, strong) FIRFirestore *firestore;
11
+ /** Registry of active snapshot listeners keyed by generated numeric IDs. */
12
+ @property(nonatomic, strong) NSMutableDictionary *firestoreListeners;
13
+ @end
14
+
15
+ @implementation FirebasexFirestorePlugin
16
+
17
+ /** Initialises the plugin, obtaining the Firestore instance and creating the listener registry. */
18
+ - (void)pluginInitialize {
19
+ NSLog(@"FirebasexFirestorePlugin: pluginInitialize");
20
+ self.firestore = [FIRFirestore firestore];
21
+ self.firestoreListeners = [[NSMutableDictionary alloc] init];
22
+ }
23
+
24
+ #pragma mark - ID generation
25
+
26
+ /** Generates a unique random numeric ID not already in use by the listener registry. */
27
+ - (int)generateId {
28
+ int key = -1;
29
+ while (key < 0 || [self.firestoreListeners objectForKey:[NSNumber numberWithInt:key]] != nil) {
30
+ key = arc4random_uniform(100000);
31
+ }
32
+ return key;
33
+ }
34
+
35
+ /**
36
+ * Saves a listener registration and returns its generated numeric key.
37
+ * Thread-safe via @synchronized.
38
+ */
39
+ - (NSNumber *)saveFirestoreListener:(id<FIRListenerRegistration>)firestoreListener {
40
+ @synchronized(self.firestoreListeners) {
41
+ int listenerId = [self generateId];
42
+ NSNumber *key = [NSNumber numberWithInt:listenerId];
43
+ [self.firestoreListeners setObject:firestoreListener forKey:key];
44
+ return key;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Removes and detaches a Firestore listener by its numeric key.
50
+ * Thread-safe via @synchronized.
51
+ *
52
+ * @return YES if a listener was found and removed.
53
+ */
54
+ - (bool)_removeFirestoreListener:(NSNumber *)key {
55
+ @synchronized(self.firestoreListeners) {
56
+ bool removed = false;
57
+ if ([self.firestoreListeners objectForKey:key] != nil) {
58
+ id<FIRListenerRegistration> firestoreListener = [self.firestoreListeners objectForKey:key];
59
+ [firestoreListener remove];
60
+ [self.firestoreListeners removeObjectForKey:key];
61
+ removed = true;
62
+ }
63
+ return removed;
64
+ }
65
+ }
66
+
67
+ #pragma mark - Data sanitization
68
+
69
+ /**
70
+ * Recursively sanitises a Firestore data dictionary.
71
+ * Converts FIRDocumentReference to path strings, FIRTimestamp to seconds/nanoseconds
72
+ * dictionaries, and filters out NaN/Infinity values.
73
+ */
74
+ - (NSMutableDictionary *)sanitiseFirestoreDataDictionary:(NSDictionary *)data {
75
+ NSMutableDictionary *sanitisedData = [[NSMutableDictionary alloc] init];
76
+ for (id key in data) {
77
+ id value = [data objectForKey:key];
78
+ value = [self sanitizeFirestoreData:value];
79
+ [sanitisedData setValue:value forKey:key];
80
+ }
81
+ return sanitisedData;
82
+ }
83
+
84
+ /**
85
+ * Sanitises a single Firestore value, converting Firestore-specific types
86
+ * to JSON-compatible representations.
87
+ */
88
+ - (id)sanitizeFirestoreData:(id)value {
89
+ if ([value isKindOfClass:[FIRDocumentReference class]]) {
90
+ FIRDocumentReference *reference = (FIRDocumentReference *)value;
91
+ return reference.path;
92
+ } else if ([value isKindOfClass:[NSDictionary class]]) {
93
+ return [self sanitiseFirestoreDataDictionary:value];
94
+ } else if ([value isKindOfClass:[NSArray class]]) {
95
+ NSMutableArray *array = [[NSMutableArray alloc] init];
96
+ for (id element in value) {
97
+ id sanitizedValue = [self sanitizeFirestoreData:element];
98
+ [array addObject:sanitizedValue];
99
+ }
100
+ return array;
101
+ } else if ([value isKindOfClass:[FIRTimestamp class]]) {
102
+ FIRTimestamp *dateTimestamp = (FIRTimestamp *)value;
103
+ NSDictionary *dateDictionary = @{
104
+ @"nanoseconds": [NSNumber numberWithInt:dateTimestamp.nanoseconds],
105
+ @"seconds": [NSNumber numberWithLong:dateTimestamp.seconds]
106
+ };
107
+ return dateDictionary;
108
+ } else if ([value isKindOfClass:[NSNumber class]]) {
109
+ double number = [value doubleValue];
110
+ if (isnan(number) || isinf(number)) {
111
+ return nil;
112
+ }
113
+ }
114
+ return value;
115
+ }
116
+
117
+ #pragma mark - Query filters
118
+
119
+ /**
120
+ * Applies an array of filter definitions to a Firestore query.
121
+ * Supports where (==, <, >, <=, >=, array-contains), orderBy, startAt, endAt, and limit.
122
+ */
123
+ - (FIRQuery *)applyFiltersToFirestoreCollectionQuery:(NSArray *)filters query:(FIRQuery *)query {
124
+ for (int i = 0; i < [filters count]; i++) {
125
+ NSArray *filter = [filters objectAtIndex:i];
126
+ if ([[filter objectAtIndex:0] isEqualToString:@"where"]) {
127
+ if ([[filter objectAtIndex:2] isEqualToString:@"=="]) {
128
+ query = [query queryWhereField:[filter objectAtIndex:1]
129
+ isEqualTo:[self getFilterValueAsType:filter valueIndex:3 typeIndex:4]];
130
+ }
131
+ if ([[filter objectAtIndex:2] isEqualToString:@"<"]) {
132
+ query = [query queryWhereField:[filter objectAtIndex:1]
133
+ isLessThan:[self getFilterValueAsType:filter valueIndex:3 typeIndex:4]];
134
+ }
135
+ if ([[filter objectAtIndex:2] isEqualToString:@">"]) {
136
+ query = [query queryWhereField:[filter objectAtIndex:1]
137
+ isGreaterThan:[self getFilterValueAsType:filter valueIndex:3 typeIndex:4]];
138
+ }
139
+ if ([[filter objectAtIndex:2] isEqualToString:@"<="]) {
140
+ query = [query queryWhereField:[filter objectAtIndex:1]
141
+ isLessThanOrEqualTo:[self getFilterValueAsType:filter valueIndex:3 typeIndex:4]];
142
+ }
143
+ if ([[filter objectAtIndex:2] isEqualToString:@">="]) {
144
+ query = [query queryWhereField:[filter objectAtIndex:1]
145
+ isGreaterThanOrEqualTo:[self getFilterValueAsType:filter valueIndex:3 typeIndex:4]];
146
+ }
147
+ if ([[filter objectAtIndex:2] isEqualToString:@"array-contains"]) {
148
+ query = [query queryWhereField:[filter objectAtIndex:1]
149
+ arrayContains:[self getFilterValueAsType:filter valueIndex:3 typeIndex:4]];
150
+ }
151
+ continue;
152
+ }
153
+ if ([[filter objectAtIndex:0] isEqualToString:@"orderBy"]) {
154
+ query = [query queryOrderedByField:[filter objectAtIndex:1]
155
+ descending:([[filter objectAtIndex:2] isEqualToString:@"desc"])];
156
+ continue;
157
+ }
158
+ if ([[filter objectAtIndex:0] isEqualToString:@"startAt"]) {
159
+ query = [query queryStartingAtValues:[self getFilterValueAsType:filter valueIndex:1 typeIndex:2]];
160
+ continue;
161
+ }
162
+ if ([[filter objectAtIndex:0] isEqualToString:@"endAt"]) {
163
+ query = [query queryEndingAtValues:[self getFilterValueAsType:filter valueIndex:1 typeIndex:2]];
164
+ continue;
165
+ }
166
+ if ([[filter objectAtIndex:0] isEqualToString:@"limit"]) {
167
+ query = [query queryLimitedTo:[(NSNumber *)[filter objectAtIndex:1] integerValue]];
168
+ continue;
169
+ }
170
+ }
171
+ return query;
172
+ }
173
+
174
+ /**
175
+ * Extracts a typed value from a filter array. Supports boolean, integer, long, double, and string types.
176
+ */
177
+ - (id)getFilterValueAsType:(NSArray *)filter valueIndex:(int)valueIndex typeIndex:(int)typeIndex {
178
+ id typedValue = [filter objectAtIndex:valueIndex];
179
+
180
+ NSString *type = @"string";
181
+ if ([filter objectAtIndex:typeIndex] != nil) {
182
+ type = [filter objectAtIndex:typeIndex];
183
+ }
184
+
185
+ if ([type isEqual:@"boolean"]) {
186
+ if ([typedValue isKindOfClass:[NSNumber class]]) {
187
+ typedValue = [NSNumber numberWithBool:typedValue];
188
+ } else if ([typedValue isKindOfClass:[NSString class]]) {
189
+ bool boolValue = [typedValue boolValue];
190
+ typedValue = [NSNumber numberWithBool:boolValue];
191
+ }
192
+ } else if ([type isEqual:@"integer"] || [type isEqual:@"long"]) {
193
+ if ([typedValue isKindOfClass:[NSString class]]) {
194
+ NSInteger intValue = [typedValue integerValue];
195
+ typedValue = [NSNumber numberWithInteger:intValue];
196
+ }
197
+ } else if ([type isEqual:@"double"]) {
198
+ if ([typedValue isKindOfClass:[NSString class]]) {
199
+ double doubleValue = [typedValue doubleValue];
200
+ typedValue = [NSNumber numberWithDouble:doubleValue];
201
+ }
202
+ } else { // string
203
+ if ([typedValue isKindOfClass:[NSNumber class]]) {
204
+ if ([self isBoolNumber:typedValue]) {
205
+ bool boolValue = [typedValue boolValue];
206
+ typedValue = boolValue ? @"true" : @"false";
207
+ } else {
208
+ typedValue = [typedValue stringValue];
209
+ }
210
+ }
211
+ }
212
+
213
+ return typedValue;
214
+ }
215
+
216
+ /** Checks if an NSNumber is a boolean by comparing CFTypeIDs. */
217
+ - (BOOL)isBoolNumber:(NSNumber *)num {
218
+ CFTypeID boolID = CFBooleanGetTypeID();
219
+ CFTypeID numID = CFGetTypeID((__bridge CFTypeRef)(num));
220
+ return numID == boolID;
221
+ }
222
+
223
+ #pragma mark - CRUD operations
224
+
225
+ /**
226
+ * Adds a new document with auto-generated ID to a collection.
227
+ * If timestamp is true, adds 'created' and 'lastUpdate' FIRTimestamp fields.
228
+ */
229
+ - (void)addDocumentToFirestoreCollection:(CDVInvokedUrlCommand *)command {
230
+ [self.commandDelegate runInBackground:^{
231
+ @try {
232
+ NSDictionary *document = [command.arguments objectAtIndex:0];
233
+ NSString *collection = [command.arguments objectAtIndex:1];
234
+ bool timestamp = [[command.arguments objectAtIndex:2] boolValue];
235
+
236
+ NSMutableDictionary *document_mutable = [document mutableCopy];
237
+
238
+ if (timestamp) {
239
+ document_mutable[@"created"] = [FIRTimestamp timestampWithDate:[NSDate date]];
240
+ document_mutable[@"lastUpdate"] = [FIRTimestamp timestampWithDate:[NSDate date]];
241
+ }
242
+
243
+ __block FIRDocumentReference *ref = [[self.firestore collectionWithPath:collection]
244
+ addDocumentWithData:document_mutable
245
+ completion:^(NSError *_Nullable error) {
246
+ [[FirebasexCorePlugin sharedInstance] handleStringResultWithPotentialError:error
247
+ command:command
248
+ result:ref.documentID];
249
+ }];
250
+ } @catch (NSException *exception) {
251
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
252
+ }
253
+ }];
254
+ }
255
+
256
+ /**
257
+ * Creates or overwrites a document with a specific ID.
258
+ * If timestamp is true, adds a 'lastUpdate' FIRTimestamp field.
259
+ */
260
+ - (void)setDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command {
261
+ [self.commandDelegate runInBackground:^{
262
+ @try {
263
+ NSString *documentId = [command.arguments objectAtIndex:0];
264
+ NSDictionary *document = [command.arguments objectAtIndex:1];
265
+ NSString *collection = [command.arguments objectAtIndex:2];
266
+ bool timestamp = [[command.arguments objectAtIndex:3] boolValue];
267
+
268
+ NSMutableDictionary *document_mutable = [document mutableCopy];
269
+
270
+ if (timestamp) {
271
+ document_mutable[@"lastUpdate"] = [FIRTimestamp timestampWithDate:[NSDate date]];
272
+ }
273
+
274
+ [[[self.firestore collectionWithPath:collection] documentWithPath:documentId]
275
+ setData:document_mutable
276
+ completion:^(NSError *_Nullable error) {
277
+ [[FirebasexCorePlugin sharedInstance] handleEmptyResultWithPotentialError:error command:command];
278
+ }];
279
+ } @catch (NSException *exception) {
280
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
281
+ }
282
+ }];
283
+ }
284
+
285
+ /**
286
+ * Updates specific fields of an existing document.
287
+ * Returns an error if the document does not exist.
288
+ */
289
+ - (void)updateDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command {
290
+ [self.commandDelegate runInBackground:^{
291
+ @try {
292
+ NSString *documentId = [command.arguments objectAtIndex:0];
293
+ NSDictionary *document = [command.arguments objectAtIndex:1];
294
+ NSString *collection = [command.arguments objectAtIndex:2];
295
+ bool timestamp = [[command.arguments objectAtIndex:3] boolValue];
296
+
297
+ NSMutableDictionary *document_mutable = [document mutableCopy];
298
+
299
+ if (timestamp) {
300
+ document_mutable[@"lastUpdate"] = [FIRTimestamp timestampWithDate:[NSDate date]];
301
+ }
302
+
303
+ FIRDocumentReference *docRef = [[self.firestore collectionWithPath:collection] documentWithPath:documentId];
304
+ if (docRef != nil) {
305
+ [docRef updateData:document_mutable
306
+ completion:^(NSError *_Nullable error) {
307
+ [[FirebasexCorePlugin sharedInstance] handleEmptyResultWithPotentialError:error command:command];
308
+ }];
309
+ } else {
310
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:@"Document not found in collection" :command];
311
+ }
312
+ } @catch (NSException *exception) {
313
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
314
+ }
315
+ }];
316
+ }
317
+
318
+ /** Deletes a document from a collection by its ID. */
319
+ - (void)deleteDocumentFromFirestoreCollection:(CDVInvokedUrlCommand *)command {
320
+ [self.commandDelegate runInBackground:^{
321
+ @try {
322
+ NSString *documentId = [command.arguments objectAtIndex:0];
323
+ NSString *collection = [command.arguments objectAtIndex:1];
324
+
325
+ [[[self.firestore collectionWithPath:collection] documentWithPath:documentId]
326
+ deleteDocumentWithCompletion:^(NSError *_Nullable error) {
327
+ [[FirebasexCorePlugin sharedInstance] handleEmptyResultWithPotentialError:error command:command];
328
+ }];
329
+ } @catch (NSException *exception) {
330
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
331
+ }
332
+ }];
333
+ }
334
+
335
+ /** Checks whether a document exists in a collection. Returns boolean. */
336
+ - (void)documentExistsInFirestoreCollection:(CDVInvokedUrlCommand *)command {
337
+ [self.commandDelegate runInBackground:^{
338
+ @try {
339
+ NSString *documentId = [command.arguments objectAtIndex:0];
340
+ NSString *collection = [command.arguments objectAtIndex:1];
341
+
342
+ FIRDocumentReference *docRef = [[self.firestore collectionWithPath:collection] documentWithPath:documentId];
343
+ if (docRef != nil) {
344
+ [docRef getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error) {
345
+ BOOL docExists = snapshot.data != nil;
346
+ [[FirebasexCorePlugin sharedInstance] handleBoolResultWithPotentialError:error command:command result:docExists];
347
+ }];
348
+ } else {
349
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:@"Collection not found" :command];
350
+ }
351
+ } @catch (NSException *exception) {
352
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
353
+ }
354
+ }];
355
+ }
356
+
357
+ /** Fetches a single document by ID and returns its sanitised data. */
358
+ - (void)fetchDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command {
359
+ [self.commandDelegate runInBackground:^{
360
+ @try {
361
+ NSString *documentId = [command.arguments objectAtIndex:0];
362
+ NSString *collection = [command.arguments objectAtIndex:1];
363
+
364
+ FIRDocumentReference *docRef = [[self.firestore collectionWithPath:collection] documentWithPath:documentId];
365
+ if (docRef != nil) {
366
+ [docRef getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error) {
367
+ if (error != nil) {
368
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:error.localizedDescription :command];
369
+ } else if (snapshot.data != nil) {
370
+ [self.commandDelegate
371
+ sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK
372
+ messageAsDictionary:[self sanitiseFirestoreDataDictionary:snapshot.data]]
373
+ callbackId:command.callbackId];
374
+ } else {
375
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:@"Document not found in collection" :command];
376
+ }
377
+ }];
378
+ } else {
379
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:@"Collection not found" :command];
380
+ }
381
+ } @catch (NSException *exception) {
382
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
383
+ }
384
+ }];
385
+ }
386
+
387
+ #pragma mark - Collection queries
388
+
389
+ /**
390
+ * Fetches all documents in a collection with optional filters.
391
+ * Returns a dictionary mapping document IDs to their sanitised data.
392
+ */
393
+ - (void)fetchFirestoreCollection:(CDVInvokedUrlCommand *)command {
394
+ [self.commandDelegate runInBackground:^{
395
+ @try {
396
+ NSString *collection = [command.arguments objectAtIndex:0];
397
+ NSArray *filters = nil;
398
+ if ([command.arguments objectAtIndex:1] != [NSNull null]) {
399
+ filters = [command.arguments objectAtIndex:1];
400
+ }
401
+
402
+ FIRQuery *query = [self.firestore collectionWithPath:collection];
403
+ if (filters != nil) {
404
+ query = [self applyFiltersToFirestoreCollectionQuery:filters query:query];
405
+ }
406
+
407
+ [query getDocumentsWithCompletion:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
408
+ if (error != nil) {
409
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:error.localizedDescription :command];
410
+ } else {
411
+ NSMutableDictionary *documents = [[NSMutableDictionary alloc] init];
412
+ for (FIRDocumentSnapshot *document in snapshot.documents) {
413
+ [documents setObject:[self sanitiseFirestoreDataDictionary:document.data]
414
+ forKey:document.documentID];
415
+ }
416
+ [self.commandDelegate
417
+ sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK
418
+ messageAsDictionary:documents]
419
+ callbackId:command.callbackId];
420
+ }
421
+ }];
422
+ } @catch (NSException *exception) {
423
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
424
+ }
425
+ }];
426
+ }
427
+
428
+ #pragma mark - Listeners
429
+
430
+ /**
431
+ * Registers a real-time listener on a single document.
432
+ * First sends the listener ID, then sends change events with snapshot, source, and fromCache.
433
+ */
434
+ - (void)listenToDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command {
435
+ [self.commandDelegate runInBackground:^{
436
+ @try {
437
+ NSString *documentId = [command.arguments objectAtIndex:0];
438
+ NSString *collection = [command.arguments objectAtIndex:1];
439
+ bool includeMetadata = [[command.arguments objectAtIndex:2] boolValue];
440
+
441
+ id<FIRListenerRegistration> listener =
442
+ [[[self.firestore collectionWithPath:collection] documentWithPath:documentId]
443
+ addSnapshotListenerWithIncludeMetadataChanges:includeMetadata
444
+ listener:^(FIRDocumentSnapshot *snapshot, NSError *error) {
445
+ @try {
446
+ if (snapshot != nil) {
447
+ NSMutableDictionary *document = [[NSMutableDictionary alloc] init];
448
+ [document setObject:@"change" forKey:@"eventType"];
449
+ if (snapshot.data != nil) {
450
+ [document setObject:[self sanitiseFirestoreDataDictionary:snapshot.data] forKey:@"snapshot"];
451
+ }
452
+ if (snapshot.metadata != nil) {
453
+ [document setObject:[NSNumber numberWithBool:snapshot.metadata.fromCache] forKey:@"fromCache"];
454
+ [document setObject:snapshot.metadata.hasPendingWrites ? @"local" : @"remote" forKey:@"source"];
455
+ }
456
+ [[FirebasexCorePlugin sharedInstance] sendPluginDictionaryResultAndKeepCallback:[self sanitiseFirestoreDataDictionary:document]
457
+ command:command
458
+ callbackId:command.callbackId];
459
+ } else {
460
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithError:error command:command];
461
+ }
462
+ } @catch (NSException *exception) {
463
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
464
+ }
465
+ }];
466
+
467
+ NSMutableDictionary *jsResult = [[NSMutableDictionary alloc] init];
468
+ [jsResult setObject:@"id" forKey:@"eventType"];
469
+ NSNumber *key = [self saveFirestoreListener:listener];
470
+ [jsResult setObject:key forKey:@"id"];
471
+ [[FirebasexCorePlugin sharedInstance] sendPluginDictionaryResultAndKeepCallback:jsResult
472
+ command:command
473
+ callbackId:command.callbackId];
474
+ } @catch (NSException *exception) {
475
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
476
+ }
477
+ }];
478
+ }
479
+
480
+ /**
481
+ * Registers a real-time listener on a collection with optional filters.
482
+ * First sends the listener ID, then sends change events with document change types
483
+ * ("new", "modified", "removed"), snapshots, source, and fromCache.
484
+ */
485
+ - (void)listenToFirestoreCollection:(CDVInvokedUrlCommand *)command {
486
+ [self.commandDelegate runInBackground:^{
487
+ @try {
488
+ NSString *collection = [command.arguments objectAtIndex:0];
489
+ NSArray *filters = nil;
490
+ if ([command.arguments objectAtIndex:1] != [NSNull null]) {
491
+ filters = [command.arguments objectAtIndex:1];
492
+ }
493
+ bool includeMetadata = [[command.arguments objectAtIndex:2] boolValue];
494
+
495
+ FIRQuery *query = [self.firestore collectionWithPath:collection];
496
+ if (filters != nil) {
497
+ query = [self applyFiltersToFirestoreCollectionQuery:filters query:query];
498
+ }
499
+
500
+ id<FIRListenerRegistration> listener =
501
+ [query addSnapshotListenerWithIncludeMetadataChanges:includeMetadata
502
+ listener:^(FIRQuerySnapshot *snapshot, NSError *error) {
503
+ @try {
504
+ if (snapshot != nil) {
505
+ NSMutableDictionary *jsResult = [[NSMutableDictionary alloc] init];
506
+ [jsResult setObject:@"change" forKey:@"eventType"];
507
+
508
+ NSMutableDictionary *documents = [[NSMutableDictionary alloc] init];
509
+ bool hasDocuments = false;
510
+ for (FIRDocumentChange *dc in snapshot.documentChanges) {
511
+ hasDocuments = true;
512
+ NSMutableDictionary *document = [[NSMutableDictionary alloc] init];
513
+ if (dc.type == FIRDocumentChangeTypeAdded) {
514
+ [document setObject:@"new" forKey:@"type"];
515
+ } else if (dc.type == FIRDocumentChangeTypeModified) {
516
+ [document setObject:@"modified" forKey:@"type"];
517
+ } else if (dc.type == FIRDocumentChangeTypeRemoved) {
518
+ [document setObject:@"removed" forKey:@"type"];
519
+ } else {
520
+ [document setObject:@"metadata" forKey:@"type"];
521
+ }
522
+ if (dc.document.data != nil) {
523
+ [document setObject:[self sanitiseFirestoreDataDictionary:dc.document.data] forKey:@"snapshot"];
524
+ }
525
+ if (dc.document.metadata != nil) {
526
+ [document setObject:[NSNumber numberWithBool:dc.document.metadata.fromCache] forKey:@"fromCache"];
527
+ [document setObject:dc.document.metadata.hasPendingWrites ? @"local" : @"remote" forKey:@"source"];
528
+ }
529
+ [documents setObject:document forKey:dc.document.documentID];
530
+ }
531
+ if (hasDocuments) {
532
+ [jsResult setObject:documents forKey:@"documents"];
533
+ }
534
+ [[FirebasexCorePlugin sharedInstance] sendPluginDictionaryResultAndKeepCallback:jsResult
535
+ command:command
536
+ callbackId:command.callbackId];
537
+ } else {
538
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithError:error command:command];
539
+ }
540
+ } @catch (NSException *exception) {
541
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
542
+ }
543
+ }];
544
+
545
+ NSMutableDictionary *jsResult = [[NSMutableDictionary alloc] init];
546
+ [jsResult setObject:@"id" forKey:@"eventType"];
547
+ NSNumber *key = [self saveFirestoreListener:listener];
548
+ [jsResult setObject:key forKey:@"id"];
549
+ [[FirebasexCorePlugin sharedInstance] sendPluginDictionaryResultAndKeepCallback:jsResult
550
+ command:command
551
+ callbackId:command.callbackId];
552
+ } @catch (NSException *exception) {
553
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
554
+ }
555
+ }];
556
+ }
557
+
558
+ /** Removes a previously registered Firestore snapshot listener by its numeric ID. */
559
+ - (void)removeFirestoreListener:(CDVInvokedUrlCommand *)command {
560
+ [self.commandDelegate runInBackground:^{
561
+ @try {
562
+ NSNumber *listenerId = @([[command.arguments objectAtIndex:0] intValue]);
563
+ bool removed = [self _removeFirestoreListener:listenerId];
564
+ if (removed) {
565
+ [[FirebasexCorePlugin sharedInstance] sendPluginSuccess:command];
566
+ } else {
567
+ [[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:@"Listener ID not found" :command];
568
+ }
569
+ } @catch (NSException *exception) {
570
+ [[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
571
+ }
572
+ }];
573
+ }
574
+
575
+ @end
@@ -0,0 +1,68 @@
1
+ interface FirebasexFirestore {
2
+ addDocumentToFirestoreCollection(
3
+ document: object,
4
+ collection: string,
5
+ timestamp: boolean,
6
+ success: (documentId: string) => void,
7
+ error: (err: string) => void
8
+ ): void;
9
+ setDocumentInFirestoreCollection(
10
+ documentId: string,
11
+ document: object,
12
+ collection: string,
13
+ timestamp: boolean,
14
+ success: () => void,
15
+ error: (err: string) => void
16
+ ): void;
17
+ updateDocumentInFirestoreCollection(
18
+ documentId: string,
19
+ document: object,
20
+ collection: string,
21
+ timestamp: boolean,
22
+ success: () => void,
23
+ error: (err: string) => void
24
+ ): void;
25
+ deleteDocumentFromFirestoreCollection(
26
+ documentId: string,
27
+ collection: string,
28
+ success: () => void,
29
+ error: (err: string) => void
30
+ ): void;
31
+ documentExistsInFirestoreCollection(
32
+ documentId: string,
33
+ collection: string,
34
+ success: (exists: boolean) => void,
35
+ error: (err: string) => void
36
+ ): void;
37
+ fetchDocumentInFirestoreCollection(
38
+ documentId: string,
39
+ collection: string,
40
+ success: (document: object) => void,
41
+ error: (err: string) => void
42
+ ): void;
43
+ fetchFirestoreCollection(
44
+ collection: string,
45
+ filters?: object[],
46
+ success?: (collection: object) => void,
47
+ error?: (err: string) => void
48
+ ): void;
49
+ listenToDocumentInFirestoreCollection(
50
+ success: (event: object) => void,
51
+ error: (err: string) => void,
52
+ documentId: string,
53
+ collection: string,
54
+ includeMetadata?: boolean
55
+ ): void;
56
+ listenToFirestoreCollection(
57
+ success: (event: object) => void,
58
+ error: (err: string) => void,
59
+ collection: string,
60
+ filters?: object[],
61
+ includeMetadata?: boolean
62
+ ): void;
63
+ removeFirestoreListener(
64
+ success: () => void,
65
+ error: (err: string) => void,
66
+ listenerId: string
67
+ ): void;
68
+ }