rastrillo / aviso Public

Example: keep the key, gate the buttons, ship the icons

Codex's review of the example: its run line minted a fresh key on
every start while reusing the database, stranding every enrolled
browser — the instructions now mint once and reuse. The buttons were
live before their handlers existed, so an early click did nothing;
they start disabled and are enabled once the worker is active and the
key fetched, and stay disabled with a logged reason on a browser
without push. The manifest had no icons and the page no Apple touch
icon, so an author copying it would not get the installability recipe
the docs describe; 192, 512 and 180 px PNGs are served and referenced,
and the test checks the routes and the page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev bb867e21dd5d709d3bdbbb78cbf63519b426fffa parent 3bf45d1
8 files changed, +42 −12
  • example/index.html +7 −4
  • example/main.go +6 −1
  • example/main_test.go +15 −1
  • example/static/app.js +10 −5
  • example/static/icon-180.png binary
  • example/static/icon-192.png binary
  • example/static/icon-512.png binary
  • example/static/manifest.webmanifest +4 −1
diff --git a/example/index.html b/example/index.html
index e8ae234..895ce32 100644
--- a/example/index.html
+++ b/example/index.html
@@ -6,6 +6,7 @@
<title>aviso example</title>
<link rel="manifest" href="/static/manifest.webmanifest">
<meta name="theme-color" content="#5b6cff">
+ <link rel="apple-touch-icon" href="/static/icon-180.png">
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 2rem auto; max-width: 40rem; padding: 0 1rem; }
button { font: inherit; padding: .5rem 1rem; margin-right: .5rem; }
@@ -16,11 +17,13 @@
<body>
<h1>aviso example</h1>
<p>Everyone here is signed in as <code>dev</code>. Enable push, then notify yourself.</p>
- <p id="coach">On a phone, add this app to your Home Screen and open it from there first — push is only delivered to an installed app.</p>
+ <p id="coach">On an iPhone or iPad, add this app to your Home Screen (Share → Add to Home Screen) and open it from there first — iOS delivers push only to an installed app.</p>
<p>
- <button id="enable">Enable notifications</button>
- <button id="disable">Disable</button>
- <button id="notify">Notify me</button>
+ <!-- Disabled until the worker is active and the key is fetched:
+ a click before then would do nothing, silently. -->
+ <button id="enable" disabled>Enable notifications</button>
+ <button id="disable" disabled>Disable</button>
+ <button id="notify" disabled>Notify me</button>
</p>
<pre id="log"></pre>
<script type="module" src="/static/app.js"></script>
diff --git a/example/main.go b/example/main.go
index 8d72e2c..94aa50c 100644
--- a/example/main.go
+++ b/example/main.go
@@ -4,7 +4,12 @@
// "dev" — an example, not a pattern — so the enrol/send loop can be
// driven from one browser.
//
-// EXAMPLE_VAPID_PRIVATE_KEY="$(go run amadan.net/rastrillo/aviso/cmd/aviso-key)" go run .
+// Mint the key once and keep it — a subscription is bound to the key
+// it was made under, so a fresh key on every start would strand every
+// enrolled browser until it re-enrols (aviso.ErrKeyMismatch on send):
+//
+// go run amadan.net/rastrillo/aviso/cmd/aviso-key > .vapid-key # once
+// EXAMPLE_VAPID_PRIVATE_KEY="$(cat .vapid-key)" go run . # every start
//
// Push needs a secure context: http://localhost is one, so the default
// origin works locally without TLS.
diff --git a/example/main_test.go b/example/main_test.go
index 95e8522..891e1c8 100644
--- a/example/main_test.go
+++ b/example/main_test.go
@@ -55,7 +55,7 @@ func TestEverySeamIsWired(t *testing.T) {
"/static/aviso/aviso-sw.js": "AvisoSW",
"/sw.js": "importScripts(\"/static/aviso/aviso-sw.js\")",
"/aviso/public-key": "publicKey",
- "/static/manifest.webmanifest": "\"display\": \"standalone\"",
+ "/static/manifest.webmanifest": "/static/icon-512.png",
} {
w := get(t, h, path)
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), want) {
@@ -72,6 +72,20 @@ func TestEverySeamIsWired(t *testing.T) {
t.Errorf("%s Content-Type = %q", path, ct)
}
}
+ // The installability recipe, modelled: manifest icons at 192 and
+ // 512, an Apple touch icon at 180 linked from the page, and
+ // buttons that start disabled until the page can act on a click.
+ for _, path := range []string{"/static/icon-192.png", "/static/icon-512.png", "/static/icon-180.png"} {
+ if w := get(t, h, path); w.Code != http.StatusOK || !strings.HasPrefix(w.Header().Get("Content-Type"), "image/png") {
+ t.Errorf("%s: %d %q", path, w.Code, w.Header().Get("Content-Type"))
+ }
+ }
+ page := get(t, h, "/").Body.String()
+ for _, want := range []string{`rel="apple-touch-icon" href="/static/icon-180.png"`, `rel="manifest"`, `id="enable" disabled`, `id="notify" disabled`} {
+ if !strings.Contains(page, want) {
+ t.Errorf("index.html lacks %q", want)
+ }
+ }
k, _ := ecdh.P256().GenerateKey(rand.Reader)
auth := make([]byte, 16)
diff --git a/example/static/app.js b/example/static/app.js
index bb02443..3133e22 100644
--- a/example/static/app.js
+++ b/example/static/app.js
@@ -1,7 +1,11 @@
-// The page half, as SKILL.md shows it.
+// The page half, as SKILL.md shows it. The buttons start disabled and
+// are enabled only once they can do something: after the worker is
+// active and the key is fetched. On a browser without push they stay
+// disabled, with the reason logged, rather than sitting there inert.
import { enable, reconcile, disable, capabilities, status } from "/static/aviso/push.mjs";
const log = (line) => { document.getElementById("log").textContent += line + "\n"; };
+const button = (id) => document.getElementById(id);
const post = (path) => (body) => fetch(path, {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
@@ -15,7 +19,7 @@ if (!caps.standalone && /iPhone|iPad/.test(navigator.userAgent)) {
document.getElementById("coach").style.display = "block";
}
if (!caps.serviceWorker || !caps.push) {
- log("this browser cannot do push");
+ log("this browser cannot do push; the buttons stay disabled");
} else {
await navigator.serviceWorker.register("/sw.js");
// Active, not merely registered: subscribe() on an installing
@@ -29,18 +33,19 @@ if (!caps.serviceWorker || !caps.push) {
log("subscribed: " + !!repaired);
// From the click, synchronously: enable prompts before its first await.
- document.getElementById("enable").onclick = () => {
+ button("enable").onclick = () => {
enable({ registration, publicKey, save })
.then((sub) => log(sub ? "enabled: " + sub.endpoint.slice(0, 40) + "…" : "permission denied"))
.catch((e) => log("enable: " + e.message));
};
- document.getElementById("disable").onclick = async () => {
+ button("disable").onclick = async () => {
try { await disable({ registration, remove }); log("disabled"); } catch (e) { log("disable: " + e.message); }
};
- document.getElementById("notify").onclick = async () => {
+ button("notify").onclick = async () => {
const r = await fetch("/notify", { method: "POST", credentials: "same-origin" });
log("notify: " + r.status + " " + await r.text());
};
+ for (const id of ["enable", "disable", "notify"]) button(id).disabled = false;
const s = await status(registration);
log("permission: " + s.permission);
}
diff --git a/example/static/icon-180.png b/example/static/icon-180.png
new file mode 100644
index 0000000..c324633
Binary files /dev/null and b/example/static/icon-180.png differ
diff --git a/example/static/icon-192.png b/example/static/icon-192.png
new file mode 100644
index 0000000..757149e
Binary files /dev/null and b/example/static/icon-192.png differ
diff --git a/example/static/icon-512.png b/example/static/icon-512.png
new file mode 100644
index 0000000..3237518
Binary files /dev/null and b/example/static/icon-512.png differ
diff --git a/example/static/manifest.webmanifest b/example/static/manifest.webmanifest
index bdbb5de..8a7cd8c 100644
--- a/example/static/manifest.webmanifest
+++ b/example/static/manifest.webmanifest
@@ -7,5 +7,8 @@
"display": "standalone",
"theme_color": "#5b6cff",
"background_color": "#f6f7fb",
- "icons": []
+ "icons": [
+ { "src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png" },
+ { "src": "/static/icon-512.png", "sizes": "512x512", "type": "image/png" }
+ ]
}