bunch of fixes for PWA, set up web push notifications, add buttons to remove service workers, add settings menu, let settings manage keys, permissions, and service worker registration

This commit is contained in:
Silas 2024-02-04 22:57:52 -05:00
parent c3db0e6002
commit c13003cd6f
Signed by: silentsilas
GPG Key ID: 4199EFB7DAA34349
22 changed files with 506 additions and 190 deletions

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
API_BASE_URL=https://yeah.sure.dog
BASE_URL=https://sure.dog

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SURE - Secure URL Requests</title>
<title>SURE DOG - Secure URL Requests</title>
<meta name="description" content="Securely request information via SURE links" />
<meta property="og:title" content="SURE - Secure URL Requests" />

60
package-lock.json generated
View File

@ -1,22 +1,25 @@
{
"name": "sure",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sure",
"version": "0.1.0",
"version": "0.2.0",
"dependencies": {
"@picocss/pico": "^1.5.11",
"uhtml": "^4.4.7"
"uhtml": "^4.4.7",
"workbox-window": "^7.0.0"
},
"devDependencies": {
"@types/workbox-precaching": "^5.0.0",
"sass": "^1.70.0",
"typescript": "^5.2.2",
"vite": "^5.0.8",
"vite-plugin-make-offline": "^1.0.0",
"vite-plugin-pwa": "^0.17.5"
"vite-plugin-pwa": "^0.17.5",
"workbox-precaching": "^7.0.0"
}
},
"node_modules/@ampproject/remapping": {
@ -2387,8 +2390,17 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@types/workbox-precaching": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/workbox-precaching/-/workbox-precaching-5.0.0.tgz",
"integrity": "sha512-Oytxs/I0nE6oM3p4vJsYyLcLldFb9tyG+WBk+UrL2aWizEdpTeVeV+CYG5/lWBhCyXaiEQPgYzWaf3kzLFZAiQ==",
"deprecated": "This is a stub types definition. workbox-precaching provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"workbox-precaching": "*"
}
},
"node_modules/@webreflection/signal": {
"version": "2.0.0",
@ -4212,12 +4224,12 @@
}
},
"node_modules/pretty-bytes": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
"dev": true,
"engines": {
"node": "^14.13.1 || >=16.0.0"
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@ -5153,6 +5165,18 @@
"workbox-window": "^7.0.0"
}
},
"node_modules/vite-plugin-pwa/node_modules/pretty-bytes": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
"dev": true,
"engines": {
"node": "^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@ -5351,18 +5375,6 @@
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"node_modules/workbox-build/node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
"dev": true,
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/workbox-build/node_modules/rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
@ -5406,8 +5418,7 @@
"node_modules/workbox-core": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz",
"integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==",
"dev": true
"integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ=="
},
"node_modules/workbox-expiration": {
"version": "7.0.0",
@ -5512,7 +5523,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.0.0.tgz",
"integrity": "sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==",
"dev": true,
"dependencies": {
"@types/trusted-types": "^2.0.2",
"workbox-core": "7.0.0"

View File

@ -1,7 +1,7 @@
{
"name": "sure",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -10,14 +10,17 @@
"preview": "vite preview"
},
"devDependencies": {
"@types/workbox-precaching": "^5.0.0",
"sass": "^1.70.0",
"typescript": "^5.2.2",
"vite": "^5.0.8",
"vite-plugin-make-offline": "^1.0.0",
"vite-plugin-pwa": "^0.17.5"
"vite-plugin-pwa": "^0.17.5",
"workbox-precaching": "^7.0.0"
},
"dependencies": {
"@picocss/pico": "^1.5.11",
"uhtml": "^4.4.7"
"uhtml": "^4.4.7",
"workbox-window": "^7.0.0"
}
}

View File

@ -1,5 +1,5 @@
import { html } from "uhtml";
import { openKeyManager } from "./keyManager.ts";
import { openSettings } from "./settings/Settings";
export const Footer = html`<footer class="container">
<div>
@ -18,15 +18,7 @@ export const Footer = html`<footer class="container">
>whoami</a
>
|
<a
href="#"
id="openKeyManager"
@click=${(event: Event) => {
event.preventDefault();
openKeyManager();
}}
>Manage Keys</a
>
<a href="#" @click=${openSettings}>Settings</a>
</p>
<p class="center">Version ${process.env.PACKAGE_VERSION}</p>

View File

@ -1,7 +1,7 @@
import { html } from "uhtml";
export const Header = html` <hgroup>
<h1>SURE</h1>
<h1>SURE<span class="primary">DOG</span></h1>
<h2>
<span class="primary">S</span>ecure <span class="primary">U</span>RL
<span class="primary">Re</span>quests

25
src/components/Modal.ts Normal file
View File

@ -0,0 +1,25 @@
import { Hole, html } from "uhtml";
import { signal } from "uhtml/preactive";
type ModalContent = Hole | ((...args: any[]) => Hole);
const isModalOpen = signal(false);
const Content = signal<ModalContent | undefined>(undefined);
export const AppModal = () => html`<dialog ?open=${isModalOpen.value}>
<article class="container-fluid">${renderContent(Content.value)}</article>
</dialog>`;
const renderContent = (innerContent: ModalContent | undefined) => {
return typeof innerContent === "function"
? (innerContent as (...args: any[]) => Hole)()
: innerContent;
};
export const openModal = (innerContent: ModalContent) => {
Content.value = innerContent;
isModalOpen.value = true;
};
export const closeModal = () => {
isModalOpen.value = false;
Content.value = undefined;
};

70
src/components/Update.ts Normal file
View File

@ -0,0 +1,70 @@
import { html } from "uhtml";
import { signal } from "uhtml/preactive";
import {
checkServiceWorkers,
deregisterServiceWorkers,
} from "./settings/Settings";
import { Workbox } from "workbox-window";
const open = signal(false);
const registration = signal<ServiceWorkerRegistration | undefined>(undefined);
export const UpdateModal = () => {
return html` <dialog ?open=${open.value}>
<article class="container-fluid">
<h3>An update is available!</h3>
<p>
A new version of the app is available! Click the button below to update
and reload. If you're stuck in an endless update loop, click Clear Cache
below to fix it.
</p>
<p>
<button aria-label="Update and reload" @click=${update}>Update</button>
</p>
<p>
<button
class="secondary"
aria-label="Clear Cache"
@click=${deregisterServiceWorkers}
>
Clear Cache
</button>
</p>
</article>
</dialog>`;
};
const update = async (event: Event) => {
event.preventDefault();
if (registration.value && registration.value.waiting) {
console.log("Service Worker is updating.");
registration.value.waiting.postMessage({ type: "SKIP_WAITING" });
} else {
console.error("Service Worker failed to update.");
}
};
export const checkForUpdates = async () => {
try {
if ("serviceWorker" in navigator) {
const wb = new Workbox("/sw.js");
wb.addEventListener("waiting", () => {
console.log("Service Worker is ready to handle offline usage.");
open.value = true;
});
wb.addEventListener("controlling", () => {
window.location.reload();
});
registration.value = await wb.register();
console.log("Service Worker has been registered.");
await checkServiceWorkers();
}
} catch (err) {
console.error("Failed to register Service Worker: ", err);
}
};

View File

@ -3,25 +3,18 @@ import {
exportKeys,
importAndSaveKeys,
retrieveOrGenerateKeyPair,
} from "../utils/crypto.ts";
} from "../../utils/crypto.ts";
import { signal } from "uhtml/preactive";
import { LOCAL_STORAGE_KEYS } from "../utils/store.ts";
import { LOCAL_STORAGE_KEYS } from "../../utils/store.ts";
import { openSettings } from "./Settings.ts";
const ecdhPublicKey = signal(
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY)
);
const ecdhPrivateKey = signal(
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY)
);
const isDialogOpen = signal(false);
const ecdhPublicKey = signal<string | null>(null);
const ecdhPrivateKey = signal<string | null>(null);
export const KeyManager =
() => html`<dialog class="key-manager" ?open=${isDialogOpen.value}>
<article>
<header>Key Manager</header>
export const KeyManager = () => html`
<h2>Key Manager</h2>
<p>
If you'd like to manage your keys, you can do so here. You can export your keys to transfer to a different
device, or import keys exported from a different device.
You can export your keys to transfer to a different device, or import keys exported from a different device.
</p>
<p>
Public Key:
@ -35,10 +28,8 @@ export const KeyManager =
<button @click=${exportKeys}>Export Keys</button>
<button @click=${importKeys}>Import Keys</button>
<input type="file" accept=".json" style="display: none" id="keypairImportEl" @change=${handleKeypairImport} />
<button @click=${close}>Close</button>
</p>
</article>
</dialog>`;
<button class="secondary" @click=${openSettings}>Back</button>
</p>`;
function importKeys(event: Event): void {
const target = event.target as HTMLElement;
@ -61,11 +52,7 @@ function handleKeypairImport(event: Event): void {
reader.readAsText(file);
}
function close(): void {
isDialogOpen.value = false;
}
export async function openKeyManager() {
export async function initKeyManager() {
await retrieveOrGenerateKeyPair();
ecdhPublicKey.value = localStorage.getItem(
LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY
@ -73,5 +60,5 @@ export async function openKeyManager() {
ecdhPrivateKey.value = localStorage.getItem(
LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY
);
isDialogOpen.value = true;
return true;
}

View File

@ -0,0 +1,135 @@
import { html } from "uhtml";
import { signal } from "uhtml/preactive";
import { API_BASE_URL } from "../../utils/store.ts";
import { retrieveOrGenerateKeyPair } from "../../utils/crypto.ts";
import { openSettings } from "./Settings.ts";
const notificationPermission = signal(Notification.permission);
const pushSubscription = signal<PushSubscription | null>(null);
export const NotificationManager = () => html` <h2>Notifications</h2>
<p>
This is an <mark>experimental feature</mark> and may not work as expected.
It will not work in offline-mode, and requires subscribing to the VAPID Web
Push server at <code>${API_BASE_URL}</code> for it to send Push
Notifications to your device in the background.
</p>
<p>
If enabled, you will receive a notification whenever a user generates a URL
with a message to send to you. Upon clicking on the notification, it will
immediately take you to their generated URL and reveal the message.
</p>
<p>Notifications: <code>${notificationPermission.value}</code></p>
<p>Subscribed: <code>${pushSubscription.value !== null}</code></p>
<p>
<button
@click=${subscribeUserToPush}
?disabled=${notificationPermission.value === "granted"}
>
Enable Notifications
</button>
<button
@click=${unsubscribeUserFromPush}
?disabled=${pushSubscription.value === null}
>
Unsubscribe
</button>
<button class="secondary" @click=${openSettings}>Back</button>
</p>`;
export async function sendNotification(publicKey: CryptoKey, url: URL) {
const exportedKey = await window.crypto.subtle.exportKey("raw", publicKey);
const publicKeyString = btoa(
String.fromCharCode(...new Uint8Array(exportedKey))
);
await fetch(`${API_BASE_URL}api/send-notification`, {
method: "POST",
body: JSON.stringify({
publicKey: publicKeyString,
url: url.toString(),
}),
headers: {
"Content-Type": "application/json",
},
});
}
export async function checkPushSubscription() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
pushSubscription.value = subscription;
}
export async function unsubscribeUserFromPush() {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (!subscription) {
throw new Error("No subscription to unsubscribe");
}
const successful = await subscription.unsubscribe();
if (!successful) {
throw new Error("Failed to unsubscribe");
}
// await fetch(`${API_BASE_URL}api/unsubscribe`, {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({ subscription }),
// });
pushSubscription.value = null;
console.log("User is unsubscribed.");
} catch (err) {
console.error("Failed to unsubscribe the user: ", err);
}
}
export async function subscribeUserToPush(
event: Event | undefined = undefined
) {
event?.preventDefault();
try {
const permission = await Notification.requestPermission();
if (permission !== "granted") {
throw new Error("Notification permission not granted");
}
notificationPermission.value = permission;
const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
const response = await fetch(`${API_BASE_URL}api/public-key`);
const { publicKey: serverPublicKey } = await response.json();
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: serverPublicKey,
});
}
const { publicKey: publicKey } = await retrieveOrGenerateKeyPair();
const exportedKey = await window.crypto.subtle.exportKey("raw", publicKey);
const exportedKeyString = btoa(
String.fromCharCode(...new Uint8Array(exportedKey))
);
await fetch(`${API_BASE_URL}api/subscribe`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ subscription, publicKey: exportedKeyString }),
});
console.log("User is subscribed.");
checkPushSubscription();
} catch (err) {
console.error("Failed to subscribe the user: ", err);
}
}

View File

@ -0,0 +1,56 @@
import { html } from "uhtml";
import { closeModal, openModal } from "../Modal.ts";
import { NotificationManager } from "./NotificationManager.ts";
import { KeyManager, initKeyManager } from "./KeyManager.ts";
import { hasServiceWorkers } from "../../utils/store.ts";
export const Settings = () => html`<h2>Settings</h2>
<section>
<button @click=${openKeyManager}>Manage Keys</button>
<button @click=${openNotificationManager}>Notifications</button>
<button
@click=${deregisterServiceWorkers}
?disabled=${!hasServiceWorkers.value}
>
Clear Cache
</button>
<button class="secondary" @click=${closeModal}>Close</button>
</section>`;
export function openSettings(event: Event) {
event.preventDefault;
openModal(Settings);
}
async function openKeyManager(event: Event) {
event.preventDefault();
await initKeyManager();
openModal(KeyManager);
}
async function openNotificationManager(event: Event) {
event.preventDefault();
openModal(NotificationManager);
}
export async function checkServiceWorkers() {
if ("serviceWorker" in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
hasServiceWorkers.value = registrations.length > 0;
console.log("Service Workers registered:", hasServiceWorkers.value);
}
}
export async function deregisterServiceWorkers(event: Event) {
event.preventDefault();
if ("serviceWorker" in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(
registrations.map((registration) => registration.unregister())
);
}
await checkServiceWorkers();
alert(
"Service Workers deregistered! If you think you're not seeing the latest version of the app, try refreshing the page."
);
}

View File

@ -1,19 +1,20 @@
import "./style.scss";
import { KeyManager } from "./template/keyManager.ts";
import { Header } from "./template/header.ts";
import { page } from "./utils/store.ts";
import { Header } from "./components/Header.ts";
import { html } from "uhtml";
import { effect } from "uhtml/preactive";
import { reactive } from "uhtml/reactive";
import { Footer } from "./template/footer.ts";
import { HowItWorks } from "./template/howitworks.ts";
import { FAQ } from "./template/faq.ts";
import { page } from "./utils/store.ts";
import { Footer } from "./components/Footer.ts";
import { HowItWorks } from "./components/HowItWorks.ts";
import { FAQ } from "./components/FAQ.ts";
import { routeToPage, hashChange } from "./router.ts";
import { UpdateModal, checkForUpdates } from "./template/update.ts";
import { UpdateModal, checkForUpdates } from "./components/Update.ts";
import { subscribeUserToPush } from "./components/settings/NotificationManager.ts";
import { AppModal } from "./components/Modal.ts";
try {
const render = reactive(effect);
checkForUpdates();
const template = () => html` ${KeyManager()} ${UpdateModal()}
const template = () => html` ${AppModal()} ${UpdateModal()}
<main class="container">
<section>${routeToPage(page.value)}</section>
<div>${HowItWorks}</div>
@ -26,9 +27,14 @@ window.addEventListener("hashchange", hashChange);
// Initialize the app
hashChange();
checkForUpdates();
const headerElement = document.getElementById("header");
render(headerElement, Header);
const appElement = document.getElementById("page");
render(appElement, template);
if (Notification.permission === "granted") subscribeUserToPush();
} catch (error) {
console.error(error);
}

View File

@ -4,7 +4,7 @@ import { signal } from "uhtml/preactive";
const URL_INPUT_ID = "request-url";
const COPY_BUTTON_ID = "copy-button";
const requestUrl = signal(new URL("https://sure.dog/"));
const requestUrl = signal<undefined | URL>(undefined);
export function RequestPage() {
return html`
@ -22,7 +22,7 @@ export function RequestPage() {
type="text"
name="request-url"
.id=${URL_INPUT_ID}
.value=${requestUrl.toString()}
.value=${requestUrl && requestUrl.toString()}
placeholder=""
aria-label="Request URL"
required
@ -31,7 +31,7 @@ export function RequestPage() {
aria-label="Copy to Clipboard"
.id=${COPY_BUTTON_ID}
@click=${(event: Event) =>
copyToClipboard(event, requestUrl.toString())}
copyToClipboard(event, requestUrl && requestUrl.toString())}
>
Copy to Clipboard
</button>
@ -40,16 +40,27 @@ export function RequestPage() {
}
async function copyToClipboard(event: Event, url: string) {
try {
event.preventDefault();
await navigator.clipboard.writeText(url);
alert("Copied to clipboard!");
} catch (err) {
console.error("Failed to copy to clipboard: ", err);
}
}
export async function generateRequestUrl() {
try {
const { publicKey: ecdhPublic } = await retrieveOrGenerateKeyPair();
const ecdhPublicJwk = await window.crypto.subtle.exportKey("jwk", ecdhPublic);
const ecdhPublicJwk = await window.crypto.subtle.exportKey(
"jwk",
ecdhPublic
);
const updatedUrl = new URL(window.location.toString());
updatedUrl.search = "";
updatedUrl.hash = `p=${btoa(JSON.stringify(ecdhPublicJwk))}`;
requestUrl.value = updatedUrl;
} catch (err) {
console.error("Failed to generate request URL: ", err);
}
}

View File

@ -6,6 +6,7 @@ import {
} from "../utils/crypto.ts";
import { signal } from "uhtml/preactive";
import { LOCAL_STORAGE_KEYS } from "../utils/store.ts";
import { sendNotification } from "../components/settings/NotificationManager.ts";
const messageToEncrypt = signal("");
const encryptedUrl = signal("");
@ -71,7 +72,9 @@ export const SendPage = () => {
async function copyToClipboard(event: Event) {
event.preventDefault();
await navigator.clipboard.writeText(encryptedUrl.value);
alert("Copied to clipboard! Send this URL to the requester.");
alert(
"Copied to clipboard! Send this URL to the requester, or they will be notified via a Push Notification if they have it enabled."
);
}
async function encryptData(event: Event) {
@ -89,7 +92,7 @@ async function encryptData(event: Event) {
name: "ECDH",
namedCurve: "P-256",
},
false,
true,
[]
);
@ -117,6 +120,8 @@ async function encryptData(event: Event) {
)
)}`;
sendNotification(publicA, url);
encryptedUrl.value = url.toString();
isCopyButtonDisabled.value = false;
}

41
src/sw.js Normal file
View File

@ -0,0 +1,41 @@
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
// This array will be injected with the actual file manifest
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
// Receive push notifications
self.addEventListener("push", function (e) {
if (!(self.Notification && self.Notification.permission === "granted")) {
//notifications aren't supported or permission not granted!
return;
}
if (e.data) {
let message = e.data.json();
e.waitUntil(
self.registration.showNotification("SURE DOG", {
body: message.body,
icon: "img/logo.png",
data: message.data,
})
);
}
});
self.addEventListener("notificationclick", function (event) {
event.notification.close();
var url = event.notification.data.url;
event.waitUntil(
clients
.openWindow(event.notification.data.url)
.catch((err) => console.log(err))
);
});
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});

View File

@ -1,52 +0,0 @@
import { html } from "uhtml";
import { signal } from "uhtml/preactive";
import { registerSW } from "virtual:pwa-register";
const open = signal(false);
const updateSW = signal<
((reloadPage?: boolean | undefined) => Promise<void>) | undefined
>(undefined);
export const UpdateModal = () => {
return html` <dialog ?open=${open.value}>
<article>
<header>An update is available!</header>
<p>
A new version of the app is available. Click the button below to update
and reload.
</p>
<footer>
<button aria-label="Update and reload" @click=${update}>Update</button>
</footer>
</article>
</dialog>`;
};
const update = (event: Event) => {
event.preventDefault();
if (updateSW.value) {
console.log("Service Worker is updating.");
updateSW.value(true);
} else {
console.error("Service Worker failed to update.");
}
};
export const checkForUpdates = () => {
if ("serviceWorker" in navigator) {
const promise = registerSW({
onNeedRefresh() {
console.log("Service Worker is ready to update.");
open.value = true;
},
onRegisteredSW() {
console.log("Service Worker has been registered.");
},
onOfflineReady() {
console.log("Service Worker is ready to handle offline usage.");
},
});
updateSW.value = promise;
}
};

View File

@ -1,6 +1,13 @@
import { signal } from "uhtml/preactive";
import { Route } from "../router";
export const API_BASE_URL = new URL(
import.meta.env.VITE_API_BASE_URL || "http://localhost:3000"
);
export const BASE_URL = new URL(
import.meta.env.VITE_BASE_URL || "http://localhost:3000"
);
// the current page / params for this URL
export const page = signal(Route.Receive);
export const params = signal(
@ -12,3 +19,5 @@ export enum LOCAL_STORAGE_KEYS {
ECDH_PUBLIC_KEY = "ecdhPublic",
ECDH_PRIVATE_KEY = "ecdhPrivate",
}
export const hasServiceWorkers = signal(false);

View File

@ -1 +1,6 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
VITE_BASE_URL?: string;
VITE_API_BASE_URL?: string;
}

View File

@ -1,18 +1,58 @@
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { version } from "./package.json";
import { VitePWA } from "vite-plugin-pwa";
const isProduction = process.env.NODE_ENV === "production";
const API_BASE_URL = new URL(
process.env.VITE_API_BASE_URL || "http://localhost:3000"
);
const API_BASE_URL_SECURE = API_BASE_URL.protocol === "https:";
const BASE_URL = new URL(process.env.VITE_BASE_URL || "http://localhost:5137");
const BASE_URL_SECURE = BASE_URL.protocol === "https:";
export default defineConfig({
server: {
proxy: {
"/ws": {
target: API_BASE_URL_SECURE
? `wss://${API_BASE_URL.hostname}`
: `ws://${API_BASE_URL.hostname}`,
ws: true,
changeOrigin: true,
secure: isProduction,
},
"/api": {
target: BASE_URL_SECURE
? `https://${BASE_URL.hostname}`
: `http://${BASE_URL.hostname}`,
changeOrigin: true,
secure: isProduction,
},
},
},
define: {
"process.env.PACKAGE_VERSION": JSON.stringify(version),
"process.env.BUILD_TIME": JSON.stringify(new Date().toISOString()),
},
publicDir: "public",
plugins: [
VitePWA({
devOptions: {
enabled: true,
srcDir: "src",
filename: "sw.js",
registerType: "prompt",
includeAssets: [
"**/*.png",
"**/*.jpg",
"**/*.jpeg",
"**/*.svg",
"**/*.gif",
],
strategies: "injectManifest",
workbox: {
globPatterns: ["**/*"],
globDirectory: "dist",
},
includeAssets: ["**/*.png", "**/*.ico", "**/*.webmanifest"],
manifest: {
name: "SURE",
short_name: "SURE",

View File

@ -1,35 +1,6 @@
import { defineConfig } from "vite";
import { makeOffline } from "vite-plugin-make-offline";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
makeOffline(),
VitePWA({
devOptions: {
enabled: true,
},
manifest: {
name: "SURE",
short_name: "SURE",
theme_color: "#44a616",
background_color: "#3d006e",
display: "minimal-ui",
scope: "/",
start_url: "/",
icons: [
{
src: "/android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
},
}),
],
plugins: [makeOffline()],
});