katzer / cordova-plugin-local-notifications

Cordova Local-Notification Plugin
Apache License 2.0
2.56k stars 1.75k forks source link

iOS Action Categories get cleared between being set and scheduling a notification #1612

Open Tawpie opened 6 years ago

Tawpie commented 6 years ago

WARNING: IF YOU IGNORE THIS TEMPLATE, WE'LL IGNORE YOUR ISSUE. YOU MUST FILL THIS IN!

Provide a general summary of the issue.

Your Environment

Expected Behavior

iOS notification has action buttons or input

Actual Behavior

Notification appears as expected but without action buttons or input. Buttons/input appear on Android (8.1 tested, buttons/input do not appear on 4.4.4 but that's not a surprise)

Steps to Reproduce

Reproduce this issue; include code to reproduce, if relevant

This is a button notification (sorry about the indenting)

// build the reminder, start with a basic no-action reminder
    let oKatzerLNPluginReminder = {
      id: new Date().getTime(),
      trigger: { at: new Date(dFireAt) }, // MUST be a date object
      text:
        sText ||
        "This is a test message scheduled to fire at " + dFireAt.toString(),
      title: sTitle || "TESTING!",
      data:
        '{"rcls":"' +
        applicationStateStore.getState().activeModule +
        '","nid":"Test"}'
    };

    // add actions as indicated
        oKatzerLNPluginReminder.actions = [
          {
            id: ksLocalNotificationActionID_buttons_yes_no_ButtonID_yes,
            title: i18n.t("Yes")
          },
          {
            id: ksLocalNotificationActionID_buttons_yes_no_ButtonID_no,
            title: i18n.t("No")
          }
        ];

// schedule it
        cordova.plugins.notification.local.schedule(
          [oKatzerLNPluginReminder]
        );

Context

nothing special, just let the notification fire and check the notification shade

Debug logs

logs don't show much...

Tawpie commented 6 years ago

ok. I got it figured out. There are two things happening:

  1. You MUST use action groups (it is not optional) to define your notifications that have actions. The example shown in the readme is incorrect. If you don't setup an action group the plugin will consider your notification to be a 'GENERAL' category and will not include the actions. Apple recommends you do this early in the app lifecycle, but there's a wrinkle
  2. Somewhere the plugin loses track of the notification categories so that when you actually schedule a notification the NotificationCategory set is blank—and iOS will use a default category for presenting the notification… no buttons or inputs

To make it work, I am adding the action groups with a function that resolves a promise when the addAction plugin function completes. Scheduling uses this promise to actually do the scheduling.

I think there is a bug in the plugin somewhere, the fact that getNotificationCategoriesWithCompletionHandler returns an empty set just before scheduling is "not right"

jsb8908 commented 6 years ago

@Tawpie can you post some code on your 'solution'?

Tawpie commented 6 years ago

Here's my code for scheduling a single notification. I always use the array method of scheduling even for single notifications, it seems to work more reliably. We also use Hungarian notation for vars, I know, but it works for us. This means that vars that start with say, 'ks' for example, are "konstant, string" so we know it's a string that isn't supposed to change and was defined in our globals file.

The promise part happens in the getTestKatzerActionGroupArray() method, I'll post that code in the next comment. What I'm doing is building a set of promises to 'add the action category' for each notification, then running them one after the other and when the last promise resolves I go ahead and cancel existing notifications and reschedule.

/**
 * an_scheduleSingleLocalNotification
 * 
 * queue up a system local notification to fire at a specific time. when dAtDate is blank
 * or bogus we'll go for 10 seconds from now.
 * 
 * @param sActions {string} // the 'type' of action, input or button or...
 * @param sTitle {string} // title string for the notification
 * @param sText {string} // body text for the notification
 * @param dAtDate {object} // date object containing the fire time
 * @param bWithoutCancelling {boolean} // then true will NOT cancel existing notifications first
 */
export function an_scheduleSingleLocalNotification(
  sActions,
  sTitle,
  sText,
  dAtDate,
  bWithoutCancelling
) {
  if (an_isLocalNotificationPluginValid()) {
    if (typeof bWithoutCancelling === "undefined") {
      bWithoutCancelling = true;
    }

    // compute a fire date and time
    let dFireAt = new Date(dAtDate);
    if (
      typeof dFireAt === "undefined" ||
      dFireAt.toString() === "Invalid Date"
    ) {
      dFireAt = dateAdd(new Date(), 10, "secs");
    }

    // build the reminder, start with a basic no-action reminder
    let oKatzerLNPluginReminder = {
      id: new Date().getTime(),
      trigger: { at: new Date(dFireAt) }, // MUST be a date object
      text:
        sText ||
        "This is a test message scheduled to fire at " + dFireAt.toString(),
      title: sTitle || "TESTING!",
      data:
        '{"rcls":"' +
        applicationStateStore.getState().activeModule +
        '","nid":"Test"}'
    };

    // add actions as indicated
    switch (sActions) {
      case ksLocalNotificationCategoryID_buttons_yes_no:
        oKatzerLNPluginReminder.actions = ksLocalNotificationCategoryID_buttons_yes_no;

        // install the listeners
        cordova.plugins.notification.local.un(
          ksLocalNotificationActionID_buttons_yes_no_ButtonID_yes,
          handleLNResponse_yes
        );
        cordova.plugins.notification.local.on(
          ksLocalNotificationActionID_buttons_yes_no_ButtonID_yes,
          handleLNResponse_yes
        );

        cordova.plugins.notification.local.un(
          ksLocalNotificationActionID_buttons_yes_no_ButtonID_no,
          handleLNResponse_no
        );
        cordova.plugins.notification.local.on(
          ksLocalNotificationActionID_buttons_yes_no_ButtonID_no,
          handleLNResponse_no
        );
        break;
      case ksLocalNotificationActionID_input:
        oKatzerLNPluginReminder.actions = ksLocalNotificationCategoryID_input;

        cordova.plugins.notification.local.un(
          ksLocalNotificationActionID_input,
          handleLNResponse_input
        );
        cordova.plugins.notification.local.on(
          ksLocalNotificationActionID_input,
          handleLNResponse_input
        );
        break;
      default:
        break;
    }

    // android can show the smallIcon in the actionbar
    if (gsRunningOnPlatform.toLowerCase() === kPlatformandroid) {
      oKatzerLNPluginReminder.smallIcon = "res://" + gsANAndroidPushIcon;
    }

    let aMultiRemindersForKatzerLNPlugin = [oKatzerLNPluginReminder];

    if (aMultiRemindersForKatzerLNPlugin.length) {
      pRunPromisesSerially(getTestKatzerActionGroupArray()).then(
        () => {
          if (!bWithoutCancelling) {
            cordova.plugins.notification.local.cancelAll(
              function scheduleAllRemindersViaKatzer() {
                // setTimeout(cordova.plugins.notification.local.cancelAll(function scheduleAllRemindersViaKatzer() {
                console.log(
                  "ANSLMH.an_sSLNFR - cancellation completed. Elapsed: " +
                  (new Date().getTime() - new Date().getTime()) / 1000 +
                  " secs"
                );
                console.log(
                  " ANSLMH.an_sSLNFR - scheduling >" +
                  aMultiRemindersForKatzerLNPlugin.length +
                  "< local reminders"
                );
                setLSInt(ksLocalMessagesActuallyScheduled, 0); // reset the count
                goScheduledLocalMessages = {}; // purge our copy

                cordova.plugins.notification.local.schedule(
                  aMultiRemindersForKatzerLNPlugin
                );
              }
            );
          } else {
            console.log(
              " ANSLMH.an_sSLNFR - scheduling >" +
              aMultiRemindersForKatzerLNPlugin.length +
              "< local reminders without cancelling"
            );

            cordova.plugins.notification.local.schedule(
              aMultiRemindersForKatzerLNPlugin
            );
          }
        },
        () => {
          console.error(
            "ANSLMH.an_sSLNFR - Can't schedule local notifications, the local notification plugin is invalid"
          );
        }
      );
    } else {
      console.error(
        "ANSLMH.an_sSLNFR - Can't schedule local notifications, the local notification plugin is invalid"
      );
    }
  }
}
Tawpie commented 6 years ago
/**
 * getTestKatzerActionGroupArray
 *
 * returns an array of promises that are resolved when the katzer plugin
 * finishes adding an action category. DO NOT LET THESE RUN IN PARALLEL, they'll
 * overwrite each other.
 *
 * @returns {*[]} // an array of test promises
 */
export function getTestKatzerActionGroupArray() {
  let aActionGroups = [];
  aActionGroups.push(() => {
    new Promise((resolve, reject) => {
      if (an_isLocalNotificationPluginValid()) {
        console.log("ANSLMH.aKAG - adding button category has begun");
        cordova.plugins.notification.local.addActions(
          ksLocalNotificationCategoryID_buttons_yes_no,
          [
            {
              id: ksLocalNotificationActionID_buttons_yes_no_ButtonID_yes,
              title: i18n.t("Yes")
            },
            {
              id: ksLocalNotificationActionID_buttons_yes_no_ButtonID_no,
              title: i18n.t("No")
            }
          ],
          () => {
            console.log("ANSLMH.aKAG - adding button category has completed");
            resolve("\nANSLMH.aKAG - did set buttons action group");
          }
        );
      } else {
        console.log(
          "ANSLMH.aKAG - could not add the notification action groups, no plugin"
        );
        reject(
          "ANSLMH.aKAG - FAILED to set action groups, plugin is not valid"
        );
      }
    });
  });

  aActionGroups.push(() => {
    new Promise((resolve, reject) => {
      if (an_isLocalNotificationPluginValid()) {
        console.log("ANSLMH.aKAG - adding input category has begun");
        cordova.plugins.notification.local.addActions(
          ksLocalNotificationCategoryID_input,
          [
            {
              id: ksLocalNotificationActionID_input,
              type: "input",
              title: i18n.t("Reply"),
              emptyText: i18n.t("Type message")
            }
          ],
          () => {
            console.log("ANSLMH.aKAG - adding input category has completed");
            resolve("\nANSLMH.aKAG - did set input action group");
          }
        );
      } else {
        console.log(
          "ANSLMH.aKAG - could not add the notification action groups, no plugin"
        );
        reject(
          "ANSLMH.aKAG - FAILED to set action groups, plugin is not valid"
        );
      }
    });
  });

  return aActionGroups.reverse();
}