Skip to content

Commit e842fdc

Browse files
Added usage documentation
1 parent 4905d38 commit e842fdc

File tree

1 file changed

+201
-4
lines changed

1 file changed

+201
-4
lines changed

README.md

Lines changed: 201 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ A **Push Service** is run by browsers to coordinate delivery of messages to subs
9696

9797
</details>
9898

99+
99100
### Generating Keys
100101

101102
Before integrating WebPush into your server, you must generate one time VAPID keys to identify your server to push services with. To help we this, we provide `vapid-key-generator`, which you can install and use as needed:
@@ -116,7 +117,7 @@ To update the generator, uninstall it and re-install it after pulling from main:
116117
% swift package experimental-install
117118
```
118119

119-
Once installed, a new configuration can be generated as needed:
120+
Once installed, a new configuration can be generated as needed. Here, we generate a configuration with `https://example.com` as our support URL for push service administrators to use to contact us when issues occur:
120121
```
121122
% ~/.swiftpm/bin/vapid-key-generator https://example.com
122123
VAPID.Configuration: {"contactInformation":"https://example.com","expirationDuration":79200,"primaryKey":"6PSSAJiMj7uOvtE4ymNo5GWcZbT226c5KlV6c+8fx5g=","validityDuration":72000}
@@ -162,22 +163,218 @@ OPTIONS:
162163
> [!TIP]
163164
> If you prefer, you can also generate keys in your own code by calling `VAPID.Key()`, but remember, the key should be persisted and re-used from that point forward!
164165
166+
165167
### Setup
166168

167-
TBD
169+
During the setup stage of your application server, decode the VAPID configuration you created above and initialize a `WebPushManager` with it:
170+
171+
```swift
172+
import WebPush
173+
174+
...
175+
176+
guard
177+
let rawVAPIDConfiguration = ProcessInfo.processInfo.environment["VAPID-CONFIG"],
178+
let vapidConfiguration = try? JSONDecoder().decode(VAPID.Configuration.self, from: Data(rawVAPIDConfiguration.utf8))
179+
else { fatalError("VAPID keys are unavailable, please generate one and add it to the environment.") }
180+
181+
let manager = WebPushManager(
182+
vapidConfiguration: vapidConfiguration,
183+
backgroundActivityLogger: logger
184+
/// If you customized the event loop group your app uses, you can set it here:
185+
// eventLoopGroupProvider: .shared(app.eventLoopGroup)
186+
)
187+
188+
try await ServiceGroup(
189+
services: [
190+
/// Your other services here
191+
manager
192+
],
193+
gracefulShutdownSignals: [.sigint],
194+
logger: logger
195+
).run()
196+
```
197+
198+
If you are not yet using [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle), you can skip adding it to the service group, and it'll shut down on deinit instead. This however may be too late to finish sending over all in-flight messages, so prefer to use a ServiceGroup for all your services if you can.
199+
200+
You'll also want to serve a `serviceWorker.mjs` file at the root of your server (it can be anywhere, but there are scoping restrictions that are simplified by serving it at the root) to handle incoming notifications:
201+
202+
```js
203+
self.addEventListener('push', function(event) {
204+
const data = event.data?.json() ?? {};
205+
event.waitUntil((async () => {
206+
/// Try parsing the data, otherwise use fallback content. DO NOT skip sending the notification, as you must display one for every push message that is received or your subscription will be dropped.
207+
let title = data?.title ?? "Your App Name";
208+
const body = data?.body ?? "New Content Available!";
209+
210+
await self.registration.showNotification(title, {
211+
body,
212+
icon: "/notification-icon.png", /// Only some browsers support this.
213+
data
214+
});
215+
})());
216+
});
217+
```
218+
219+
> [!NOTE]
220+
> `.mjs` here allows your code to import other js modules as needed. If you are not using Vapor, please make sure your server uses the correct mime type for this file extension.
221+
168222
169223
### Registering Subscribers
170224
171-
TBD
225+
To register a subscriber, you'll need backend code to provide your VAPID key, and frontend code to ask the browser for a subscription on behalf of the user.
226+
227+
On the backend (we are assuming Vapor here), register a route that returns your VAPID public key:
228+
229+
```swift
230+
import WebPush
231+
232+
...
233+
234+
/// Listen somewhere for a VAPID key request. This path can be anything you want, and should be available to all parties you with to serve push messages to.
235+
app.get("vapidKey", use: loadVapidKey)
236+
237+
...
238+
239+
/// A wrapper for the VAPID key that Vapor can encode.
240+
struct WebPushOptions: Codable, Content, Hashable, Sendable {
241+
static let defaultContentType = HTTPMediaType(type: "application", subType: "webpush-options+json")
242+
243+
var vapid: VAPID.Key.ID
244+
}
245+
246+
/// The route handler, usually part of a route controller.
247+
@Sendable func loadVapidKey(request: Request) async throws -> WebPushOptions {
248+
WebPushOptions(vapid: manager.nextVAPIDKeyID)
249+
}
250+
```
251+
252+
Also register a route for persisting `Subscriber`'s:
253+
254+
```swift
255+
import WebPush
256+
257+
...
258+
259+
/// Listen somewhere for new registrations. This path can be anything you want, and should be available to all parties you with to serve push messages to.
260+
app.get("registerSubscription", use: registerSubscription)
261+
262+
...
263+
264+
/// A custom type for communicating the status of your subscription. Fill this out with any options you'd like to communicate back to the user.
265+
struct SubscriptionStatus: Codable, Content, Hashable, Sendable {
266+
var subscribed = true
267+
}
268+
269+
/// The route handler, usually part of a route controller.
270+
@Sendable func registerSubscription(request: Request) async throws -> SubscriptionStatus {
271+
let subscriptionRequest = try request.content.decode(Subscriber.self, as: .jsonAPI)
272+
273+
// TODO: Persist subscriptionRequest!
274+
275+
return SubscriptionStatus()
276+
}
277+
```
278+
279+
> [!NOTE]
280+
> `WebPushManager` (`manager` here) is fully sendable, and should be shared with your controllers using dependency injection. This allows you to fully test your application server by relying on the provided `WebPushTesting` library in your unit tests to mock keys, verify delivery, and simulate errors.
281+
282+
On the frontend, register your service worker, fetch your vapid key, and subscribe on behalf of the user:
283+
284+
```js
285+
const serviceRegistration = await navigator.serviceWorker?.register("/serviceWorker.mjs", { type: "module" });
286+
let subscription = await registration?.pushManager?.getSubscription();
287+
288+
/// Wait for the user to interact with the page to request a subscription.
289+
document.getElementById("notificationsSwitch").addEventListener("click", async ({ currentTarget }) => {
290+
try {
291+
/// If we couldn't load a subscription, now's the time to ask for one.
292+
if (!subscription) {
293+
const applicationServerKey = await loadVAPIDKey();
294+
subscription = await serviceRegistration.pushManager.subscribe({
295+
userVisibleOnly: true,
296+
applicationServerKey,
297+
});
298+
}
299+
300+
/// It is safe to re-register the same subscription.
301+
const subscriptionStatusResponse = await registerSubscription(subscription);
302+
303+
/// Do something with your registration. Some may use it to store notification settings and display those back to the user.
304+
...
305+
} catch (error) {
306+
/// Tell the user something went wrong here.
307+
console.error(error);
308+
}
309+
}
310+
});
311+
312+
...
313+
314+
async function loadVAPIDKey() {
315+
/// Make sure this is the same route used above.
316+
const httpResponse = await fetch(`/vapidKey`);
317+
318+
const webPushOptions = await httpResponse.json();
319+
if (httpResponse.status != 200) throw new Error(webPushOptions.reason);
320+
321+
return webPushOptions.vapid;
322+
}
323+
324+
export async function registerSubscription(subscription) {
325+
/// Make sure this is the same route used above.
326+
const subscriptionStatusResponse = await fetch(`/registerSubscription`, {
327+
method: "POST",
328+
body: {
329+
...subscription.toJSON(),
330+
/// It is good practice to provide the applicationServerKey back here so we can track which one was used if multiple were provided during configuration.
331+
applicationServerKey: subscription.options.applicationServerKey,
332+
}
333+
});
334+
335+
/// Do something with your registration. Some may use it to store notification settings and display those back to the user.
336+
...
337+
}
338+
```
339+
172340
173341
### Sending Messages
174342
175-
TBD
343+
To send a message, call one of the `send()` methods on `WebPushManager` with a `Subscriber`:
344+
345+
```swift
346+
import WebPush
347+
348+
...
349+
350+
do {
351+
try await manager.send(
352+
json: ["title": "Test Notification", "body": "Hello, World!"
353+
/// If sent from a request, pass the request's logger here to maintain its metadata.
354+
// logger: request.logger
355+
)
356+
} catch BadSubscriberError() {
357+
/// The subscription is no longer valid and should be removed.
358+
} catch MessageTooLargeError() {
359+
/// The message was too long and should be shortened.
360+
} catch let error as HTTPError {
361+
/// The push service ran into trouble. error.response may help here.
362+
} catch {
363+
/// An unknown error occurred.
364+
}
365+
```
366+
367+
Your service worker will receive this message, decode it, and present it to the user.
368+
369+
> [!NOTE]
370+
> Although the spec supports it, most browsers do not support silent notifications, and will drop a subscription if they are used.
371+
176372
177373
### Testing
178374
179375
The `WebPushTesting` module can be used to obtain a mocked `WebPushManager` instance that allows you to capture all messages that are sent out, or throw your own errors to validate your code functions appropriately. Only import `WebPushTesting` in your testing targets.
180376
377+
181378
## Specifications
182379
183380
- [RFC 7515 JSON Web Signature (JWS)](https://datatracker.ietf.org/doc/html/rfc7515)

0 commit comments

Comments
 (0)