Conceptual/fundamental questions regarding basic setup & shared code with service workers #70
Replies: 1 comment 4 replies
-
There's a
npx web-push generate-vapid-keys --json NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=THE_COMMAND_ABOVE_OUTPUT.publicKey
WEB_PUSH_PRIVATE_KEY=THE_COMMAND_ABOVE_OUTPUT.privateKey
WEB_PUSH_EMAIL=email@example.com // next.config.mjs
import withSerwistInit from "@serwist/next";
const withSerwist = withSerwistInit({
cacheOnNavigation: true,
swSrc: "app/sw.ts",
swDest: "public/sw.js",
// Disable precaching.
exclude: [() => true],
});
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default withSerwist(nextConfig);
// app/SendNotification.tsx
"use client";
import type { MouseEventHandler } from "react";
import { useEffect, useState } from "react";
const base64ToUint8Array = (base64: string) => {
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const b64 = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(b64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
export default function SendNotification() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [subscription, setSubscription] = useState<PushSubscription | null>(
null
);
const [registration, setRegistration] =
useState<ServiceWorkerRegistration | null>(null);
useEffect(() => {
if (
typeof window !== "undefined" &&
"serviceWorker" in navigator &&
window.serwist !== undefined
) {
// run only in browser
navigator.serviceWorker.ready.then((reg) => {
reg.pushManager.getSubscription().then((sub) => {
if (
sub &&
!(
sub.expirationTime &&
Date.now() > sub.expirationTime - 5 * 60 * 1000
)
) {
setSubscription(sub);
setIsSubscribed(true);
}
});
setRegistration(reg);
});
}
}, []);
const subscribeButtonOnClick: MouseEventHandler<HTMLButtonElement> = async (
event
) => {
if (!process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY) {
throw new Error("Environment variables supplied not sufficient.");
}
if (!registration) {
console.error("No SW registration available.");
return;
}
event.preventDefault();
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: base64ToUint8Array(
process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY
),
});
// TODO: you should call your API to save subscription data on the server in order to send web push notification from the server
setSubscription(sub);
setIsSubscribed(true);
alert("Web push subscribed!");
console.log(sub);
};
const unsubscribeButtonOnClick: MouseEventHandler<HTMLButtonElement> = async (
event
) => {
if (!subscription) {
console.error("Web push not subscribed");
return;
}
event.preventDefault();
await subscription.unsubscribe();
// TODO: you should call your API to delete or invalidate subscription data on the server
setSubscription(null);
setIsSubscribed(false);
console.log("Web push unsubscribed!");
};
const sendNotificationButtonOnClick: MouseEventHandler<
HTMLButtonElement
> = async (event) => {
event.preventDefault();
if (!subscription) {
alert("Web push not subscribed");
return;
}
try {
await fetch("/notification", {
method: "POST",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({
subscription,
}),
signal: AbortSignal.timeout(10000),
});
} catch (err) {
if (err instanceof Error) {
if (err.name === "TimeoutError") {
console.error("Timeout: It took too long to get the result.");
} else if (err.name === "AbortError") {
console.error(
"Fetch aborted by user action (browser stop button, closing tab, etc.)"
);
} else if (err.name === "TypeError") {
console.error("The AbortSignal.timeout() method is not supported.");
} else {
// A network error, or some other problem.
console.error(`Error: type: ${err.name}, message: ${err.message}`);
}
} else {
console.error(err);
}
alert("An error happened.");
}
};
return (
<>
<button
type="button"
onClick={subscribeButtonOnClick}
disabled={isSubscribed}
>
Subscribe
</button>
<button
type="button"
onClick={unsubscribeButtonOnClick}
disabled={!isSubscribed}
>
Unsubscribe
</button>
<button
type="button"
onClick={sendNotificationButtonOnClick}
disabled={!isSubscribed}
>
Send Notification
</button>
</>
);
}
// app/page.tsx
import type { Metadata } from "next";
import SendNotification from "./SendNotification";
export const metadata: Metadata = {
title: "Home",
};
export default function Page() {
return (
<>
<h1>Next.js + Serwist = Ain't no way</h1>
<SendNotification />
</>
);
};
// app/notification/route.ts
import { NextRequest, NextResponse } from "next/server";
import webPush from "web-push";
export const POST = async (req: NextRequest) => {
if (!process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY || !process.env.WEB_PUSH_EMAIL || !process.env.WEB_PUSH_PRIVATE_KEY) {
throw new Error("Environment variables supplied not sufficient.");
}
const { subscription } = (await req.json()) as {
subscription: webPush.PushSubscription;
};
try {
webPush.setVapidDetails(`mailto:${process.env.WEB_PUSH_EMAIL}`, process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, process.env.WEB_PUSH_PRIVATE_KEY);
const response = await webPush.sendNotification(
subscription,
JSON.stringify({
title: "Hello Web Push",
message: "Your web push notification is here!",
}),
);
return new NextResponse(response.body, {
status: response.statusCode,
headers: response.headers,
});
} catch (err) {
if (err instanceof webPush.WebPushError) {
return new NextResponse(err.body, {
status: err.statusCode,
headers: err.headers,
});
}
console.log(err);
return new NextResponse("Internal Server Error", {
status: 500,
});
}
};
// app/sw.ts
import type { PrecacheEntry } from "@serwist/precaching";
import { installSerwist } from "@serwist/sw";
declare const self: ServiceWorkerGlobalScope & {
// Change this attribute's name to your `injectionPoint`.
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
};
self.addEventListener("push", function (event) {
const data = JSON.parse(event.data?.text() ?? '{ title: "" }');
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.message,
icon: "/icons/android-chrome-192x192.png",
})
);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(
self.clients
.matchAll({ type: "window", includeUncontrolled: true })
.then(function (clientList) {
if (clientList.length > 0) {
let client = clientList[0];
for (let i = 0; i < clientList.length; i++) {
if (clientList[i].focused) {
client = clientList[i];
}
}
return client.focus();
}
return self.clients.openWindow("/");
})
);
});
installSerwist({
// This should be an empty array thanks to `exclude: [() => true]`.
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: false,
// Disable runtimeCaching.
runtimeCaching: undefined,
}); Ported from https://github.com/DuCanhGH/next-pwa/blob/bbc8cf13e97737cdee3e65f0a7b2ab1fb05736c6/examples/web-push/pages/index.tsx. We can probably make a new example in Serwist's repo, too.
Yes. Go ham on that thing. |
Beta Was this translation helpful? Give feedback.
4 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Hi there!
I'm currently migrating an existing next js page router app to the app router and in this process I'd like to migrate from
next-pwa
toserwist
asnext-pwa
doesn't seem to be maintained anymore.Before doing so, I have a 2 questions which I hopefully find answers to here:
1. How to best setup/configure serwist for my use case?
I'm working on a reminder app and I'm only using the service worker to register push notifications which show a notification to the user. Also the sw performs some action when the user clicks on such a notifications.
That's all I need. I don't need no offline functionality, no advanced caching etc.
Questions
serwist
in between? I'd really appreciate some honest opinion on this as I can't really estimate how much effort this would take in comparison to usingserwist
.2. Can I use/import util functions from my "app code" in the service worker when using TS path aliases?
With
next-pwa
I was in theory able to import util functions from my app code like some shared date/time formatting stuff insrc/utils/date.utils.ts
. However, my app is using TS path aliases likeimport { x } from '@/utils/date.utils'
and I wasn't able to make this work withnext-pwa
(see this question of mine for more details).Should this be possible with
serwist
or am I also going to have troubles with this setup?Thanks!
Beta Was this translation helpful? Give feedback.
All reactions