2

I have an application which implements remote notifications via firebase messaging api. In this app, I have implemented a notification service extension, which among others, implement UNNotificationActions.

In one of these actions, I've implemented an input field where you can write something, which then should be posted to firestore.

I've tried implementing this, but without success. So my question is how can I write to firestore from a rich notification running in the background - is this even possible?

My implementation looks like this:

    let likeAction = UNNotificationAction(identifier: "likeAction", title: "Like", options: [])
    let commentAction = UNTextInputNotificationAction(identifier: "commentAction", title: "Comment", options: [UNNotificationActionOptions.authenticationRequired], textInputButtonTitle: "Send", textInputPlaceholder: "Type your message")

    let category = UNNotificationCategory(identifier: "posts", actions: [likeAction, commentAction], intentIdentifiers: [], options: [])
    UNUserNotificationCenter.current().setNotificationCategories([category])
    

Then in AppDelegate, I implement the function to run whenever this is triggered like this:

 func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    switch response.actionIdentifier {
    case "commentAction":
      guard let data = response.notification.request.content.userInfo["data"] as? [String: Any] else { return }
      
      guard
        let channelName = data["channelName"],
        let postId = data["postId"]
        else { return }

      if let message = response as? UNTextInputNotificationResponse {
        let documentPath = "\(channelName)/\(postId))"
        
        let post = Post()
        post.documentPath = documentPath
        post.addComment(text: message.userText, postDocumentPath: documentPath)
      }

I've debugged the code, and the method post.addComment() does actually get fired, and every field has a value. When I check the database, nothing gets inserted into it. The console prints out this, which I don't know if is related to the problem, I haven't been able to find anything online about these lines:

dnssd_clientstub deliver_request ERROR: write_all(21, 65 bytes) failed

nssd_clientstub read_all(26) DEFUNCT

When running the post method, no error from firebase comes up.

This was the initial information I could think of. I can provide more code or info if need be.

Update

I've discovered if I press the button while on lock screen, but with the iPhone in an unlocked state, nothing happens - as soon as I swipe up, and the app shows, the request gets send. This does indeed seem to be a background issue.

Kind regards Chris

Community
  • 1
  • 1
ChrisEenberg
  • 773
  • 1
  • 5
  • 19

2 Answers2

1

So I finally found a solution / workaround.

It turns out that firebase will not post in the background, instead it stores the data locally, until the app comes into foreground, then it will post it.

The solution was to add another firebase function that listens for HTTP requests. In this function, I added a method to post to the database with the data from the action.

I then do a regular but modified http post request with Alamofire from the action to the url like this:

      let headers: HTTPHeaders = [
        "Authorization": "Bearer \(token)",
        "Content-Type": "application/json"
      ]
      let configuration = URLSessionConfiguration.default
      configuration.timeoutIntervalForRequest = 15.0
      configuration.timeoutIntervalForResource = 15.0
      configuration.waitsForConnectivity = true
      self.alamofireManager = Alamofire.SessionManager(configuration: configuration)

      guard let manager = self.alamofireManager else {
        return
      }

      manager.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString(completionHandler: { (response) in
        let _ = manager
        print(response)
        closure(true)
      })

The important part of this, for me, was to set configuration.waitsForConnectivity = true otherwise, it would come back saying no connection to the internet.

This enables the function to comment on a notification from the lock screen.

Hope this information helps others looking for the same.

ChrisEenberg
  • 773
  • 1
  • 5
  • 19
1

For anyone using a standard NSURLSession to complete the firestore HTTP REST request (and not Alamofire like in @ChrisEenberg's excellent answer above)

[NB: A previous version of this answer used background tasks, which are not necessary. This edit uses an ordinary NSURLSessionDataTask upload task and appears to work when app is backgrounded or closed, as expected.]


OBJECTIVE-C

Inside didReceiveNotificationResponse:

  1. Prepare request

        // Grab authenticated firestore user
            FIRUser *db_user = [FIRAuth auth].currentUser;
    
        // Set up the response
            NSDictionary *reqFields;
    
        // Fields for firestore object (REST API)
            NSDictionary *my_uid_val = [[NSDictionary alloc] initWithObjectsAndKeys: (db_user.uid ?: [NSNull null]), (db_user.uid ? @"stringValue" : @"nullValue"), nil];
            NSDictionary *action_val = [[NSDictionary alloc] initWithObjectsAndKeys: response.actionIdentifier, (response.actionIdentifier ? @"stringValue" : @"nullValue"), nil];
    
        // Create object
            reqFields = @{
                @"my_uid": my_uid_val,
                @"action": action_val
            };
    
        // Place fields into expected reqBody format (i.e. under 'fields' property)
            NSDictionary *reqBody = [[NSDictionary alloc] initWithObjectsAndKeys:
                reqFields, @"fields", 
            nil];
    
        // Confirm...?
            NSLog(@"%@", reqBody);
    
  2. Compose the request

        // Grab current user's token (for authenticated firestore REST API call)
            [db_user getIDTokenWithCompletion:^(NSString * _Nullable token, NSError * _Nullable error) {
                if (!error && token) {
                    NSLog(@"Successfully obtained getIDTokenWithCompletion: %@", token);
    
                // Compose stringified response
                // + (per https://stackoverflow.com/a/44923210/1183749 )
                    NSError *error;
                    NSData *postData = [NSJSONSerialization dataWithJSONObject:reqBody options:kNilOptions error:&error];
                    if(!postData){
                        NSLog(@"Error creating JSON: %@", [error localizedDescription]);
                    }else{
                        NSLog(@"Successfully created JSON.");
                    }
    
                // Create the request
                    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
                    [request setURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://firestore.googleapis.com/v1beta1/projects/%@/databases/%@/documents/%@", @"project_id", @"(default)", @"collection_id"]]];
    
                    [request setHTTPMethod:@"POST"];
                    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
                    [request setValue:[NSString stringWithFormat:@"Bearer %@", token] forHTTPHeaderField:@"Authorization"]; // 'token' is returned in [[FIRAuth auth].currentUser getIDTokenWithCompletion]. (All of this code resides inside the getIDTokenWithCompletion block so we can pass it along with the request and let firebase security rules take care of authenticating the request.)
    
                    [request setHTTPBody:postData];
    
                // Set up the session configuration
                    NSURLSessionConfiguration *sessionConfig;
                    sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    
                // Set up the session
                    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
    
  3. Start the upload task

                // Start the upload task
                    NSURLSessionDataTask *uploadTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *uploadTaskResp, NSError *error) {
    
                        NSLog(@"dataTask Request reply: %@", uploadTaskResp);
                        if(error){
                            NSLog(@"dataTask Request error: %@", error);
                        }
    
                    // Call completion handler
                        completionHandler();
                    }
                }
            }
    
irfaan
  • 176
  • 1
  • 11