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:
parent
c3db0e6002
commit
c13003cd6f
|
@ -0,0 +1,2 @@
|
||||||
|
API_BASE_URL=https://yeah.sure.dog
|
||||||
|
BASE_URL=https://sure.dog
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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 name="description" content="Securely request information via SURE links" />
|
||||||
<meta property="og:title" content="SURE - Secure URL Requests" />
|
<meta property="og:title" content="SURE - Secure URL Requests" />
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
{
|
{
|
||||||
"name": "sure",
|
"name": "sure",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sure",
|
"name": "sure",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@picocss/pico": "^1.5.11",
|
"@picocss/pico": "^1.5.11",
|
||||||
"uhtml": "^4.4.7"
|
"uhtml": "^4.4.7",
|
||||||
|
"workbox-window": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/workbox-precaching": "^5.0.0",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.70.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.0.8",
|
"vite": "^5.0.8",
|
||||||
"vite-plugin-make-offline": "^1.0.0",
|
"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": {
|
"node_modules/@ampproject/remapping": {
|
||||||
|
@ -2387,8 +2390,17 @@
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||||
"dev": true
|
},
|
||||||
|
"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": {
|
"node_modules/@webreflection/signal": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
@ -4212,12 +4224,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
"version": "6.1.1",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||||
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
|
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.13.1 || >=16.0.0"
|
"node": ">=6"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
@ -5153,6 +5165,18 @@
|
||||||
"workbox-window": "^7.0.0"
|
"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": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||||
|
@ -5351,18 +5375,6 @@
|
||||||
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
|
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/workbox-build/node_modules/rollup": {
|
||||||
"version": "2.79.1",
|
"version": "2.79.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||||
|
@ -5406,8 +5418,7 @@
|
||||||
"node_modules/workbox-core": {
|
"node_modules/workbox-core": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz",
|
||||||
"integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==",
|
"integrity": "sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/workbox-expiration": {
|
"node_modules/workbox-expiration": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
@ -5512,7 +5523,6 @@
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.0.0.tgz",
|
||||||
"integrity": "sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==",
|
"integrity": "sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/trusted-types": "^2.0.2",
|
"@types/trusted-types": "^2.0.2",
|
||||||
"workbox-core": "7.0.0"
|
"workbox-core": "7.0.0"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "sure",
|
"name": "sure",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -10,14 +10,17 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/workbox-precaching": "^5.0.0",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.70.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.0.8",
|
"vite": "^5.0.8",
|
||||||
"vite-plugin-make-offline": "^1.0.0",
|
"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": {
|
"dependencies": {
|
||||||
"@picocss/pico": "^1.5.11",
|
"@picocss/pico": "^1.5.11",
|
||||||
"uhtml": "^4.4.7"
|
"uhtml": "^4.4.7",
|
||||||
|
"workbox-window": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { html } from "uhtml";
|
import { html } from "uhtml";
|
||||||
import { openKeyManager } from "./keyManager.ts";
|
import { openSettings } from "./settings/Settings";
|
||||||
|
|
||||||
export const Footer = html`<footer class="container">
|
export const Footer = html`<footer class="container">
|
||||||
<div>
|
<div>
|
||||||
|
@ -18,15 +18,7 @@ export const Footer = html`<footer class="container">
|
||||||
>whoami</a
|
>whoami</a
|
||||||
>
|
>
|
||||||
|
|
|
|
||||||
<a
|
<a href="#" @click=${openSettings}>Settings</a>
|
||||||
href="#"
|
|
||||||
id="openKeyManager"
|
|
||||||
@click=${(event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
openKeyManager();
|
|
||||||
}}
|
|
||||||
>Manage Keys</a
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="center">Version ${process.env.PACKAGE_VERSION}</p>
|
<p class="center">Version ${process.env.PACKAGE_VERSION}</p>
|
|
@ -1,7 +1,7 @@
|
||||||
import { html } from "uhtml";
|
import { html } from "uhtml";
|
||||||
|
|
||||||
export const Header = html` <hgroup>
|
export const Header = html` <hgroup>
|
||||||
<h1>SURE</h1>
|
<h1>SURE<span class="primary">DOG</span></h1>
|
||||||
<h2>
|
<h2>
|
||||||
<span class="primary">S</span>ecure <span class="primary">U</span>RL
|
<span class="primary">S</span>ecure <span class="primary">U</span>RL
|
||||||
<span class="primary">Re</span>quests
|
<span class="primary">Re</span>quests
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -3,25 +3,18 @@ import {
|
||||||
exportKeys,
|
exportKeys,
|
||||||
importAndSaveKeys,
|
importAndSaveKeys,
|
||||||
retrieveOrGenerateKeyPair,
|
retrieveOrGenerateKeyPair,
|
||||||
} from "../utils/crypto.ts";
|
} from "../../utils/crypto.ts";
|
||||||
import { signal } from "uhtml/preactive";
|
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(
|
const ecdhPublicKey = signal<string | null>(null);
|
||||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY)
|
const ecdhPrivateKey = signal<string | null>(null);
|
||||||
);
|
|
||||||
const ecdhPrivateKey = signal(
|
|
||||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY)
|
|
||||||
);
|
|
||||||
const isDialogOpen = signal(false);
|
|
||||||
|
|
||||||
export const KeyManager =
|
export const KeyManager = () => html`
|
||||||
() => html`<dialog class="key-manager" ?open=${isDialogOpen.value}>
|
<h2>Key Manager</h2>
|
||||||
<article>
|
|
||||||
<header>Key Manager</header>
|
|
||||||
<p>
|
<p>
|
||||||
If you'd like to manage your keys, you can do so here. You can export your keys to transfer to a different
|
You can export your keys to transfer to a different device, or import keys exported from a different device.
|
||||||
device, or import keys exported from a different device.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Public Key:
|
Public Key:
|
||||||
|
@ -35,10 +28,8 @@ export const KeyManager =
|
||||||
<button @click=${exportKeys}>Export Keys</button>
|
<button @click=${exportKeys}>Export Keys</button>
|
||||||
<button @click=${importKeys}>Import Keys</button>
|
<button @click=${importKeys}>Import Keys</button>
|
||||||
<input type="file" accept=".json" style="display: none" id="keypairImportEl" @change=${handleKeypairImport} />
|
<input type="file" accept=".json" style="display: none" id="keypairImportEl" @change=${handleKeypairImport} />
|
||||||
<button @click=${close}>Close</button>
|
<button class="secondary" @click=${openSettings}>Back</button>
|
||||||
</p>
|
</p>`;
|
||||||
</article>
|
|
||||||
</dialog>`;
|
|
||||||
|
|
||||||
function importKeys(event: Event): void {
|
function importKeys(event: Event): void {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
|
@ -61,11 +52,7 @@ function handleKeypairImport(event: Event): void {
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): void {
|
export async function initKeyManager() {
|
||||||
isDialogOpen.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openKeyManager() {
|
|
||||||
await retrieveOrGenerateKeyPair();
|
await retrieveOrGenerateKeyPair();
|
||||||
ecdhPublicKey.value = localStorage.getItem(
|
ecdhPublicKey.value = localStorage.getItem(
|
||||||
LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY
|
LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY
|
||||||
|
@ -73,5 +60,5 @@ export async function openKeyManager() {
|
||||||
ecdhPrivateKey.value = localStorage.getItem(
|
ecdhPrivateKey.value = localStorage.getItem(
|
||||||
LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY
|
LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY
|
||||||
);
|
);
|
||||||
isDialogOpen.value = true;
|
return true;
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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."
|
||||||
|
);
|
||||||
|
}
|
54
src/main.ts
54
src/main.ts
|
@ -1,34 +1,40 @@
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { KeyManager } from "./template/keyManager.ts";
|
import { page } from "./utils/store.ts";
|
||||||
import { Header } from "./template/header.ts";
|
import { Header } from "./components/Header.ts";
|
||||||
import { html } from "uhtml";
|
import { html } from "uhtml";
|
||||||
import { effect } from "uhtml/preactive";
|
import { effect } from "uhtml/preactive";
|
||||||
import { reactive } from "uhtml/reactive";
|
import { reactive } from "uhtml/reactive";
|
||||||
import { Footer } from "./template/footer.ts";
|
import { Footer } from "./components/Footer.ts";
|
||||||
import { HowItWorks } from "./template/howitworks.ts";
|
import { HowItWorks } from "./components/HowItWorks.ts";
|
||||||
import { FAQ } from "./template/faq.ts";
|
import { FAQ } from "./components/FAQ.ts";
|
||||||
import { page } from "./utils/store.ts";
|
|
||||||
import { routeToPage, hashChange } from "./router.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";
|
||||||
|
|
||||||
const render = reactive(effect);
|
try {
|
||||||
checkForUpdates();
|
const render = reactive(effect);
|
||||||
const template = () => html` ${KeyManager()} ${UpdateModal()}
|
const template = () => html` ${AppModal()} ${UpdateModal()}
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<section>${routeToPage(page.value)}</section>
|
<section>${routeToPage(page.value)}</section>
|
||||||
<div>${HowItWorks}</div>
|
<div>${HowItWorks}</div>
|
||||||
<div>${FAQ}</div>
|
<div>${FAQ}</div>
|
||||||
</main>
|
</main>
|
||||||
${Footer}`;
|
${Footer}`;
|
||||||
|
|
||||||
// Listen for changes in the hash and update the state accordingly
|
// Listen for changes in the hash and update the state accordingly
|
||||||
window.addEventListener("hashchange", hashChange);
|
window.addEventListener("hashchange", hashChange);
|
||||||
|
|
||||||
// Initialize the app
|
// Initialize the app
|
||||||
hashChange();
|
hashChange();
|
||||||
|
checkForUpdates();
|
||||||
|
const headerElement = document.getElementById("header");
|
||||||
|
render(headerElement, Header);
|
||||||
|
|
||||||
const headerElement = document.getElementById("header");
|
const appElement = document.getElementById("page");
|
||||||
render(headerElement, Header);
|
render(appElement, template);
|
||||||
|
|
||||||
const appElement = document.getElementById("page");
|
if (Notification.permission === "granted") subscribeUserToPush();
|
||||||
render(appElement, template);
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { signal } from "uhtml/preactive";
|
||||||
|
|
||||||
const URL_INPUT_ID = "request-url";
|
const URL_INPUT_ID = "request-url";
|
||||||
const COPY_BUTTON_ID = "copy-button";
|
const COPY_BUTTON_ID = "copy-button";
|
||||||
const requestUrl = signal(new URL("https://sure.dog/"));
|
const requestUrl = signal<undefined | URL>(undefined);
|
||||||
|
|
||||||
export function RequestPage() {
|
export function RequestPage() {
|
||||||
return html`
|
return html`
|
||||||
|
@ -22,7 +22,7 @@ export function RequestPage() {
|
||||||
type="text"
|
type="text"
|
||||||
name="request-url"
|
name="request-url"
|
||||||
.id=${URL_INPUT_ID}
|
.id=${URL_INPUT_ID}
|
||||||
.value=${requestUrl.toString()}
|
.value=${requestUrl && requestUrl.toString()}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
aria-label="Request URL"
|
aria-label="Request URL"
|
||||||
required
|
required
|
||||||
|
@ -31,7 +31,7 @@ export function RequestPage() {
|
||||||
aria-label="Copy to Clipboard"
|
aria-label="Copy to Clipboard"
|
||||||
.id=${COPY_BUTTON_ID}
|
.id=${COPY_BUTTON_ID}
|
||||||
@click=${(event: Event) =>
|
@click=${(event: Event) =>
|
||||||
copyToClipboard(event, requestUrl.toString())}
|
copyToClipboard(event, requestUrl && requestUrl.toString())}
|
||||||
>
|
>
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
</button>
|
</button>
|
||||||
|
@ -40,16 +40,27 @@ export function RequestPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(event: Event, url: string) {
|
async function copyToClipboard(event: Event, url: string) {
|
||||||
event.preventDefault();
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
event.preventDefault();
|
||||||
alert("Copied to clipboard!");
|
await navigator.clipboard.writeText(url);
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy to clipboard: ", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateRequestUrl() {
|
export async function generateRequestUrl() {
|
||||||
const { publicKey: ecdhPublic } = await retrieveOrGenerateKeyPair();
|
try {
|
||||||
const ecdhPublicJwk = await window.crypto.subtle.exportKey("jwk", ecdhPublic);
|
const { publicKey: ecdhPublic } = await retrieveOrGenerateKeyPair();
|
||||||
const updatedUrl = new URL(window.location.toString());
|
const ecdhPublicJwk = await window.crypto.subtle.exportKey(
|
||||||
updatedUrl.search = "";
|
"jwk",
|
||||||
updatedUrl.hash = `p=${btoa(JSON.stringify(ecdhPublicJwk))}`;
|
ecdhPublic
|
||||||
requestUrl.value = updatedUrl;
|
);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "../utils/crypto.ts";
|
} from "../utils/crypto.ts";
|
||||||
import { signal } from "uhtml/preactive";
|
import { signal } from "uhtml/preactive";
|
||||||
import { LOCAL_STORAGE_KEYS } from "../utils/store.ts";
|
import { LOCAL_STORAGE_KEYS } from "../utils/store.ts";
|
||||||
|
import { sendNotification } from "../components/settings/NotificationManager.ts";
|
||||||
|
|
||||||
const messageToEncrypt = signal("");
|
const messageToEncrypt = signal("");
|
||||||
const encryptedUrl = signal("");
|
const encryptedUrl = signal("");
|
||||||
|
@ -71,7 +72,9 @@ export const SendPage = () => {
|
||||||
async function copyToClipboard(event: Event) {
|
async function copyToClipboard(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await navigator.clipboard.writeText(encryptedUrl.value);
|
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) {
|
async function encryptData(event: Event) {
|
||||||
|
@ -89,7 +92,7 @@ async function encryptData(event: Event) {
|
||||||
name: "ECDH",
|
name: "ECDH",
|
||||||
namedCurve: "P-256",
|
namedCurve: "P-256",
|
||||||
},
|
},
|
||||||
false,
|
true,
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -117,6 +120,8 @@ async function encryptData(event: Event) {
|
||||||
)
|
)
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
|
sendNotification(publicA, url);
|
||||||
|
|
||||||
encryptedUrl.value = url.toString();
|
encryptedUrl.value = url.toString();
|
||||||
isCopyButtonDisabled.value = false;
|
isCopyButtonDisabled.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { signal } from "uhtml/preactive";
|
import { signal } from "uhtml/preactive";
|
||||||
import { Route } from "../router";
|
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
|
// the current page / params for this URL
|
||||||
export const page = signal(Route.Receive);
|
export const page = signal(Route.Receive);
|
||||||
export const params = signal(
|
export const params = signal(
|
||||||
|
@ -12,3 +19,5 @@ export enum LOCAL_STORAGE_KEYS {
|
||||||
ECDH_PUBLIC_KEY = "ecdhPublic",
|
ECDH_PUBLIC_KEY = "ecdhPublic",
|
||||||
ECDH_PRIVATE_KEY = "ecdhPrivate",
|
ECDH_PRIVATE_KEY = "ecdhPrivate",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const hasServiceWorkers = signal(false);
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
VITE_BASE_URL?: string;
|
||||||
|
VITE_API_BASE_URL?: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,58 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
|
||||||
import { version } from "./package.json";
|
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({
|
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: {
|
define: {
|
||||||
"process.env.PACKAGE_VERSION": JSON.stringify(version),
|
"process.env.PACKAGE_VERSION": JSON.stringify(version),
|
||||||
"process.env.BUILD_TIME": JSON.stringify(new Date().toISOString()),
|
"process.env.BUILD_TIME": JSON.stringify(new Date().toISOString()),
|
||||||
},
|
},
|
||||||
|
publicDir: "public",
|
||||||
plugins: [
|
plugins: [
|
||||||
VitePWA({
|
VitePWA({
|
||||||
devOptions: {
|
srcDir: "src",
|
||||||
enabled: true,
|
filename: "sw.js",
|
||||||
|
registerType: "prompt",
|
||||||
|
includeAssets: [
|
||||||
|
"**/*.png",
|
||||||
|
"**/*.jpg",
|
||||||
|
"**/*.jpeg",
|
||||||
|
"**/*.svg",
|
||||||
|
"**/*.gif",
|
||||||
|
],
|
||||||
|
strategies: "injectManifest",
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ["**/*"],
|
||||||
|
globDirectory: "dist",
|
||||||
},
|
},
|
||||||
includeAssets: ["**/*.png", "**/*.ico", "**/*.webmanifest"],
|
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "SURE",
|
name: "SURE",
|
||||||
short_name: "SURE",
|
short_name: "SURE",
|
||||||
|
|
|
@ -1,35 +1,6 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { makeOffline } from "vite-plugin-make-offline";
|
import { makeOffline } from "vite-plugin-make-offline";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [makeOffline()],
|
||||||
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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue