Skip to content

Commit

Permalink
(iOS) Add support for Firestore real-time listeners.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dave Alden authored and dpa99c committed Sep 11, 2020
1 parent bccd22b commit 4aed3be
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 59 deletions.
3 changes: 3 additions & 0 deletions src/ios/FirebasePlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@
- (void)documentExistsInFirestoreCollection:(CDVInvokedUrlCommand*)command;
- (void)fetchDocumentInFirestoreCollection:(CDVInvokedUrlCommand*)command;
- (void)fetchFirestoreCollection:(CDVInvokedUrlCommand*)command;
- (void)listenToDocumentInFirestoreCollection:(CDVInvokedUrlCommand*)command;
- (void)listenToFirestoreCollection:(CDVInvokedUrlCommand*)command;
- (void)removeFirestoreListener:(CDVInvokedUrlCommand*)command;


// Internals
Expand Down
288 changes: 230 additions & 58 deletions src/ios/FirebasePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ @implementation FirebasePlugin
static FIRFirestore* firestore;
static NSUserDefaults* preferences;
static NSDictionary* googlePlist;
static NSMutableDictionary* firestoreListeners;


+ (FirebasePlugin*) firebasePlugin {
Expand Down Expand Up @@ -82,6 +83,7 @@ - (void)pluginInitialize {
[GIDSignIn sharedInstance].presentingViewController = self.viewController;

authCredentials = [[NSMutableDictionary alloc] init];
firestoreListeners = [[NSMutableDictionary alloc] init];
}@catch (NSException *exception) {
[self handlePluginExceptionWithoutContext:exception];
}
Expand Down Expand Up @@ -441,9 +443,7 @@ - (void)sendNotification:(NSDictionary *)userInfo {
return;
}
if (self.notificationCallbackId != nil) {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:userInfo];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.notificationCallbackId];
[self sendPluginDictionaryResultAndKeepCallback:userInfo command:self.commandDelegate callbackId:self.notificationCallbackId];
} else {
if (!self.notificationStack) {
self.notificationStack = [[NSMutableArray alloc] init];
Expand All @@ -464,9 +464,7 @@ - (void)sendNotification:(NSDictionary *)userInfo {
- (void)sendToken:(NSString *)token {
@try {
if (self.tokenRefreshCallbackId != nil) {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:token];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.tokenRefreshCallbackId];
[self sendPluginStringResultAndKeepCallback:token command:self.commandDelegate callbackId:self.tokenRefreshCallbackId];
}
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :self.commandDelegate];
Expand All @@ -476,9 +474,7 @@ - (void)sendToken:(NSString *)token {
- (void)sendApnsToken:(NSString *)token {
@try {
if (self.apnsTokenRefreshCallbackId != nil) {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:token];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.apnsTokenRefreshCallbackId];
[self sendPluginStringResultAndKeepCallback:token command:self.commandDelegate callbackId:self.apnsTokenRefreshCallbackId];
}
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :self.commandDelegate];
Expand Down Expand Up @@ -608,9 +604,7 @@ - (void)authenticateUserWithGoogle:(CDVInvokedUrlCommand*)command{
self.googleSignInCallbackId = command.callbackId;
[[GIDSignIn sharedInstance] signIn];

CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[self sendPluginNoResultAndKeepCallback:command callbackId:command.callbackId];
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :command];
}
Expand Down Expand Up @@ -1548,54 +1542,61 @@ - (void)fetchDocumentInFirestoreCollection:(CDVInvokedUrlCommand*)command {
}];
}

