From 56c5432a10d995b62303a4c2eef69be7fcbe7470 Mon Sep 17 00:00:00 2001 From: Anne van Kesteren Date: Fri, 12 Jul 2024 17:03:50 +0200 Subject: [PATCH] Add Declarative Web Push This introduces a new feature whereby push messages conforming to a certain JSON format directly create an end user notification and show it (possibly preceded by an enhanced push event). In addition to showing a notification, the app badge can be updated as well. This builds on https://github.com/whatwg/notifications/pull/213 which adds URL members to notifications. Exposing PushManager outside of service workers is handled by #393. --- index.html | 842 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 790 insertions(+), 52 deletions(-) diff --git a/index.html b/index.html index 295228c..3cc7aa0 100644 --- a/index.html +++ b/index.html @@ -56,7 +56,8 @@ }; - +

The Push API enables sending of a push message to a web application via @@ -164,6 +165,555 @@

service worker is not currently running, the worker is started to enable delivery.

+
+

+ Declarative push message +

+

+ A declarative push message is a [=push message=] whose data is a JSON document + that is understood by the user agent. A user agent opportunistically parses each incoming + [=push message=] to determine if it is a [=declarative push message=] using the + [=declarative push message parser=]. +

+

+ A [=declarative push message=] allows for the creation and display of a notification + without the involvement of a service worker. Nevertheless, a service worker can still be + involved if desired by the [=application server=]. In such a scenario the declarative + nature of the [=push message=] serves as a backup in case the service worker was evicted + due to storage pressure, for instance. And also provides a more object-oriented approach + to transmitting notification data. +

+
+          {
+            "web_push": 8030,
+            "notification": {
+              "title": "Watch the new season of Return of the Example now!",
+              "lang": "en-US",
+              "dir": "ltr",
+              "body": "The examples are going to great lengths once more.",
+              "url": "https://tv.example/return"
+            }
+          }
+        
+
+

+ Members +

+

+ A [=declarative push message=] has the following members: +

+
+
+ web_push (required) +
+
+

+ An integer that must be 8030. Used to disambiguate a [=declarative push message=] + from other JSON documents. +

+
+
+ notification (required) +
+
+

+ A JSON object consisting of the following members, all analogous to Notifications + API features, though sometimes with a slightly stricter type. Apart from + title all members are derived from the {{NotificationOptions}} + dictionary and to be maintained in tandem. [[NOTIFICATIONS]] +

+
+
+ title (required) +
+
+

+ A string. +

+
+
+ dir +
+
+

+ "auto", "ltr", or "rtl". +

+
+
+ lang +
+
+

+ A string that holds a language tag. +

+
+
+ body +
+
+

+ A string. +

+
+
+ navigate (required) +
+
+

+ A string that holds a URL. +

+
+
+ tag +
+
+

+ A string. +

+
+
+ image +
+
+

+ A string that holds a URL. +

+
+
+ icon +
+
+

+ A string that holds a URL. +

+
+
+ badge +
+
+

+ A string that holds a URL. +

+
+
+ vibrate +
+
+

+ An array of [=/32-bit unsigned integers=]. +

+
+
+ timestamp +
+
+

+ A [=/64-bit unsigned integer=]. +

+
+
+ renotify +
+
+

+ A boolean. +

+
+
+ silent +
+
+

+ A boolean. +

+
+
+ require_interaction +
+
+

+ A boolean. +

+
+
+ data +
+
+

+ Any JSON value. +

+
+
+ actions +
+
+

+ An array of JSON objects consisting of the following members, all derived from + the {{NotificationAction}} dictionary and to be maintained in tandem. +

+
+
+ action (required) +
+
+

+ A string. +

+
+
+ title (required) +
+
+

+ A string. +

+
+
+ navigate (required) +
+
+

+ A string that holds a URL. +

+
+
+ icon +
+
+

+ A string that holds a URL. +

+
+
+
+
+
+
+ app_badge +
+
+

+ A [=/64-bit unsigned integer=]. +

+

+ Platform conventions are likely to impose a lower limit with regards to what is + displayed to the end user. [[BADGING]] +

+
+
+ mutable +
+
+

