Skip to content

Commit ab234c9

Browse files
authored
feat: Add the ability to manage channels and send broadcasts (#164)
1 parent 2554634 commit ab234c9

16 files changed

+3946
-354
lines changed

README.md

+119-20
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ A Node.js module for interfacing with the Apple Push Notification service.
2121
- [Connecting through an HTTP proxy](#connecting-through-an-http-proxy)
2222
- [Using a pool of http/2 connections](#using-a-pool-of-http2-connections)
2323
- [Sending a notification](#sending-a-notification)
24+
- [Managing channels](#manage-channels)
25+
- [Sending a broadcast notification](#sending-a-broadcast-notification)
2426

2527
# Features
2628

@@ -36,7 +38,7 @@ $ npm install @parse/node-apn --save
3638

3739
# Quick Start
3840

39-
This readme is a brief introduction, please refer to the full [documentation](doc/apn.markdown) in `doc/` for more details.
41+
This readme is a brief introduction; please refer to the full [documentation](doc/apn.markdown) in `doc/` for more details.
4042

4143
If you have previously used v1.x and wish to learn more about what's changed in v2.0, please see [What's New](doc/whats-new.markdown)
4244

@@ -59,15 +61,20 @@ var options = {
5961
production: false
6062
};
6163

62-
var apnProvider = new apn.Provider(options);
64+
const apnProvider = new apn.Provider(options);
6365
```
6466

6567
By default, the provider will connect to the sandbox unless the environment variable `NODE_ENV=production` is set.
6668

67-
For more information about configuration options consult the [provider documentation](doc/provider.markdown).
69+
For more information about configuration options, consult the [provider documentation](doc/provider.markdown).
6870

6971
Help with preparing the key and certificate files for connection can be found in the [wiki][certificateWiki]
7072

73+
> [!WARNING]
74+
> You should only create one `Provider` per-process for each certificate/key pair you have. You do not need to create a new `Provider` for each notification. If you are only sending notifications to one app, there is no need for more than one `Provider`.
75+
>
76+
> If you are constantly creating `Provider` instances in your app, make sure to call `Provider.shutdown()` when you are done with each provider to release its resources and memory.
77+
7178
### Connecting through an HTTP proxy
7279

7380
If you need to connect through an HTTP proxy, you simply need to provide the `proxy: {host, port}` option when creating the provider. For example:
@@ -86,7 +93,7 @@ var options = {
8693
production: false
8794
};
8895

89-
var apnProvider = new apn.Provider(options);
96+
const apnProvider = new apn.Provider(options);
9097
```
9198

9299
The provider will first send an HTTP CONNECT request to the specified proxy in order to establish an HTTP tunnel. Once established, it will create a new secure connection to the Apple Push Notification provider API through the tunnel.
@@ -111,11 +118,11 @@ var options = {
111118
production: false
112119
};
113120

114-
var apnProvider = new apn.MultiProvider(options);
121+
const apnProvider = new apn.MultiProvider(options);
115122
```
116123

117124
## Sending a notification
118-
To send a notification you will first need a device token from your app as a string
125+
To send a notification, you will first need a device token from your app as a string.
119126

120127
```javascript
121128
let deviceToken = "a9d0ed10e9cfd022a61cb08753f49c5a0b0dfb383697bf9f9d750a1003da19c7"
@@ -124,7 +131,7 @@ let deviceToken = "a9d0ed10e9cfd022a61cb08753f49c5a0b0dfb383697bf9f9d750a1003da1
124131
Create a notification object, configuring it with the relevant parameters (See the [notification documentation](doc/notification.markdown) for more details.)
125132

126133
```javascript
127-
var note = new apn.Notification();
134+
let note = new apn.Notification();
128135

129136
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
130137
note.badge = 3;
@@ -137,29 +144,32 @@ note.topic = "<your-app-bundle-id>";
137144
Send the notification to the API with `send`, which returns a promise.
138145

139146
```javascript
140-
apnProvider.send(note, deviceToken).then( (result) => {
147+
try {
148+
const result = apnProvider.send(note, deviceToken);
141149
// see documentation for an explanation of result
142-
});
150+
} catch(error) {
151+
// Handle error...
152+
}
143153
```
144154

145-
This will result in the the following notification payload being sent to the device
155+
This will result in the following notification payload being sent to the device.
146156

147157
```json
148158
{"messageFrom":"John Appelseed","aps":{"badge":3,"sound":"ping.aiff","alert":"\uD83D\uDCE7 \u2709 You have a new message"}}
149159
```
150160

151-
Create a Live Activity notification object, configuring it with the relevant parameters (See the [notification documentation](doc/notification.markdown) for more details.)
161+
Create a Live Activity notification object and configure it with the relevant parameters (See the [notification documentation](doc/notification.markdown) for more details.)
152162

153163
```javascript
154-
var note = new apn.Notification();
164+
let note = new apn.Notification();
155165

166+
note.topic = "<your-app-bundle-id>.push-type.liveactivity";
156167
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
168+
note.pushType = "liveactivity",
157169
note.badge = 3;
158170
note.sound = "ping.aiff";
159171
note.alert = "\uD83D\uDCE7 \u2709 You have a new message";
160172
note.payload = {'messageFrom': 'John Appleseed'};
161-
note.topic = "<your-app-bundle-id>";
162-
note.pushType = "liveactivity",
163173
note.relevanceScore = 75,
164174
note.timestamp = Math.floor(Date.now() / 1000); // Current time
165175
note.staleDate = Math.floor(Date.now() / 1000) + (8 * 3600); // Expires 8 hour from now.
@@ -170,18 +180,107 @@ note.contentState = {}
170180
Send the notification to the API with `send`, which returns a promise.
171181

172182
```javascript
173-
apnProvider.send(note, deviceToken).then( (result) => {
174-
// see documentation for an explanation of result
175-
});
183+
try {
184+
const result = await apnProvider.send(note, deviceToken);
185+
// see the documentation for an explanation of the result
186+
} catch (error) {
187+
// Handle error...
188+
}
176189
```
177190

178-
This will result in the the following notification payload being sent to the device
191+
This will result in the following notification payload being sent to the device.
179192

180193

181194
```json
182195
{"messageFrom":"John Appleseed","aps":{"badge":3,"sound":"ping.aiff","alert":"\uD83D\uDCE7 \u2709 You have a new message", "relevance-score":75,"timestamp":1683129662,"stale-date":1683216062,"event":"update","content-state":{}}}
183196
```
184197

185-
You should only create one `Provider` per-process for each certificate/key pair you have. You do not need to create a new `Provider` for each notification. If you are only sending notifications to one app then there is no need for more than one `Provider`.
198+
## Manage Channels
199+
Starting in iOS 18 and iPadOS 18 Live Activities can be used to broadcast push notifications over channels. To do so, you will need your apps' `bundleId`.
200+
201+
```javascript
202+
let bundleId = "com.node.apn";
203+
```
204+
205+
Create a notification object, configuring it with the relevant parameters (See the [notification documentation](doc/notification.markdown) for more details.)
206+
207+
```javascript
208+
let note = new apn.Notification();
209+
210+
note.requestId = "0309F412-AA57-46A8-9AC6-B5AECA8C4594"; // Optional
211+
note.payload = {'message-storage-policy': '1', 'push-type': 'liveactivity'}; // Required
212+
```
213+
214+
Create a channel with `manageChannels` and the `create` action, which returns a promise.
215+
216+
```javascript
217+
try {
218+
const result = await apnProvider.manageChannels(note, bundleId, 'create');
219+
// see the documentation for an explanation of the result
220+
} catch (error) {
221+
// Handle error...
222+
}
223+
```
224+
225+
If the channel is created successfully, the result will look like the following:
226+
```javascript
227+
{
228+
apns-request-id: '0309F412-AA57-46A8-9AC6-B5AECA8C4594',
229+
apns-channel-id: 'dHN0LXNyY2gtY2hubA==' // The new channel
230+
}
231+
```
232+
233+
Similarly, `manageChannels` has additional `action`s that allow you to `read`, `readAll`, and `delete` channels. The `read` and `delete` actions require similar information to the `create` example above, with the exception that they require `note.channelId` to be populated. To request all active channel id's, you can use the `readAll` action:
234+
235+
```javascript
236+
try {
237+
const result = await apnProvider.manageChannels(note, bundleId, 'readAll');
238+
// see the documentation for an explanation of the result
239+
} catch (error) {
240+
// Handle error...
241+
}
242+
```
243+
244+
After the promise is fulfilled, `result` will look like the following:
245+
246+
```javascript
247+
{
248+
apns-request-id: 'some id value',
249+
channels: ['dHN0LXNyY2gtY2hubA==', 'eCN0LXNyY2gtY2hubA==' ...] // A list of active channels
250+
}
251+
```
252+
253+
Further information about managing channels can be found in [Apple's documentation](https://developer.apple.com/documentation/usernotifications/sending-channel-management-requests-to-apns).
254+
255+
## Sending A Broadcast Notification
256+
Starting in iOS 18 and iPadOS 18, after a channel is created using `manageChannels`, broadcast push notifications can be sent to any device subscribed to the respective `channelId` created for a `bundleId`. A broadcast notification looks similar to a standard Live Activity notification mentioned above but requires `note.channelId` to be populated. An example is below:
257+
258+
```javascript
259+
let note = new apn.Notification();
260+
261+
note.channelId = "dHN0LXNyY2gtY2hubA=="; // Required
262+
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
263+
note.pushType = "liveactivity",
264+
note.badge = 3;
265+
note.sound = "ping.aiff";
266+
note.alert = "\uD83D\uDCE7 \u2709 You have a new message";
267+
note.payload = {'messageFrom': 'John Appleseed'};
268+
note.relevanceScore = 75,
269+
note.timestamp = Math.floor(Date.now() / 1000); // Current time
270+
note.staleDate = Math.floor(Date.now() / 1000) + (8 * 3600); // Expires 8 hour from now.
271+
note.event = "update"
272+
note.contentState = {}
273+
```
274+
275+
Send the broadcast notification to the API with `broadcast`, which returns a promise.
276+
277+
```javascript
278+
try {
279+
const result = await apnProvider.broadcast(note, bundleId);
280+
// see documentation for an explanation of result
281+
} catch (error) {
282+
// Handle error...
283+
}
284+
```
186285

187-
If you are constantly creating `Provider` instances in your app, make sure to call `Provider.shutdown()` when you are done with each provider to release its resources and memory.
286+
Further information about broadcast notifications can be found in [Apple's documentation](https://developer.apple.com/documentation/usernotifications/sending-broadcast-push-notification-requests-to-apns).

doc/provider.markdown

+51-12
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,34 @@ Options:
1313

1414
- `key` {Buffer|String} The filename of the connection key to load from disk, or a Buffer/String containing the key data. (Defaults to: `key.pem`)
1515

16-
- `ca` An array of trusted certificates. Each element should contain either a filename to load, or a Buffer/String (in PEM format) to be used directly. If this is omitted several well known "root" CAs will be used. - You may need to use this as some environments don't include the CA used by Apple (entrust_2048).
16+
- `ca` An array of trusted certificates. Each element should contain either a filename to load, or a Buffer/String (in PEM format) to be used directly. If this is omitted several well known "root" CAs will be used. - You may need to use this as some environments don't include the CA used by Apple (entrust_2048)
1717

18-
- `pfx` {Buffer|String} File path for private key, certificate and CA certs in PFX or PKCS12 format, or a Buffer containing the PFX data. If supplied will always be used instead of certificate and key above.
18+
- `pfx` {Buffer|String} File path for private key, certificate and CA certs in PFX or PKCS12 format, or a Buffer containing the PFX data. If supplied will always be used instead of certificate and key above
1919

2020
- `passphrase` {String} The passphrase for the connection key, if required
2121

2222
- `production` {Boolean} Specifies which environment to connect to: Production (if true) or Sandbox - The hostname will be set automatically. (Defaults to NODE_ENV == "production", i.e. false unless the NODE_ENV environment variable is set accordingly)
2323

2424
- `rejectUnauthorized` {Boolean} Reject Unauthorized property to be passed through to tls.connect() (Defaults to `true`)
2525

26+
- `address` {String} The address of the APNs server to send notifications to. If not provided, will connect to standard APNs server
27+
28+
- `port` {Number} The port of the APNs server to send notifications to. (Defaults to 443)
29+
30+
- `manageChannelsAddress` {String} The address of the APNs channel management server to send notifications to. If not provided, will connect to standard APNs channel management server
31+
32+
- `manageChannelsPort` {Number} The port of the APNs channel management server to send notifications to. If not provided, will connect to standard APNs channel management port
33+
34+
- `proxy` {host: String, port: Number|String} Connect through an HTTP proxy when sending notifications
35+
36+
- `manageChannelsProxy` {host: String, port: Number|String} Connect through an HTTP proxy when managing channels
37+
38+
- `rejectUnauthorized` {Boolean} Reject Unauthorized property to be passed through to tls.connect() (Defaults to `true`)
39+
2640
- `connectionRetryLimit` {Number} The maximum number of connection failures that will be tolerated before `apn.Provider` will "give up". [See below.](#connection-retry-limit) (Defaults to: 3)
2741

42+
- `heartBeat` {Number} The delay interval in ms that apn will ping APNs servers. (Defaults to: 60000)
43+
2844
- `requestTimeout` {Number} The maximum time in ms that apn will wait for a response to a request. (Defaults to: 5000)
2945

3046
#### Provider Certificates vs. Authentication Tokens
@@ -47,31 +63,54 @@ The `Provider` can continue to be used for sending notifications and the counter
4763

4864
## Class: apn.Provider
4965

66+
`apn.Provider` provides a number of methods for sending notifications, broadcasting notifications, and managing channels. Calling any of the methods will return a `Promise` for each notification, which is discussed more in [Results from APN Provider Methods](#results-from-apnprovider-methods).
67+
5068
### connection.send(notification, recipients)
5169

52-
This is main interface for sending notifications. Create a `Notification` object and pass it in, along with a single recipient or an array of them and node-apn will take care of the rest, delivering a copy of the notification to each recipient.
70+
This is the main interface for sending notifications. Create a `Notification` object and pass it in, along with a single recipient or an array of them, and node-apn will take care of the rest, delivering a copy of the notification to each recipient.
5371

5472
> A "recipient" is a `String` containing the hex-encoded device token.
5573
56-
Calling `send` will return a `Promise`. The Promise will resolve after each notification (per token) has reached a final state. Each notification can end in one of three possible states:
74+
Calling `send` will return a `Promise`. The Promise will resolve after each notification (per token) has reached a final state.
75+
76+
### connection.manageChannels(notification, bundleId, action)
77+
This is the interface for managing broadcast channels. Create a single `Notification` object or an array of them and pass the notification(s) in, along with a bundleId, and an action (`create`, `read`, `readAll`, `delete`) and node-apn will take care of the rest, asking the APNs to perform the action using the criteria specified in each notification.
78+
79+
> A "bundleId" is a `String` containing the bundle identifier for the application.
80+
81+
> An "action" is a `String` containing: `create`, `read`, `readAll`, or `delete` and represents what action to perform with a channel (See more in [Apple Documentation](https://developer.apple.com/documentation/usernotifications/sending-channel-management-requests-to-apns)).
82+
83+
Calling `manageChannels` will return a `Promise`. The Promise will resolve after each notification has reached a final state.
84+
85+
### connection.broadcast(notification, bundleId)
86+
87+
This is the interface for broadcasting Live Activity notifications. Create a single `Notification` object or an array of them and pass the notification(s) in, along with a bundleId and node-apn will take care of the rest, asking the APNs to broadcast using the criteria specified in each notification.
88+
89+
> A "bundleId" is a `String` containing the bundle identifier for the application.
90+
91+
Calling `broadcast` will return a `Promise`. The Promise will resolve after each notification has reached a final state.
92+
93+
### Results from apn.Provider methods
94+
95+
Each notification can end in one of three possible states:
5796

5897
- `sent` - the notification was accepted by Apple for delivery to the given recipient
59-
- `failed` (rejected) - the notification was rejected by Apple. A rejection has an associated `status` and `reason` which is included.
60-
- `failed` (error) - a connection-level error occurred which prevented successful communication with Apple. In very rare cases it's possible that the notification was still delivered. However, this state usually results from a network problem.
98+
- `failed` (rejected) - the notification was rejected by Apple. A rejection has an associated `status` and `reason` which are included.
99+
- `failed` (error) - a connection-level error occurred, which prevented successful communication with Apple. In very rare cases, it's possible that the notification was still delivered. However, this state usually results from a network problem.
61100

62101
When the returned `Promise` resolves, its value will be an Object containing two properties
63102

64103
#### sent
65104

66-
An array of device tokens to which the notification was successfully sent and accepted by Apple.
105+
An array of device tokens or bundle identifiers to which the notification was successfully sent and accepted by Apple.
67106

68-
Being `sent` does **not** guaranteed the notification will be _delivered_, other unpredictable factors - including whether the device is reachable - can ultimately prevent delivery.
107+
Being `sent` does **not** guarantee the notification will be _delivered_, other unpredictable factors - including whether the device is reachable - can ultimately prevent delivery.
69108

70109
#### failed
71110

72-
An array of objects for each failed token. Each object will contain the device token which failed and details of the failure which will differ between rejected and errored notifications.
111+
An array of objects for each failed token or bundle identifier. Each object will contain the device token or bundle identifier that failed and details of the failure, which will differ between rejected and errored notifications.
73112

74-
For **rejected** notifications the object will take the following form
113+
For **rejected** notifications using `send()`, the object will take the following form
75114

76115
```javascript
77116
{
@@ -85,7 +124,7 @@ For **rejected** notifications the object will take the following form
85124

86125
More details about the response and associated status codes can be found in the [HTTP/2 Response from APN documentation][http2-response].
87126

88-
If a failed notification was instead caused by an **error** then it will have an `error` property instead of `response` and `status`:
127+
If a failed notification using `send()` was instead caused by an **error** then it will have an `error` property instead of `response` and `status`:
89128

90129
```javascript
91130
{
@@ -103,7 +142,7 @@ If you wish to send notifications containing emoji or other multi-byte character
103142

104143
Indicate to node-apn that it should close all open connections when the queue of pending notifications is fully drained. This will allow your application to terminate.
105144

106-
**Note:** If notifications are pushed after the connection has started, an error will be thrown.
145+
**Note:** If notifications are pushed after the shutdown has started, an error will be thrown.
107146

108147
[provider-api]: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html
109148
[provider-auth-tokens]: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1

0 commit comments

Comments
 (0)