- (void)fetchFirestoreCollection:(CDVInvokedUrlCommand*)command {
- (void)listenToDocumentInFirestoreCollection:(CDVInvokedUrlCommand*)command {
[self.commandDelegate runInBackground:^{
@try {
NSString* collection = [command.arguments objectAtIndex:0];
NSArray* filters = [command.arguments objectAtIndex:1];
FIRQuery* query = [firestore collectionWithPath:collection];

for (int i = 0; i < [filters count]; i++) {
NSArray* filter = [filters objectAtIndex:i];
if ([[filter objectAtIndex:0] isEqualToString:@"where"]) {
if ([[filter objectAtIndex:2] isEqualToString:@"=="]) {
query = [query queryWhereField:[filter objectAtIndex:1] isEqualTo:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@"<"]) {
query = [query queryWhereField:[filter objectAtIndex:1] isLessThan:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@">"]) {
query = [query queryWhereField:[filter objectAtIndex:1] isGreaterThan:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@"<="]) {
query = [query queryWhereField:[filter objectAtIndex:1] isLessThanOrEqualTo:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@">="]) {
query = [query queryWhereField:[filter objectAtIndex:1] isGreaterThanOrEqualTo:[filter objectAtIndex:3]];
NSString* documentId = [command.arguments objectAtIndex:0];
NSString* collection = [command.arguments objectAtIndex:1];
bool includeMetadata = [command.arguments objectAtIndex:2];

id<FIRListenerRegistration> listener = [[[firestore collectionWithPath:collection] documentWithPath:documentId]
addSnapshotListenerWithIncludeMetadataChanges:includeMetadata
listener:^(FIRDocumentSnapshot *snapshot, NSError *error) {
@try {
if(snapshot != nil){
NSMutableDictionary* document = [[NSMutableDictionary alloc] init];;
[document setObject:@"change" forKey:@"eventType"];
if(snapshot.data != nil){
[document setObject:snapshot.data forKey:@"snapshot"];
}
if ([[filter objectAtIndex:2] isEqualToString:@"array-contains"]) {
query = [query queryWhereField:[filter objectAtIndex:1] arrayContains:[filter objectAtIndex:3]];
if(snapshot.metadata != nil){
[document setObject:[NSNumber numberWithBool:snapshot.metadata.fromCache] forKey:@"fromCache"];
[document setObject:snapshot.metadata.hasPendingWrites ? @"local" : @"remote" forKey:@"source"];
}
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"orderBy"]) {
query = [query queryOrderedByField:[filter objectAtIndex:1] descending:([[filter objectAtIndex:2] isEqualToString:@"desc"])];
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"startAt"]) {
query = [query queryStartingAtValues:[filter objectAtIndex:1]];
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"endAt"]) {
query = [query queryEndingAtValues:[filter objectAtIndex:1]];
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"limit"]) {
query = [query queryLimitedTo:[(NSNumber *)[filter objectAtIndex:1] integerValue]];
continue;
[self sendPluginDictionaryResultAndKeepCallback:document command:command callbackId:command.callbackId];
}else{
[self sendPluginErrorWithError:error command:command];
}
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :command];
}
}
}];

NSMutableDictionary* jsResult = [[NSMutableDictionary alloc] init];;
[jsResult setObject:@"id" forKey:@"eventType"];
NSNumber* key = [self saveFirestoreListener:listener];
[jsResult setObject:key forKey:@"id"];
[self sendPluginDictionaryResultAndKeepCallback:jsResult command:command callbackId:command.callbackId];
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :command];
}
}];
}

- (void)fetchFirestoreCollection:(CDVInvokedUrlCommand*)command {
[self.commandDelegate runInBackground:^{
@try {
NSString* collection = [command.arguments objectAtIndex:0];
NSArray* filters = nil;
if([command.arguments objectAtIndex:1] != [NSNull null]){
filters = [command.arguments objectAtIndex:1];
}

FIRQuery* query = [firestore collectionWithPath:collection];
if(filters != nil){
[self applyFiltersToFirestoreCollectionQuery:filters query:query];
}

[query getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error != nil) {
[self sendPluginErrorWithMessage:error.localizedDescription:command];
Expand All @@ -1613,13 +1614,178 @@ - (void)fetchFirestoreCollection:(CDVInvokedUrlCommand*)command {
}];
}

- (void)listenToFirestoreCollection:(CDVInvokedUrlCommand*)command {
[self.commandDelegate runInBackground:^{
@try {
NSString* collection = [command.arguments objectAtIndex:0];
NSArray* filters = nil;
if([command.arguments objectAtIndex:1] != [NSNull null]){
filters = [command.arguments objectAtIndex:1];
}
bool includeMetadata = [command.arguments objectAtIndex:2];

FIRQuery* query = [firestore collectionWithPath:collection];
if(filters != nil){
[self applyFiltersToFirestoreCollectionQuery:filters query:query];
}

id<FIRListenerRegistration> listener = [query
addSnapshotListenerWithIncludeMetadataChanges:includeMetadata
listener:^(FIRQuerySnapshot *snapshot, NSError *error) {
@try {
if(snapshot != nil){
NSMutableDictionary* jsResult = [[NSMutableDictionary alloc] init];
[jsResult setObject:@"change" forKey:@"eventType"];

NSMutableDictionary* documents = [[NSMutableDictionary alloc] init];
bool hasDocuments = false;
for (FIRDocumentChange* dc in snapshot.documentChanges) {
hasDocuments = true;
NSMutableDictionary* document = [[NSMutableDictionary alloc] init];
if (dc.type == FIRDocumentChangeTypeAdded) {
[document setObject:@"new" forKey:@"type"];
}else if (dc.type == FIRDocumentChangeTypeModified) {
[document setObject:@"modified" forKey:@"type"];
}else if (dc.type == FIRDocumentChangeTypeRemoved) {
[document setObject:@"removed" forKey:@"type"];
}else{
[document setObject:@"metadata" forKey:@"type"];
}
if(dc.document.data != nil){
[document setObject:dc.document.data forKey:@"snapshot"];
}
if(dc.document.metadata != nil){
[document setObject:[NSNumber numberWithBool:dc.document.metadata.fromCache] forKey:@"fromCache"];
[document setObject:dc.document.metadata.hasPendingWrites ? @"local" : @"remote" forKey:@"source"];
}
[documents setObject:document forKey:dc.document.documentID];
}
if(hasDocuments){
[jsResult setObject:documents forKey:@"documents"];
}
[self sendPluginDictionaryResultAndKeepCallback:jsResult command:command callbackId:command.callbackId];
}else{
[self sendPluginErrorWithError:error command:command];
}
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :command];
}
}
];

NSMutableDictionary* jsResult = [[NSMutableDictionary alloc] init];;
[jsResult setObject:@"id" forKey:@"eventType"];
NSNumber* key = [self saveFirestoreListener:listener];
[jsResult setObject:key forKey:@"id"];
[self sendPluginDictionaryResultAndKeepCallback:jsResult command:command callbackId:command.callbackId];
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :command];
}
}];
}

- (void) applyFiltersToFirestoreCollectionQuery:(NSArray*)filters query:(FIRQuery*)query {
for (int i = 0; i < [filters count]; i++) {
NSArray* filter = [filters objectAtIndex:i];
if ([[filter objectAtIndex:0] isEqualToString:@"where"]) {
if ([[filter objectAtIndex:2] isEqualToString:@"=="]) {
query = [query queryWhereField:[filter objectAtIndex:1] isEqualTo:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@"<"]) {
query = [query queryWhereField:[filter objectAtIndex:1] isLessThan:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@">"]) {
query = [query queryWhereField:[filter objectAtIndex:1] isGreaterThan:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@"<="]) {
query = [query queryWhereField:[filter objectAtIndex:1] isLessThanOrEqualTo:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@">="]) {
query = [query queryWhereField:[filter objectAtIndex:1] isGreaterThanOrEqualTo:[filter objectAtIndex:3]];
}
if ([[filter objectAtIndex:2] isEqualToString:@"array-contains"]) {
query = [query queryWhereField:[filter objectAtIndex:1] arrayContains:[filter objectAtIndex:3]];
}
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"orderBy"]) {
query = [query queryOrderedByField:[filter objectAtIndex:1] descending:([[filter objectAtIndex:2] isEqualToString:@"desc"])];
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"startAt"]) {
query = [query queryStartingAtValues:[filter objectAtIndex:1]];
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"endAt"]) {
query = [query queryEndingAtValues:[filter objectAtIndex:1]];
continue;
}
if ([[filter objectAtIndex:0] isEqualToString:@"limit"]) {
query = [query queryLimitedTo:[(NSNumber *)[filter objectAtIndex:1] integerValue]];
continue;
}
}
}

