Compare commits

...

28 Commits

Author SHA1 Message Date
Jeff a534a6c761 WIP, mostly just noodling
Test / test (push) Failing after 57s Details
2025-03-15 12:52:51 -04:00
Jeff c684e2e31c Add json content type
Test / test (push) Failing after 53s Details
2024-10-14 15:30:39 -04:00
Jeff f3e3077374 Log on service worker end
Test / test (push) Failing after 54s Details
2024-10-14 15:11:18 -04:00
Jeff 4f0e68be41 Log subscription
Test / test (push) Failing after 52s Details
2024-10-14 15:08:17 -04:00
Jeff 65185a4e9f Try converting subscription to json first
Test / test (push) Failing after 53s Details
2024-10-14 15:05:09 -04:00
Jeff b966b30c06 Add pwa build variables
Test / test (push) Failing after 32s Details
2024-10-14 14:59:04 -04:00
Jeff 43ee91c8e4 Remove window. in b64ToUInt8
Test / test (push) Failing after 55s Details
2024-10-13 00:21:41 -04:00
Jeff cc7faccecc Narrow further
Test / test (push) Failing after 53s Details
2024-10-13 00:19:25 -04:00
Jeff 478cd10022 Even more logs
Test / test (push) Failing after 56s Details
2024-10-13 00:16:01 -04:00
Jeff 5b5c8fa847 Log even more garbage
Test / test (push) Failing after 54s Details
2024-10-12 17:46:29 -04:00
Jeff b816cb56da Log even more garbage
Test / test (push) Failing after 54s Details
2024-10-12 17:43:30 -04:00
Jeff 121cc90de9 Log even more garbage
Test / test (push) Failing after 54s Details
2024-10-12 17:40:06 -04:00
Jeff 3ff16c58b2 Log more, try adding /
Test / test (push) Failing after 52s Details
2024-10-12 17:30:53 -04:00
Jeff 00876f08e9 Log what the stupid event data is
Test / test (push) Failing after 52s Details
2024-10-12 17:27:34 -04:00
Jeff fc043461a2 Use event.data instead of event in service worker subscription logic
Test / test (push) Failing after 53s Details
2024-10-12 17:18:15 -04:00
Jeff e3a0b6e730 Register for messages on the serviceWorker property I guess
Test / test (push) Failing after 53s Details
2024-10-12 17:13:45 -04:00
Jeff b92750e5d5 Add more log
Test / test (push) Failing after 30s Details
2024-10-12 17:06:34 -04:00
Jeff a07f028b75 Add even more logging
Test / test (push) Failing after 57s Details
2024-10-12 16:58:54 -04:00
Jeff 5741b9fee2 Fix jsonification of message, try sending reply immediately upon receiving message event
Test / test (push) Failing after 54s Details
2024-10-12 15:08:40 -04:00
Jeff 16250ef533 Fix error in render method
Test / test (push) Failing after 53s Details
2024-10-12 14:47:12 -04:00
Jeff 1d3edec7bc Actually display messages
Test / test (push) Failing after 52s Details
2024-10-12 14:44:17 -04:00
Jeff 07d638b66c Send debug messages back to client
Test / test (push) Failing after 52s Details
2024-10-12 14:38:35 -04:00
Jeff 4a7d752230 Convert subscription to JSON
Test / test (push) Failing after 55s Details
2024-10-12 14:07:13 -04:00
Jeff 0c3e6f60b1 Catch errors in subscribe callback
Test / test (push) Failing after 54s Details
2024-10-12 14:03:22 -04:00
Jeff d655492d16 Show error when enabling notifications
Test / test (push) Failing after 54s Details
2024-10-12 13:58:46 -04:00
Jeff a23f5bdd4a getSubscription returns subscription at the start of the current session, so rely on the result of the subscribe call
Test / test (push) Failing after 53s Details
2024-10-12 13:34:57 -04:00
Jeff 8890093dfd Send subscribed event to worker
Test / test (push) Failing after 53s Details
2024-10-12 12:33:30 -04:00
Jeff 93340fb1dd See what's going on with notifications
Test / test (push) Failing after 51s Details
2024-10-12 12:16:53 -04:00
17 changed files with 372 additions and 63 deletions

