2
votes

First of all, just to clarify...

I have done quite some searchings and readings on the existing sources (medium, stackoverflow, apple developer forum, etc.) before asking this "duplicate question" for confirming and concluding my knowledge is correct.

  1. Will iOS launch my app into the background if it was force-quit by the user?
  2. How to get the push notification payload when force-quit / swipe up to kill the iOS app without tapping on the banner/alert?
  3. https://medium.com/fenrir-inc/handling-ios-push-notifications-the-not-so-apparent-side-420891ddf10b
  4. Handling Push Notifications when App is Terminated
  5. https://developer.apple.com/forums/thread/62005#:~:text=In%20most%20cases%2C%20the%20system,force%20quit%20by%20the%20user.

Before we begin, just to put the terms in very precise manner. Here's what I'm referring when I mention

  1. Foreground - App is Active and running, user is basically interacting with the application
  2. Background - User tapped on home button after interacting. App remains in background, user can double tap on home button and find the app from App Switcher.
  3. Quit - App is actually in background, but it was terminated by the System itself.
  4. Kill - App is no longer in background, user double tap on home button and SWIPE the app away from App Switcher.

Use case i'm trying to tackle

App receive push notification in BACKGROUND, QUIT & KILL states then perform certain background actions (updating Application Badge + storing the Notification in device)

  1. For background - Yes I've managed to achieve this by having content-available = 1 sent together in APNS payload. Notification banner appear, background action executed!
  2. For QUIT - Yes I've managed to achieve this by having content-available = 1 sent together in APNS payload. Notification banner appear, background action executed!
  3. For KILL - Notification banner appear, BUT background action is not trigger.

My questions

  1. Whenever app is kill, it's not possible to awake the app to perform any background action when notification is receive?
  2. How does it works for messaging app like Whatsapp?
  3. How should I handle my cases when app is force quit?
  • ONLY if user tapped notification banner, my app gets to run those background actions (Increase badge count + storing the data).

  • Otherwise, if user choose to tap on App Icon to open my app. The pushed notification won't exist in my app at all, including badge count is not increase.

Below are what I've tested with Whatsapp

Background State

  1. Open Whatsapp, tap Home button (keep app in background)
  2. Send a text message in device, banner notification appear
  3. Open app by tapping app icon, message is there in app.

FORCE QUIT State

  1. Open Whatsapp, double tap Home button, swipe app away
  2. Send a text message in device, banner notification appear.
  3. Open app by tapping app icon, message is there in app.

FORCE QUIT State + WiFi & Cellular data turned OFF

  1. Open Whatsapp, double tap Home button, swipe app away
  2. Send a text message in device, banner notification appear
  3. Turn off WiFi & Cellular data is off. (Confirm and tried to access website via Safari)
  4. Open app by tapping app icon, message is there in app.

The tests with Whatsapp, basically concludes that it's possible to have your app awake to perform background actions (Especially with the case of FORCE QUIT State + WiFi & Cellular data turned OFF)

The only "explanation" I'm able to explain myself is, they are using PushKit notifications Framework instead of User Notification Framework.

Update 3 Nov - iOS 13 wakes app even in KILL state

Apparently on iOS 13, like what @hubsi has mentioned down at the comment as well as some comments from Apple forum. iOS 13 does wakes my app even the app was manually killed by the user.

2

2 Answers

2
votes

Update swift 5.x Plus iOS 14.x

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.deplinkNotification(userInfo: response.notification.request.content.userInfo )
               }
}

Update: In addition to what's mentioned below, it seems that as of iOS 13 apps get actually launched when a push notification is received in killed state. I found that in order to have the didReceiveRemoteNotification:fetchCompletionHandler delegate to trigger, the app needs to launch the registerForRemoteNotifications (UIApplication) method asap when launched (e.g. in didFinishLaunchingWithOptions). In case of React Native, opposed to what's stated below, you can actually run JS code. Though you have to wait for RN to have launched before running code, e.g. like this:

    public class func didReceiveRemoteNotification(_ userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            // some RN bridged method/event
        }
    }

Old Answer:

There are two ways to have code run when a push notifications is received on iOS with the app being killed:

  1. by using the PushKit framework
    note: Apple allows the PushKit framework only to be used for VoIP and watchOS applications, so using it without actually having that requirement your app will probably not make it through Apple's review
  2. by using a Notification Service Extension
    Using this extension enables you to run (headless) code in background, including "enriching" the notification, e.g. with an image attachment

As for WhatsApp I'm pretty sure they're using PushKit (they require it anyway for incoming calls). Though I think what you described in your last test would also be possible using a Notification Service Extension:
In the extension you can process the pushed notification including its payload and write data (e.g. the chat message) to your local app storage (e.g. shared with your main app through App Groups). When the app gets opened it reads that data from storage (chat message appears without data network).

Since the question is tagged with "react-native": Please note that to my knowledge it's not possible to invoke RN bridged JavaScript code in a Notification Service Extension. You'd have to write native (Swift/Obj-C) code.

0
votes

This is working for us for both iOS and Android even when the app is explicitly killed by the user.

Using-

  1. https://github.com/zo0r/react-native-push-notification
  2. https://github.com/react-native-push-notification-ios/push-notification-ios (do the complete setup required for these two packages)

APN Request

$url = "https://api.sandbox.push.apple.com/3/device/<device_token>";
$headers = array(
    "apns-push-type: voip",
    "apns-expiration: 10",
    "apns-topic: com.example.app.voip", // .voip as suffix to bundleID
    "apns-collapse-id: lcall", 
    "Content-Type: application/x-www-form-urlencoded",
);
$certificate_file = config('pushnotification.apn.certificate');
$payloadArray['aps'] = [
    'alert' => [
        'title' => "Calling title",
        'body' => "Calling body",
    ],
    'badge' => 1,
    "content-available" => 1
];
$data = json_encode($payloadArray);
$client = new Client();
$response = $client->post($url, [
    'headers' => $headers,
    'cert' => $certificate_file,
    'curl' => [
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
    ],
    'body'=> $data,
]);

Read official for apns-push-type- https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns

FCM Request

$url = "https://fcm.googleapis.com/fcm/send";
$headers = array(
    'Authorization' => 'key=<fcm_server_key',
    'Content-Type' => 'application/json'
);
$payloadArray = [
    "to" => "<device_token>",
    "data" => [
        "title"=> "Calling Title",
        "body" => "Calling Body",
    ]
];
$data = json_encode($payloadArray);
$client = new Client();
$response = $client->post($url, [
    'headers' => $headers,
    'curl' => [
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    ],
    'body'=> $data,
]);