+ A boolean. When true causes a push event to be dispatched to a service + worker (if any) containing the {{Notification}} object and app badge number (if + any) described by the declarative push message. +

+
+
+
+
+

+ Parser +

+

+ A declarative push message parser result is a [=/tuple=] consisting of a + notification (a + [=/notification=]), an app + badge (null or an integer), and a mutable (a boolean). +

+

+ The declarative push message parser given a [=/byte sequence=] + bytes, [=/origin=] origin, [=/URL=] baseURL, and + {{EpochTimeStamp}} fallbackTimestamp runs these steps. They return failure + or a [=/declarative push message parser result=]. +

+
    +
  1. +

    + Let message be the result of [=parse JSON bytes to an Infra + value|parsing JSON bytes to an Infra value=] given bytes. If that throws + an exception, then return failure. +

    +
  2. +
  3. +

    + If message is not a [=/map=], then return failure. +

    +
  4. +
  5. +

    + If message["`web_push`"] does not [=map/exist=] or is not 8030, then + return failure. +

    +
  6. +
  7. +

    + If message["`notification`"] does not [=map/exist=], then return + failure. +

    +
  8. +
  9. +

    + Let notificationInput be message["`notification`"]. +

    +
  10. +
  11. +

    + If notificationInput is not a [=/map=], then return failure. +

    +
  12. +
  13. +

    + If notificationInput["`title`"] does not [=map/exist=] or is not a + string, then return failure. +

    +
  14. +
  15. +

    + If notificationInput["`navigate`"] does not [=map/exist=] or is not a + string, then return failure. +

    +
  16. + +
  17. +

    + Let notificationTitle be notificationInput["`title`"]. +

    +
  18. +
  19. +

    + Let notificationOptions be a {{NotificationOptions}} dictionary. +

    +
  20. +
  21. +

    + If notificationInput["`dir`"] [=map/exists=] and is "`auto`", "`ltr`", + or "`rtl`", then set notificationOptions["{{NotificationOptions/dir}}"] + to notificationInput["`dir`"]. +

    +
  22. +
  23. +

    + If notificationInput["`lang`"] [=map/exists=] and is a string, then set + notificationOptions["{{NotificationOptions/lang}}"] to + notificationInput["`lang`"]. +

    +
  24. +
  25. +

    + If notificationInput["`body`"] [=map/exists=] and is a string, then set + notificationOptions["{{NotificationOptions/body}}"] to + notificationInput["`body`"]. +

    +
  26. +
  27. +

    + Set notificationOptions["{{NotificationOptions/navigate}}"] to + notificationInput["`navigate`"], [=string/converted=]. +

    +
  28. +
  29. +

    + If notificationInput["`tag`"] [=map/exists=] and is a string, then set + notificationOptions["{{NotificationOptions/tag}}"] to + notificationInput["`tag`"]. +

    +
  30. +
  31. +

    + If notificationInput["`image`"] [=map/exists=] and is a string, then set + notificationOptions["{{NotificationOptions/image}}"] to + notificationInput["`image`"], [=string/converted=]. +

    +
  32. +
  33. +

    + If notificationInput["`icon`"] [=map/exists=] and is a string, then set + notificationOptions["{{NotificationOptions/icon}}"] to + notificationInput["`icon`"], [=string/converted=]. +

    +
  34. +
  35. +

    + If notificationInput["`badge`"] [=map/exists=] and is a string, then set + notificationOptions["{{NotificationOptions/badge}}"] to + notificationInput["`badge`"], [=string/converted=]. +

    +
  36. +
  37. +

    + If notificationInput["`vibrate`"] [=map/exists=] and is a [=/list=] of + which each [=list/item=] is a [=/32-bit unsigned integer=], then set + notificationOptions["{{NotificationOptions/vibrate}}"] to + notificationInput["`vibrate`"]. +

    +
  38. +
  39. +

    + If notificationInput["`timestamp`"] [=map/exists=] and is a [=/64-bit + unsigned integer=], then set + notificationOptions["{{NotificationOptions/timestamp}}"] to + notificationInput["`timestamp`"]. +

    +
  40. +
  41. +

    + If notificationInput["`renotify`"] [=map/exists=] and is a boolean, then + set notificationOptions["{{NotificationOptions/renotify}}"] to + notificationInput["`renotify`"]. +

    +
  42. +
  43. +

    + If notificationInput["`silent`"] [=map/exists=] and is a boolean, then + set notificationOptions["{{NotificationOptions/silent}}"] to + notificationInput["`silent`"]. +

    +
  44. +
  45. +

    + If notificationInput["`require_interaction`"] [=map/exists=] and is a + boolean, then set + notificationOptions["{{NotificationOptions/requireInteraction}}"] to + notificationInput["`require_interaction`"]. +

    +
  46. +
  47. +

    + If notificationInput["`data`"] [=map/exists=], then set + notificationOptions["{{NotificationOptions/data}}"] to the result of + running convert an Infra value to a JSON-compatible JavaScript value given + notificationInput["`data`"]. +

    +
  48. +
  49. +

    + If notificationInput["`actions`"] [=map/exists=] and is a [=/list=]: +

    +
      +
    1. +

      + Let notificationActions be « ». +

      +
    2. +
    3. +

      + [=list/For each=] actionInput of + notificationInput["`actions`"]: +

      +
        +
      1. +

        + If actionInput["`action`"] does not [=map/exist=] or is not a + string, then [=iteration/continue=]. +

        +
      2. +
      3. +

        + If actionInput["`title`"] does not [=map/exist=] or is not a + string, then [=iteration/continue=]. +

        +
      4. +
      5. +

        + If actionInput["`navigate`"] does not [=map/exist=] or is not a + string, then [=iteration/continue=]. +

        +
      6. + +
      7. +

        + Let actionNavigate be actionInput["`navigate`"], + [=string/converted=]. +

        +
      8. +
      9. +

        + Let notificationAction be the {{NotificationAction}} dictionary + «[ "{{NotificationAction/action}}" → actionInput["`action`"], + "{{NotificationAction/title}}" → actionInput["`title`"], + "{{NotificationAction/navigate}}" → actionNavigate ]». +

        +
      10. + +
      11. +

        + If actionInput["`icon`"] [=map/exists=] and is a string, then + set notificationAction["{{NotificationAction/icon}}"] to + actionInput["`icon`"], [=string/converted=]. +

        +
      12. +
      13. +

        + [=list/Append=] notificationAction to + notificationActions. +

        +
      14. +
      +
    4. +
    5. +

      + Set notificationOptions["{{NotificationOptions/actions}}"] to + notificationActions. +

      +
    6. +
    +
  50. +
  51. +

    + Let notification be the result of creating a notification given + notificationTitle, notificationOptions, origin, + baseURL, and fallbackTimestamp. If this throws an exception, + then return failure. +

    +
  52. +
  53. +

    + If notification's [=notification/navigation URL=] is null, then return + failure. +

    +
  54. +
  55. +

    + If the [=notification action/navigation URL=] of any [=/notification action=] of + notification's [=notification/actions=] is null, then return failure. +

    +
  56. +
  57. +

    + Let appBadge be null. +

    +
  58. +
  59. +

    + If message["`app_badge`"] [=map/exists=] and + message["`app_badge`"] is a [=/64-bit unsigned integer=], then set + appBadge to message["`app_badge`"]. +

    +
  60. +
  61. +

    + Let mutable be false. +

    +
  62. +
  63. +

    + If message["`mutable`"] [=map/exists=] and + message["`mutable`"] is a boolean, then set mutable to + message["`mutable`"]. +

    +
  64. +
  65. +

    + Return (notification, appBadge, mutable). +

    +
  66. +
+
+

Push subscription @@ -978,8 +1528,8 @@

};

- {{PushMessageData}} objects have an associated bytes (a [=byte sequence=]), - which is set on creation. + {{PushMessageData}} objects have an associated bytes (a [=byte sequence=]), which is set on creation.

The arrayBuffer() method steps are to return an {{ArrayBuffer}} whose contents @@ -1063,12 +1613,22 @@

PushEvent Interface

-
+        
             [Exposed=ServiceWorker, SecureContext]
             interface PushEvent : ExtendableEvent {
               constructor(DOMString type, optional PushEventInit eventInitDict = {});
               readonly attribute PushMessageData? data;
+              readonly attribute Notification? notification;
+              readonly attribute unsigned long long? appBadge;
             };
+
+            dictionary PushEventInit : ExtendableEventInit {
+              PushMessageDataInit? data = null;
+              Notification? notification = null;
+              unsigned long long? appBadge = null;
+            };
+
+            typedef (BufferSource or USVString) PushMessageDataInit;
           

When a constructor of the PushEvent interface, or of an interface that @@ -1082,29 +1642,18 @@

  • Set |b| to the result of extracting a byte sequence from the "`data`" member of |eventInitDict|.
  • -
  • Set the `data` attribute of the event to a new PushMessageData instance with - `bytes` set to |b|. +
  • Set the `data` attribute of the event to a new {{PushMessageData}} instance whose + [=PushMessageData/bytes=] is |b|.
  • - The data, when getting, returns the value it was initialized with. + The data attribute must return the value it was initialized with. +

    +

    + The notification attribute must return the value it was initialized with.

    -

    -
    -

    - PushEventInit dictionary -

    -
    -            typedef (BufferSource or USVString) PushMessageDataInit;
    -
    -            dictionary PushEventInit : ExtendableEventInit {
    -              PushMessageDataInit data;
    -            };
    -          

    - The data member contains the data included in the push message when - included and the user agent verified its authenticity. The value will be set to - `null` in all other cases. + The appBadge attribute must return the value it was initialized with.

    @@ -1127,28 +1676,169 @@

  • If the push message contains a payload:
      -
    1. Decrypt the push message payload using the private key from the key pair +
    2. Decrypt the push message's payload using the private key from the key pair associated with |subscription| and the process described in [[RFC8291]]. Set |bytes| to the resulting [=/byte sequence=].
    3. -
    4. If the push message payload could not be decrypted for any reason: +
    5. +

      + If the push message payload could not be decrypted for any reason, then + [=acknowledge a push message|acknowledge=] the push message and abort + these steps. +

      +

      + A `push` event is not fired for a push message that was not successfully + decrypted using the key pair associated with the push subscription. +

      +
    6. +
    +
  • +
  • +

    + If |bytes| is non-null: +

    +
      +
    1. +

      + Let |baseURL| be |registration|'s [=service worker registration/scope URL=]. +

      +
    2. +
    3. +

      + Let |origin| be |baseURL|'s [=url/origin=]. +

      +
    4. +
    5. +

      + Let |fallbackTimestamp| be [=current coarsened wall time=]. +

      +
    6. +
    7. +

      + Let |declarativeResult| be the result of running the [=/declarative push message + parser=] given |bytes|, |origin|, |baseURL|, and |fallbackTimestamp|. +

      +
    8. +
    9. +

      + If |declarativeResult| is not failure: +

        -
      1. Acknowledge the receipt of the push message according to [[RFC8030]]. - Though the message was not successfully received and processed, this prevents the - push service from attempting to retransmit the message; a badly encrypted message - is not recoverable. +
      2. +

        + Let |notification| be |declarativeResult|'s [=declarative push message parser + result/notification=]. +

        +
      3. +
      4. +

        + Set |notification|'s [=notification/service worker registration=] to + |registration|. +

      5. -
      6. Abort these steps. +
      7. +

        + Let |notificationShown| be false. +

        +
      8. +
      9. +

        + Let |appBadgeSet| be false. +

        +
      10. +
      11. +

        + If |declarativeResult|'s [=declarative push message parser result/mutable=] + is true: +

        +
          +
        1. +

          + Let |result| be the result of [=fire a push event|firing a push event=] + given |registration|, null, a new {{Notification}} object representing + |notification|, and |declarativeResult|'s [=declarative push message + parser result/app badge=]. +

          +
        2. +
        3. +

          + If |result| is not failure, then set |notificationShown| to |result|'s + [=push event result/notification shown=] and |appBadgeSet| to |result|'s + [=push event result/app badge set=]. +

          +
        4. +
        +
      12. +
      13. +

        + If |notificationShown| is false, then run the [=notification show steps=] + given |notification|. +

        +
      14. +
      15. +

        + If |appBadgeSet| is false, then w3c/badging #111... +

        +
      16. +
      17. +

        + [=acknowledge a push message|Acknowledge=] the push message and abort + these steps. +

      -

      - A `push` event will not be fired for a push message that was not - successfully decrypted using the key pair associated with the push - subscription. -

  • +
  • +

    + Let |data| be a new {{PushMessageData}} object whose [=PushMessageData/bytes=] is + |bytes| if |bytes| is non-null; otherwise null. +

    +
  • +
  • +

    + Let |result| be the result of [=fire a push event|firing a push event=] given + |registration|, |data|, null, and null. +

    +
  • +
  • +

    + If result is failure and the same push message has been delivered + to a service worker registration multiple times unsuccessfully, then + [=acknowledge a push message|acknowledge=] the push message. +

    +
  • +
  • +

    + If result is not failure, then [=acknowledge a push message|acknowledge=] + the push message. +

    +
  • + +

    + A push event result is a [=/tuple=] consisting of a notification shown (a [=/boolean=]) and a app badge set (a [=/boolean=]). +

    +

    + To fire a push event given a [=/service worker registration=] |registration|, + {{PushMessageData}} object or null |data|, a [=/notification=] or null |notification|, + and an integer or null |appBadge|, run these steps. They return failure or a [=/push + event result=]. +

    +
      +
    1. +

      + Let |notificationResult| be null. +

      +
    2. +
    3. +

      + Let |appBadgeResult| be null. +

      +
    4. Fire a functional event named "`push`" using PushEvent on @@ -1156,44 +1846,92 @@

      - `data` + {{PushEvent//data}}
      - A new {{PushMessageData}} object whose [=bytes=] is |bytes|. + |data| +
      +
      + {{PushEvent/notification}} +
      +
      + |notification| +
      +
      + {{PushEvent/appBadge}} +
      +
      + |appBadge|

      Then run the following steps in parallel, with |dispatchedEvent|:

        -
      1. Wait for all of the promises in the [=ExtendableEvent/extend lifetime promises=] - of |dispatchedEvent| to resolve. -
      2. -
      3. If all the promises resolve successfully, acknowledge the receipt of the push - message according to [[RFC8030]] and abort these steps. +
      4. +

        + Wait for all of the promises in the [=ExtendableEvent/extend lifetime promises=] + of |dispatchedEvent| to resolve. +

      5. - If the same push message has been delivered to a service worker - registration multiple times unsuccessfully, acknowledge the receipt of the - push message according to [[RFC8030]]. + If they do not resolve successfully, then set |notificationResult| and + |appBadgeResult| to failure and abort these steps.

        +
      6. +
      7. - Acknowledging the push message causes the push service to stop - delivering the message and to report success to the application server. - This prevents the same push message from being retried by the push - service indefinitely. + Set |notificationResult| to true if + {{ServiceWorkerRegistration/showNotification()}} has been invoked; otherwise + false.

        +
      8. +
      9. - Acknowledging also means that an application server could incorrectly - receive a delivery receipt indicating successful delivery of the push - message. Therefore, multiple rejections SHOULD be permitted before - acknowledging; allowing at least three attempts is recommended. + Set |appBadgeResult| to true if {{NavigatorBadge/setAppBadge()}} has been + invoked; otherwise false.

    5. +
    6. +

      + Wait for |notificationResult| and |appBadgeResult| to be non-null. +

      +
    7. +
    8. +

      + If |notificationResult| is failure, then return failure. +

      +
    9. +
    10. +

      + [=/Assert=]: |notificationResult| and |appBadgeResult| are [=/booleans=]. +

      +
    11. +
    12. +

      + Return (|notificationResult|, |appBadgeResult|). +

      +
    +

    + To acknowledge a push message given a push message + pushMessage means to acknowledge the receipt of pushMessage + according to [[RFC8030]]. +

    +

    + Acknowledging the push message causes the push service to stop delivering + the message and to report success to the application server. This prevents the + same push message from being retried by the push service indefinitely. +

    +

    + Acknowledging also means that an application server could incorrectly receive a + delivery receipt indicating successful delivery of the push message. Therefore, + multiple rejections SHOULD be permitted before acknowledging; allowing at least three + attempts is recommended. +