iosswiftpush-notificationunusernotificationcenter

IOS User Notification not displayed to user


I'm trying to understand actionable notifications (Notifications with text and button(s) - thus allowing user to interact with the notification with more than just a tap). I'm following this documentation.

First, requesting user permission,

// Log() is my custom logging wrapper.

// Called from application(_:willFinishLaunchingWithOptions:).
func RequestUserPermission() {
       
    AppDelegate.sUserNotificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { (pGranted: Bool, pError: Error?) in
            
        Log("Completion handler of requestAuthorization(options:completionHandler:) invoked!")
            
        if(pGranted) {
            Log("Permission granted!")
        } else {
            Log("Permission denied!")
            guard let error = pError else {
                Log("nil value in pError!")
                return;
            }
            Log(String(format: "error = %@", error.localizedDescription))
        }
    }
}

Then, registering for remote notification and to obtain a device token,

// static let sApp: UIApplication = UIApplication.shared
// static let sUserNotificationCenter: UNUserNotificationCenter = UNUserNotificationCenter.current()

// Called from application(_:willFinishLaunchingWithOptions:).
func RegisterForRemoteNotifications() {
    
    AppDelegate.sApp.registerForRemoteNotifications()
        
    // Set the app delegate to be notified.
    AppDelegate.sUserNotificationCenter.delegate = self
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        
    Log(String(format: "error.localizedDescription = %@", error.localizedDescription))
}
    
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken pDeviceToken: Data) {
        
    // Print the device token as a string.
    Log("pDeviceToken = " + pDeviceToken.hexEncodedString())
}

extension Data {
    
    func hexEncodedString() -> String {
        return map { String(format: "%02hhx", $0) }.joined()
    }
}

Then, the notification categories supported by the app needs to be registered. I'm only looking at one meeting notification with 3 buttons.

// static let sAcceptActionID: String = "ACCEPT_ACTION"
// static let sTentativeActionID: String = "TENTATIVE_ACTION"
// static let sDeclineActionID: String = "DECLINE_ACTION"
// static let sMeetingInviteID: String = "MEETING_INVITE"

func RegisterNotificationCategories() {
        
    // Specify the actions (buttons) to a notification
    let meeting_accept: UNNotificationAction = UNNotificationAction(identifier: AppDelegate.sAcceptActionID, title: "ACTION")
    let meeting_tentative: UNNotificationAction = UNNotificationAction(identifier: AppDelegate.sTentativeActionID, title: "TENTATIVE")
    let meeting_decline: UNNotificationAction = UNNotificationAction(identifier: AppDelegate.sDeclineActionID, title: "DECLINE")
        
    // Create the notification category object
    let meeting_invite: UNNotificationCategory = UNNotificationCategory(identifier: AppDelegate.sMeetingInviteID, actions: [meeting_accept, meeting_tentative, meeting_decline], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: "Preview")
        
    // Register the notification category
    AppDelegate.sUserNotificationCenter.setNotificationCategories([meeting_invite])
}

Next, to handle the notification by implementing the userNotificationCenter(_:didReceive:withCompletionHandler:) delegate method.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive pResponse: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        
    let user_info: [AnyHashable : Any] = pResponse.notification.request.content.userInfo
    Log(String(format: "user_info = %@", String(describing: user_info)))
       
    completionHandler()
}

This method will be invoked after user has tapped a button in the notification. App is placed in background state to process user's action.

Finally, to send the remote notification, I'm doing it the command line way (Token based approach). But it never works. The test notification, which uses the following command works

curl -v --header "apns-topic: $TOPIC" --header "apns-push-type: alert" --header "authorization: bearer $AUTHENTICATION_TOKEN" --data '{"aps":{"alert":"test"}}' --http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}

But the modified one for showing an actionable notification doesn't work. There's no notification (even though I did receive the sample test notification). This is my JSON payload,

{
   "aps" : {
      "category" : "MEETING_INVITE"
      "alert" : {
         "title" : "Meeting Invite"
         "body" : "Weekly Drag"
      },
   },
   "MEETING_ORGANIZER" : "El Diablo"
}

The modified curl command (The above JSON payload is given to the data header of the POST request as a single string) is,

curl -v --header "apns-topic: $TOPIC" --header "apns-push-type: alert" --header "authorization: bearer $AUTHENTICATION_TOKEN" --data '{"aps" : {"category" : "MEETING_INVITE""alert" : {"title" : "Meeting Invite""body" : "Weekly Drag"},},"MEETING_ORGANIZER" : "El Diablo"}' --http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}

I do get HTTP 200 in the terminal after the command (meaning it reached APNS), but no notification in my phone.

% curl -v --header "apns-topic: $TOPIC" --header "apns-push-type: alert" --header "authorization: bearer $AUTHENTICATION_TOKEN" --data '{"aps" : {"category" : "MEETING_INVITE""alert" : {"title" : "Meeting Invite""body" : "Weekly Drag"},},"MEETING_ORGANIZER" : "El Diablo"}' --http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}
....
....
* We are completely uploaded and fine
* Connection state changed (MAX_CONCURRENT_STREAMS == 1)!
* Connection state changed (MAX_CONCURRENT_STREAMS == 1000)!
< HTTP/2 200 
< apns-id: EB0400F7-3CC1-64EF-5DD6-F8158E44BC1D
< 
* Connection #0 to host api.sandbox.push.apple.com left intact

I have verified that the device token is correct and the category key in aps dictionary has the right value i.e MEETING_INVITE.... as registered in code.

What am I missing?

Environment: Xcode 14.2 and iPhone running iOS 16.0.


Solution

  • Your json seems to be malformed. Try this:

    {
        "aps": {
            "category": "MEETING_INVITE",
            "alert": {
                "title": "Meeting Invite",
                "body": "Weekly Drag"
            }
        },
        "MEETING_ORGANIZER": "El Diablo"
    }
    

    Note the commas at the end of lines.

    JSON has some unintuitive rules for commas. When in doubt, use a JSON validation tool, such as https://jsonlint.com