- (NSNumber*) saveFirestoreListener: (id<FIRListenerRegistration>) firestoreListener {
int id = [self generateId];
NSNumber* key = [NSNumber numberWithInt:id];
[firestoreListeners setObject:firestoreListener forKey:key];
return key;
}

- (void) removeFirestoreListener:(CDVInvokedUrlCommand*)command {
[self.commandDelegate runInBackground:^{
@try {
NSNumber* listenerId = @([[command.arguments objectAtIndex:0] intValue]);
bool removed = [self _removeFirestoreListener:listenerId];
if(removed){
[self sendPluginSuccess:command];
}else{
[self sendPluginErrorWithMessage:@"Listener ID not found" :command];
}
}@catch (NSException *exception) {
[self handlePluginExceptionWithContext:exception :command];
}
}];
}

- (bool) _removeFirestoreListener: (NSNumber*) key {
bool removed = false;
if([firestoreListeners objectForKey:key] != nil){
id<FIRListenerRegistration> firestoreListener = [firestoreListeners objectForKey:key];
[firestoreListener remove];
[firestoreListeners removeObjectForKey:key];
removed = true;
}
return removed;
}

/********************************/
#pragma mark - utility functions
/********************************/
- (void) sendPluginSuccess:(CDVInvokedUrlCommand*)command{
[self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId];
}

- (void) sendPluginNoResultAndKeepCallback:(CDVInvokedUrlCommand*)command callbackId:(NSString*)callbackId {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
}

- (void) sendPluginStringResultAndKeepCallback:(NSString*)result command:(CDVInvokedUrlCommand*)command callbackId:(NSString*)callbackId {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:result];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
}

- (void) sendPluginDictionaryResultAndKeepCallback:(NSDictionary*)result command:(CDVInvokedUrlCommand*)command callbackId:(NSString*)callbackId {
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
[pluginResult setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
}

- (void) sendPluginError:(CDVInvokedUrlCommand*)command{
[self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR] callbackId:command.callbackId];
}
Expand Down Expand Up @@ -1771,13 +1937,19 @@ - (void) handleAuthResult:(FIRAuthDataResult*) authResult error:(NSError*) error
}

- (int) saveAuthCredential: (FIRAuthCredential*) authCredential {
int key = [self generateId];
[authCredentials setObject:authCredential forKey:[NSNumber numberWithInt:key]];
return key;
}

- (int) generateId {
int key = -1;
while (key < 0 || [authCredentials objectForKey:[NSNumber numberWithInt:key]] != nil) {
while (key < 0
|| [authCredentials objectForKey:[NSNumber numberWithInt:key]] != nil
|| [firestoreListeners objectForKey:[NSNumber numberWithInt:key]] != nil
) {
key = arc4random_uniform(100000);
}

[authCredentials setObject:authCredential forKey:[NSNumber numberWithInt:key]];

return key;
}

Expand Down
Loading

0 comments on commit 4aed3be

Please sign in to comment.