Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions LICENCE.txt

This file was deleted.

25 changes: 0 additions & 25 deletions README.md

This file was deleted.

3 changes: 3 additions & 0 deletions extension/README.md.args.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const extraContents = `
testing whoaoaoaoa
`;
6 changes: 6 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"scripts": {
"generate-vapid-keys": "yarn workspace @se-2/nextjs web-push generate-vapid-keys --json"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
4 changes: 4 additions & 0 deletions extension/packages/nextjs/.env.example.args.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const additionalVars = "# Generate VAPID keys with: `yarn generate-vapid-keys`
WEB_PUSH_EMAIL=
WEB_PUSH_PRIVATE_KEY=
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=";
29 changes: 29 additions & 0 deletions extension/packages/nextjs/.gitignore.args.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//// This is the current gitignore template
//// But I don't get how we can add things to it.

// const contents = () =>
// `# dependencies
// node_modules

// # yarn
// .yarn/*
// !.yarn/patches
// !.yarn/plugins
// !.yarn/releases
// !.yarn/sdks
// !.yarn/versions

// # eslint
// .eslintcache

// # misc
// .DS_Store

// # IDE
// .vscode
// .idea

// # cli
// dist`;

// export default contents
141 changes: 141 additions & 0 deletions extension/packages/nextjs/app/SendNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"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 (
<div className="flex flex-col items-center gap-2 bg-base-100 p-4 m-8 rounded-lg">
<h2 className="text-lg font-bold">Web Push Notifications</h2>
<div className="flex flex-col md:flex-row gap-4">
<button
type="button"
className="btn btn-xs btn-primary"
onClick={subscribeButtonOnClick}
disabled={isSubscribed}
>
Subscribe
</button>
<button
type="button"
className="btn btn-xs btn-primary"
onClick={unsubscribeButtonOnClick}
disabled={!isSubscribed}
>
Unsubscribe
</button>
<button
type="button"
className="btn btn-xs btn-primary"
onClick={sendNotificationButtonOnClick}
disabled={!isSubscribed}
>
Send Notification
</button>
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions extension/packages/nextjs/app/notification/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type 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 Scaffold-Serwist",
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,
});
}
};
Empty file.
63 changes: 63 additions & 0 deletions extension/packages/nextjs/app/sw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// <reference lib="webworker" />
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";

declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
// Change this attribute's name to your `injectionPoint`.
// `injectionPoint` is an InjectManifest option.
// See https://serwist.pages.dev/docs/build/configuring
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
}
}

declare const self: ServiceWorkerGlobalScope;

const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
fallbacks: {
entries: [
{
url: "/~offline",
matcher({ request }) {
return request.destination === "document";
},
},
],
},
});

self.addEventListener("push", 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(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("/");
}),
);
});

serwist.addEventListeners();
14 changes: 14 additions & 0 deletions extension/packages/nextjs/app/~offline/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Offline",
};

export default function Page() {
return (
<>
<h1>This is offline fallback page</h1>
<h2>When offline, any page route will fallback to this page</h2>
</>
);
}
37 changes: 37 additions & 0 deletions extension/packages/nextjs/components/InstallPWA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from "react";

export const InstallPWA = () => {
const [supportsPWA, setSupportsPWA] = useState(false);
const [promptInstall, setPromptInstall] = useState<any>(null);

useEffect(() => {
const handler = (e: any) => {
e.preventDefault();
setSupportsPWA(true);
setPromptInstall(e);
};
window.addEventListener("beforeinstallprompt", handler);

return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);

const onClick = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
evt.preventDefault();
if (!promptInstall) {
return;
}
promptInstall.prompt();
};

if (!supportsPWA) {
return null;
}

return (
<div className="flex flex-col items-center gap-2 rounded-lg">
<button className="btn btn-sm btn-primary" onClick={onClick}>
Install App
</button>
</div>
);
};
Loading