View File

@ -8,7 +8,7 @@ app.use(express.json())
app.get("/api", (req, res) => res.send("HI"))
app.post("/api/subscription", postSubscription)
app.post("/api/subscription/", postSubscription)
app.put("/api/subscription/:id/", putSubscription)
export default app

View File

@ -21,10 +21,6 @@ describe("/api/subscription", () => {
mockedCreateSubscription.mockImplementation((subscription) =>
Promise.resolve({ id: 40, subscription })
)
mockedUpdateSubscription.mockImplementation((subscription, id) =>
Promise.resolve({ id, subscription })
)
})
test("POST", async () => {

View File

@ -5,6 +5,8 @@ import { createSubscription } from "../data/subscription"
export async function postSubscription(req: Request, res: Response) {
const subscriptionBody = req.body
console.log("Posted subscription", subscriptionBody)
const { id: subscriptionId } = await createSubscription(subscriptionBody)
res.status(200).send({ subscriptionId })

View File

@ -1,8 +1,9 @@
import React from "react"
import React, { useState } from "react"
import { useEffect } from "react"
import { usePush } from "../usePush"
import pushPublicKey from "../../pushPublicKey"
import { request } from "http"
import urlB64ToUint8Array from "../../b64ToUInt8"
import pushPublicKey from "../../pushPublicKey"
export default function EnableNotifications({
onSubscribe,
@ -24,6 +25,9 @@ export default function EnableNotifications({
function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
const { subscribeToPush, requestPermission, canSendPush } = usePush()
const [error, setError] = useState<Error>()
const [log, setLog] = useState<string[]>([])
function subscribe() {
requestPermission()
}
@ -31,8 +35,45 @@ function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
useEffect(() => {
if (!canSendPush) return
subscribeToPush(urlB64ToUint8Array(pushPublicKey) as any, onSubscribe)
setLog((prev) => [...prev, "Subscribing to push notifications"])
subscribeToPush(
urlB64ToUint8Array(pushPublicKey) as any,
(subscription) => {
setLog((prev) => [
...prev,
`controller is undefined? ${navigator.serviceWorker.controller === undefined}`,
])
setLog((prev) => [
...prev,
`subscriptions: ${JSON.stringify(subscription.toJSON())}`,
])
try {
navigator.serviceWorker.ready.then((registration) => {
registration.active?.postMessage({
type: "subscribed",
subscription: subscription.toJSON(),
})
setLog((prev) => [...prev, "After post message"])
})
// onSubscribe()
} catch (error) {
setError(error as Error)
}
},
(error) => {
setError(error)
}
)
}, [canSendPush])
return <button onClick={subscribe}>Enable Notifications</button>
return (
<>
<button onClick={subscribe}>Enable Notifications</button>
<div>{error?.toString()}</div>
{log.map((log, index) => (
<div key={index}>{log}</div>
))}
</>
)
}

View File

@ -93,21 +93,33 @@ function TackUpNow({
const [isInstalled, setIsInstalled] = useState(installed)
const [messages, setMessages] = useState<any[]>([])
useEffect(() => {
navigator.serviceWorker.onmessage = (event) =>
setMessages((prev) => [...prev, JSON.stringify(event.data)])
}, [])
return (
<ClientOnly fallback={<div>Loading</div>}>
{() =>
isInstalled ? (
<div>Your Notifications</div>
) : (
<InstallPrompts
isMobileSafari={isMobileSafari}
isSupported={isSupported}
isIOS={isIOS}
notificationsEnabled={false}
onInstallComplete={() => setIsInstalled(true)}
/>
)
}
{() => (
<>
{messages.map((message, index) => (
<div key={index}>{JSON.stringify(message)}</div>
))}
{isInstalled ? (
<div>Your Notifications</div>
) : (
<InstallPrompts
isMobileSafari={isMobileSafari}
isSupported={isSupported}
isIOS={isIOS}
notificationsEnabled={false}
onInstallComplete={() => setIsInstalled(true)}
/>
)}
</>
)}
</ClientOnly>
)
}

View File

@ -40,54 +40,177 @@ export default function start(self: ServiceWorkerGlobalScope) {
// )
// })
async function sendMessage(message: any) {
const clients = await self.clients.matchAll()
await Promise.all(
clients.map(async (client) => client.postMessage(message))
)
}
self.addEventListener("message", function (event) {
const waitEvent = event as ExtendableEvent
waitEvent.waitUntil(
(async () => {
await sendMessage({
message: "Got message",
data: event.data,
})
if (
"type" in event.data &&
event.data.type === "subscribed" &&
"subscription" in event.data
) {
try {
await event.waitUntil(
submitSubscription(
self.registration,
event.data.subscription as PushSubscription
)
)
} catch (e) {
await sendMessage({
message: "Got error processing subscription",
error: (e as Error).toString(),
})
}
}
await sendMessage({
message: "Processed subscription",
})
})()
)
})
self.addEventListener("pushsubscriptionchange", function (event) {
const waitEvent = event as ExtendableEvent
waitEvent.waitUntil(submitSubscription(self.registration))
waitEvent.waitUntil(
(async () => {
const existingSubscription =
await self.registration.pushManager.getSubscription()
const newSubscription = await submitSubscription(
self.registration,
existingSubscription
)
self.registration.pushManager.getSubscription()
})
}
async function submitSubscription(registration: ServiceWorkerRegistration) {
const db = database()
const subscription = await registration.pushManager.getSubscription()
if (subscription === null) return
const existingSubscriptionId = await db.subscriptions.get(1)
const applicationServerKey = urlB64ToUint8Array(pushPublicKey)
const newSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
await sendMessage({ type: "sent subscription", newSubscription })
})()
)
})
await (existingSubscriptionId === undefined
? postSubscription(newSubscription, db)
: putSubscription(newSubscription, existingSubscriptionId.subscriptionId))
}
async function submitSubscription(
registration: ServiceWorkerRegistration,
subscription: PushSubscription | null
) {
const db = database()
async function postSubscription(
subscription: PushSubscription,
db: ReturnType<typeof database>
) {
const response = await fetch(`${process.env.BASE_URL}/api/subscription/`, {
method: "POST",
body: JSON.stringify(subscription),
})
await sendMessage({
message: `Is subscription null? ${subscription === null}`,
})
db.subscriptions.put({
id: 1,
subscriptionId: (await response.json()).subscriptionId,
})
}
if (subscription === null) return
function putSubscription(subscription: PushSubscription, id: number) {
return fetch(`${process.env.BASE_URL}/api/subscription/${id}`, {
method: "PUT",
body: JSON.stringify(subscription),
})
const existingSubscriptionId = await db.subscriptions.get(1)
await sendMessage({
message: `Existing subscription ID ${existingSubscriptionId}`,
})
await sendMessage({
message: `pushPublicKey ${pushPublicKey}`,
})
let applicationServerKey
try {
applicationServerKey = urlB64ToUint8Array(pushPublicKey)
} catch (error) {
await sendMessage({
message: `B64 error ${(error as Error).toString()}`,
})
}
await sendMessage({
message: `Converted public key`,
})
const newSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
})
await sendMessage({
message: `subscribed via pushManager`,
})
const stupid =
existingSubscriptionId === undefined
? postSubscription(newSubscription.toJSON(), db)
: putSubscription(
newSubscription.toJSON(),
existingSubscriptionId.subscriptionId
)
await stupid
return newSubscription
}
async function postSubscription(
subscription: PushSubscriptionJSON,
db: ReturnType<typeof database>
) {
await sendMessage({
message: "Log something you piece of garbage",
})
try {
await sendMessage({
message: `URL ${process.env.BASE_URL}/api/subscription/`,
})
await sendMessage({
message: `Submitting subscription ${subscription}`,
})
await sendMessage({
message: `JSON formatted: ${JSON.stringify(subscription)}`,
})
const response = await fetch(
`${process.env.BASE_URL}/api/subscription/`,
{
method: "POST",
body: JSON.stringify(subscription),
headers: {
"Content-Type": "application/json",
},
}
)
await sendMessage({
message: `Response status ${response.status}`,
})
db.subscriptions.put({
id: 1,
subscriptionId: (await response.json()).subscriptionId,
})
} catch (error) {
await sendMessage({
message: `ERRRROR ${(error as Error).toString()}}`,
})
}
}
function putSubscription(subscription: PushSubscriptionJSON, id: number) {
return fetch(`${process.env.BASE_URL}/api/subscription/${id}`, {
method: "PUT",
body: JSON.stringify(subscription),
headers: {
"Content-Type": "application/json",
},
})
}
}
function database() {

View File

@ -2,7 +2,7 @@ export default function urlB64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/")
const rawData = window.atob(base64)
const rawData = atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {

View File

@ -19,3 +19,8 @@ global.FormData = JSDOMFormData
process.env.BASE_URL = "http://localhost"
enableFetchMocks()
afterAll(() => {
jest.restoreAllMocks()
jest.resetModules()
})

View File

@ -0,0 +1,21 @@
interface TackUpNotification<Name extends string, Params> {
name: Name
params: Params
subscriptionJson: any
start: () => void
}
type TackUpNotifications =
TackUpNotification<"left to go in class", {
classNumber: number,
leftToGo: number
}> |
function create<NotificationType>(type: NotificationType["name"], params: NotificationType["params"]) {
}

View File

@ -0,0 +1,21 @@
function buildLeftToGoInClass({ data }) {
function buildNotification({ data: }) {
const leftToGo = getLeftToGoInClass(data.classNumber)
const className = getClassNameFromClassNumber(data.classNumber)
return {
title: `There are ${leftToGo} left to go in ${className}`
}
}
function startWatch() {
watchClass({number: data.classNumber}, )
}
return {
buildNotification,
}
}

9
show-watcher/notify.ts Normal file
View File

@ -0,0 +1,9 @@
interface NotificationParams {
title: string,
badge:
}
export default function notify(recipientSubscriptionJSON: any, notification: {
title: string,
options: NotificationOptions
}) {}

View File

@ -0,0 +1,41 @@
import register from "./register"
import watch from "./watch"
import notify from "./notify"
jest.mock("./watch")
jest.mock("./notify")
const mockedNotify = jest.mocked(notify)
const mockedWatch = jest.mocked(watch)
const recipientSubscriptionJSON = {
endpoint: "https://zombo.com",
expirationTime: null,
keys: {
auth: "O_JFRKmhy3OU9LfpfWeQ8Q",
p256dh: "somegoofystring",
},
}
describe("register", () => {
beforeEach(() => {
mockedWatch.mockImplementation(() => jest.fn())
})
test("starts a watch for the provided event", () => {
register({
type: "left to go in class",
data: {
count: 5,
class: 2102,
},
recipientSubscriptionJSON,
})
const watchCallback = mockedWatch.mock.calls[0][0].callback
expect(mockedNotify).toHaveBeenCalledWith(recipientSubscriptionJSON, {
title: "5 left to go in class 2102",
})
})
})

22
show-watcher/register.ts Normal file
View File

@ -0,0 +1,22 @@
import notify from "./notify"
import watch from "./watch"
export default function register({
type,
data,
recipientSubscriptionJSON,
}: {
type: string
data: any
recipientSubscriptionJSON: any
}) {
function onNotify() {
notify(recipientSubscriptionJSON)
}
watch({
type,
data,
callback: onNotify,
})
}

View File

@ -0,0 +1,3 @@
describe("Left to go in class notification", () => {
test("")
})

9
show-watcher/watch.ts Normal file
View File

@ -0,0 +1,9 @@
export default function watch({
type,
data,
callback,
}: {
type: string
data: any
callback: () => void
}) {}

View File

@ -10,6 +10,10 @@ export default defineConfig({
plugins: [
remix({ ignoredRouteFiles: ["**/*.test.*"] }),
tsconfigPaths(),
remixPWA(),
remixPWA({
buildVariables: {
"process.env.BASE_URL": process.env.BASE_URL!,
},
}),
],
})