Compare commits
27 Commits
master
...
spike-noti
| Author | SHA1 | Date |
|---|---|---|
|
|
c684e2e31c | |
|
|
f3e3077374 | |
|
|
4f0e68be41 | |
|
|
65185a4e9f | |
|
|
b966b30c06 | |
|
|
43ee91c8e4 | |
|
|
cc7faccecc | |
|
|
478cd10022 | |
|
|
5b5c8fa847 | |
|
|
b816cb56da | |
|
|
121cc90de9 | |
|
|
3ff16c58b2 | |
|
|
00876f08e9 | |
|
|
fc043461a2 | |
|
|
e3a0b6e730 | |
|
|
b92750e5d5 | |
|
|
a07f028b75 | |
|
|
5741b9fee2 | |
|
|
16250ef533 | |
|
|
1d3edec7bc | |
|
|
07d638b66c | |
|
|
4a7d752230 | |
|
|
0c3e6f60b1 | |
|
|
d655492d16 | |
|
|
a23f5bdd4a | |
|
|
8890093dfd | |
|
|
93340fb1dd |
|
|
@ -1,2 +1 @@
|
||||||
data/
|
data/
|
||||||
data-test/
|
|
||||||
6
.env.ci
6
.env.ci
|
|
@ -1,6 +0,0 @@
|
||||||
DATABASE="postgres"
|
|
||||||
POSTGRES_PASSWORD="testpassword"
|
|
||||||
POSTGRES_HOST="database"
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
VAPID_PRIVATE_KEY="privatekey"
|
|
||||||
BASE_URL="http://localhost:5173"
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
DATABASE="postgres"
|
DATABASE="postgres"
|
||||||
POSTGRES_PASSWORD="testpassword"
|
POSTGRES_PASSWORD="testpassword"
|
||||||
POSTGRES_HOST="localhost"
|
|
||||||
POSTGRES_PORT=5434
|
|
||||||
VAPID_PRIVATE_KEY="privatekey"
|
VAPID_PRIVATE_KEY="privatekey"
|
||||||
BASE_URL="http://localhost:5173"
|
BASE_URL="http://localhost:5173"
|
||||||
|
|
@ -5,27 +5,6 @@ on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: mcr.microsoft.com/playwright:v1.48.1-jammy
|
|
||||||
services:
|
|
||||||
database:
|
|
||||||
image: postgres:16
|
|
||||||
env:
|
|
||||||
POSTGRES_PASSWORD: testpassword
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: test
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
ports:
|
|
||||||
- 5434:5432
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Run Tests
|
- run: npm install && npm run test && npx playwright install && npx playwright test
|
||||||
env: NODE_OPTIONS=--max-old-space-size=8192
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
npm run ci
|
|
||||||
npm ci
|
|
||||||
npx playwright test
|
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,3 @@ build/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
send-it.js
|
|
||||||
|
|
@ -8,7 +8,7 @@ app.use(express.json())
|
||||||
|
|
||||||
app.get("/api", (req, res) => res.send("HI"))
|
app.get("/api", (req, res) => res.send("HI"))
|
||||||
|
|
||||||
app.post("/api/subscription", postSubscription)
|
app.post("/api/subscription/", postSubscription)
|
||||||
app.put("/api/subscription/:id/", putSubscription)
|
app.put("/api/subscription/:id/", putSubscription)
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,6 @@ const dialect = new PostgresDialect({
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export let db = new Kysely<Database>({
|
export const db = new Kysely<Database>({
|
||||||
dialect,
|
dialect,
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function resetDbInstance() {
|
|
||||||
await db.destroy()
|
|
||||||
db = new Kysely<Database>({
|
|
||||||
dialect,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
database: process.env.DATABASE ?? "postgres",
|
database: process.env.DATABASE ?? "postgres",
|
||||||
host: process.env.POSTGRES_HOST ?? "database",
|
host: "database",
|
||||||
user: "postgres",
|
user: "postgres",
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
port: process.env.POSTGRES_PORT ?? 5432,
|
port: 5432,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { db, resetDbInstance } from "./database"
|
import { db } from "./database"
|
||||||
import migrateToLatest, { resetAll } from "./migrate"
|
import migrateToLatest, { resetAll } from "./migrate"
|
||||||
import {
|
import {
|
||||||
createSubscription,
|
createSubscription,
|
||||||
|
|
@ -26,29 +26,17 @@ const pushSubscription2 = {
|
||||||
}
|
}
|
||||||
|
|
||||||
jest.mock("./settings", () => ({
|
jest.mock("./settings", () => ({
|
||||||
port: process.env.POSTGRES_PORT,
|
port: 5434,
|
||||||
user: "postgres",
|
user: "postgres",
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
database: "test",
|
database: "test",
|
||||||
host: process.env.POSTGRES_HOST,
|
host: "localhost",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe("subscriptions", () => {
|
describe("subscriptions", () => {
|
||||||
beforeAll(async () => {
|
beforeEach(migrateToLatest)
|
||||||
await resetDbInstance()
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
afterEach(resetAll)
|
||||||
await migrateToLatest()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await resetAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await db.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("createSubscription", async () => {
|
test("createSubscription", async () => {
|
||||||
const { id } = await createSubscription(pushSubscription1)
|
const { id } = await createSubscription(pushSubscription1)
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,6 @@ describe("/api/subscription", () => {
|
||||||
mockedCreateSubscription.mockImplementation((subscription) =>
|
mockedCreateSubscription.mockImplementation((subscription) =>
|
||||||
Promise.resolve({ id: 40, subscription })
|
Promise.resolve({ id: 40, subscription })
|
||||||
)
|
)
|
||||||
|
|
||||||
mockedUpdateSubscription.mockImplementation((subscription, id) =>
|
|
||||||
Promise.resolve({ id, subscription })
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("POST", async () => {
|
test("POST", async () => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { createSubscription } from "../data/subscription"
|
||||||
export async function postSubscription(req: Request, res: Response) {
|
export async function postSubscription(req: Request, res: Response) {
|
||||||
const subscriptionBody = req.body
|
const subscriptionBody = req.body
|
||||||
|
|
||||||
|
console.log("Posted subscription", subscriptionBody)
|
||||||
|
|
||||||
const { id: subscriptionId } = await createSubscription(subscriptionBody)
|
const { id: subscriptionId } = await createSubscription(subscriptionBody)
|
||||||
|
|
||||||
res.status(200).send({ subscriptionId })
|
res.status(200).send({ subscriptionId })
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React from "react"
|
import React, { useState } from "react"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { usePush } from "../usePush"
|
import { usePush } from "../usePush"
|
||||||
import pushPublicKey from "../../pushPublicKey"
|
import { request } from "http"
|
||||||
import urlB64ToUint8Array from "../../b64ToUInt8"
|
import urlB64ToUint8Array from "../../b64ToUInt8"
|
||||||
|
import pushPublicKey from "../../pushPublicKey"
|
||||||
|
|
||||||
export default function EnableNotifications({
|
export default function EnableNotifications({
|
||||||
onSubscribe,
|
onSubscribe,
|
||||||
|
|
@ -24,6 +25,9 @@ export default function EnableNotifications({
|
||||||
function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
|
function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
|
||||||
const { subscribeToPush, requestPermission, canSendPush } = usePush()
|
const { subscribeToPush, requestPermission, canSendPush } = usePush()
|
||||||
|
|
||||||
|
const [error, setError] = useState<Error>()
|
||||||
|
const [log, setLog] = useState<string[]>([])
|
||||||
|
|
||||||
function subscribe() {
|
function subscribe() {
|
||||||
requestPermission()
|
requestPermission()
|
||||||
}
|
}
|
||||||
|
|
@ -31,19 +35,45 @@ function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canSendPush) return
|
if (!canSendPush) return
|
||||||
|
|
||||||
|
setLog((prev) => [...prev, "Subscribing to push notifications"])
|
||||||
|
|
||||||
subscribeToPush(
|
subscribeToPush(
|
||||||
urlB64ToUint8Array(pushPublicKey) as any,
|
urlB64ToUint8Array(pushPublicKey) as any,
|
||||||
async (subscription) => {
|
(subscription) => {
|
||||||
await navigator.serviceWorker.ready.then((registration) => {
|
setLog((prev) => [
|
||||||
registration.active?.postMessage({
|
...prev,
|
||||||
type: "subscribed",
|
`controller is undefined? ${navigator.serviceWorker.controller === undefined}`,
|
||||||
subscription: subscription.toJSON(),
|
])
|
||||||
|
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()
|
||||||
onSubscribe()
|
} catch (error) {
|
||||||
|
setError(error as Error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
setError(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}, [canSendPush])
|
}, [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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,21 +93,33 @@ function TackUpNow({
|
||||||
|
|
||||||
const [isInstalled, setIsInstalled] = useState(installed)
|
const [isInstalled, setIsInstalled] = useState(installed)
|
||||||
|
|
||||||
|
const [messages, setMessages] = useState<any[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigator.serviceWorker.onmessage = (event) =>
|
||||||
|
setMessages((prev) => [...prev, JSON.stringify(event.data)])
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientOnly fallback={<div>Loading</div>}>
|
<ClientOnly fallback={<div>Loading</div>}>
|
||||||
{() =>
|
{() => (
|
||||||
isInstalled ? (
|
<>
|
||||||
<div>Your Notifications</div>
|
{messages.map((message, index) => (
|
||||||
) : (
|
<div key={index}>{JSON.stringify(message)}</div>
|
||||||
<InstallPrompts
|
))}
|
||||||
isMobileSafari={isMobileSafari}
|
{isInstalled ? (
|
||||||
isSupported={isSupported}
|
<div>Your Notifications</div>
|
||||||
isIOS={isIOS}
|
) : (
|
||||||
notificationsEnabled={false}
|
<InstallPrompts
|
||||||
onInstallComplete={() => setIsInstalled(true)}
|
isMobileSafari={isMobileSafari}
|
||||||
/>
|
isSupported={isSupported}
|
||||||
)
|
isIOS={isIOS}
|
||||||
}
|
notificationsEnabled={false}
|
||||||
|
onInstallComplete={() => setIsInstalled(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,167 +3,64 @@ import "core-js/stable/structured-clone"
|
||||||
import initWorker from "./worker"
|
import initWorker from "./worker"
|
||||||
import Dexie, { EntityTable } from "dexie"
|
import Dexie, { EntityTable } from "dexie"
|
||||||
|
|
||||||
function createSubscription(testToken = "HI") {
|
|
||||||
return {
|
|
||||||
toJSON: () => ({
|
|
||||||
subscription: testToken,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSelf = () =>
|
const createSelf = () =>
|
||||||
({
|
({
|
||||||
addEventListener: jest.fn(),
|
addEventListener: jest.fn(),
|
||||||
skipWaiting: jest.fn(() => Promise.resolve()),
|
skipWaiting: jest.fn(() => Promise.resolve()),
|
||||||
registration: {
|
registration: {
|
||||||
pushManager: {
|
pushManager: {
|
||||||
subscribe: jest.fn(() => Promise.resolve(createSubscription())),
|
subscribe: jest.fn(() => Promise.resolve({ subscription: "HI" })),
|
||||||
getSubscription: jest.fn(() => Promise.resolve(createSubscription())),
|
getSubscription: jest.fn(() => Promise.resolve({ subscription: "HI" })),
|
||||||
},
|
},
|
||||||
showNotification: jest.fn(),
|
|
||||||
},
|
},
|
||||||
clients: {
|
clients: {
|
||||||
claim: jest.fn(() => Promise.resolve()),
|
claim: jest.fn(() => Promise.resolve()),
|
||||||
openWindow: jest.fn(() => Promise.resolve()),
|
|
||||||
matchAll: jest.fn(() => Promise.resolve([])),
|
|
||||||
},
|
},
|
||||||
}) as unknown as ServiceWorkerGlobalScope
|
}) as unknown as ServiceWorkerGlobalScope
|
||||||
|
|
||||||
let originalWindow: Window & typeof globalThis
|
|
||||||
|
|
||||||
describe("service worker", () => {
|
describe("service worker", () => {
|
||||||
let self: ServiceWorkerGlobalScope
|
let self: ServiceWorkerGlobalScope
|
||||||
let controlledClients: Client[]
|
|
||||||
let uncontrolledClients: Client[]
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalWindow = global.window
|
|
||||||
global.window = undefined as any
|
|
||||||
indexedDB = new IDBFactory()
|
indexedDB = new IDBFactory()
|
||||||
self = createSelf()
|
self = createSelf()
|
||||||
controlledClients = []
|
|
||||||
uncontrolledClients = []
|
// fetchMock.mockIf(/http:\/\/localhost\/api\/subscribe\/?/, (req) => {
|
||||||
|
// return Promise.resolve(
|
||||||
|
// req.method === "POST"
|
||||||
|
// ? {
|
||||||
|
// body: JSON.stringify({ subscriptionId: 123 }),
|
||||||
|
// }
|
||||||
|
// : req.method === "PUT"
|
||||||
|
// ? { init: { status: 200 } }
|
||||||
|
// : { init: { status: 405 } }
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
fetchMock.mockResponse((req) => {
|
fetchMock.mockResponse((req) => {
|
||||||
if (
|
return Promise.resolve(
|
||||||
req.url.replace(/https?:\/\/[a-zA-Z0-9\.]*/, "") ===
|
|
||||||
"/api/subscription/" &&
|
|
||||||
req.method === "POST"
|
req.method === "POST"
|
||||||
) {
|
? {
|
||||||
return Promise.resolve({
|
body: JSON.stringify({ subscriptionId: 123 }),
|
||||||
body: JSON.stringify({ subscriptionId: 123 }),
|
}
|
||||||
})
|
: req.method === "PUT"
|
||||||
} else if (
|
? { init: { status: 200 } }
|
||||||
req.url
|
: { init: { status: 405 } }
|
||||||
.replace(/https?:\/\/[a-zA-Z0-9\.]*/, "")
|
)
|
||||||
.startsWith("/api/subscription/") &&
|
|
||||||
req.method === "PUT"
|
|
||||||
) {
|
|
||||||
return Promise.resolve({ init: { status: 200 } })
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({ init: { status: 500 } })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
global.window = originalWindow
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("displays push notifications", async () => {
|
xtest("displays push notifications", () => {
|
||||||
initWorker(self)
|
initWorker(self)
|
||||||
|
|
||||||
const pushHandler = getHandler("push")
|
const pushHandler = getHandler("push")
|
||||||
|
|
||||||
const pushEvent = createPushEvent(
|
const pushEvent = createEvent({ data: "HI" })
|
||||||
JSON.stringify({
|
|
||||||
title: "Test title",
|
|
||||||
body: "Test text",
|
|
||||||
icon: "Test icon",
|
|
||||||
badge: "Test badge",
|
|
||||||
destination: "tackupnow.com/hi",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
pushHandler(pushEvent)
|
pushHandler(pushEvent)
|
||||||
await waitUntilCalls(pushEvent)
|
|
||||||
|
|
||||||
expect(self.registration.showNotification).toHaveBeenCalledWith(
|
|
||||||
"Test title",
|
|
||||||
{
|
|
||||||
body: "Test text",
|
|
||||||
icon: "Test icon",
|
|
||||||
badge: "Test badge",
|
|
||||||
data: { destination: "tackupnow.com/hi" },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("on notification click", () => {
|
|
||||||
test("the notification is closed", async () => {
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const notificationHandler = getHandler("notificationclick")
|
|
||||||
|
|
||||||
const event = createNotificationEvent("tackupnow.com")
|
|
||||||
notificationHandler(event)
|
|
||||||
await waitUntilCalls(event)
|
|
||||||
|
|
||||||
expect(event.notification.close).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("opens tack up now if no clients match the destination", async () => {
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const notificationHandler = getHandler("notificationclick")
|
|
||||||
|
|
||||||
const event = createNotificationEvent("tackupnow.com")
|
|
||||||
notificationHandler(event)
|
|
||||||
await waitUntilCalls(event)
|
|
||||||
|
|
||||||
expect(self.clients.openWindow).toHaveBeenCalledWith("tackupnow.com")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("focuses first matching client", async () => {
|
|
||||||
addClient("https://tackupnow.com/place", "window", true)
|
|
||||||
const matchingClient = addClient(
|
|
||||||
"https://tackupnow.com/otherplace",
|
|
||||||
"window",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
addClient("https://tackupnow.com/yetanotherotherplace", "window", true)
|
|
||||||
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const notificationHandler = getHandler("notificationclick")
|
|
||||||
|
|
||||||
const event = createNotificationEvent("https://tackupnow.com/otherplace")
|
|
||||||
notificationHandler(event)
|
|
||||||
await waitUntilCalls(event)
|
|
||||||
|
|
||||||
expect(matchingClient.focus).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("focuses uncontrolled matching clients", async () => {
|
|
||||||
addClient("https://derpatious.world", "window", false)
|
|
||||||
addClient("https://tackupnow.com/1", "window", false)
|
|
||||||
const matchingClient = addClient(
|
|
||||||
"https://tackupnow.com/2",
|
|
||||||
"window",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
addClient("https://tackupnow.com/controlled", "window", true)
|
|
||||||
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const notificationHandler = getHandler("notificationclick")
|
|
||||||
|
|
||||||
const event = createNotificationEvent("https://tackupnow.com/2")
|
|
||||||
notificationHandler(event)
|
|
||||||
await waitUntilCalls(event)
|
|
||||||
|
|
||||||
expect(matchingClient.focus).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("claims on activate", async () => {
|
test("claims on activate", async () => {
|
||||||
|
|
@ -190,175 +87,64 @@ describe("service worker", () => {
|
||||||
expect(self.skipWaiting).toHaveBeenCalled()
|
expect(self.skipWaiting).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("on subscription message", () => {
|
test("puts push subscription if the subscription id is set", async () => {
|
||||||
test("puts push subscription if the subscription id is set", async () => {
|
initWorker(self)
|
||||||
initWorker(self)
|
await setSavedSubscription(5000)
|
||||||
await setSavedSubscription(5000)
|
|
||||||
|
|
||||||
const messageHandler = getHandler("message")
|
const changeHandler = getHandler("pushsubscriptionchange")
|
||||||
|
|
||||||
const messageEvent = createEvent({
|
const changeEvent = createEvent()
|
||||||
data: {
|
changeHandler(changeEvent)
|
||||||
subscription: { subscription: "YO" },
|
await waitUntilCalls(changeEvent)
|
||||||
type: "subscribed",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
messageHandler(messageEvent)
|
|
||||||
await waitUntilCalls(messageEvent)
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
expect(self.registration.pushManager.subscribe).toHaveBeenCalledWith({
|
||||||
`${process.env.BASE_URL}/api/subscription/5000`,
|
userVisibleOnly: true,
|
||||||
{
|
applicationServerKey: expect.any(Uint8Array),
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({ subscription: "YO" }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("posts to subscriptions when there is no user set", async () => {
|
expect(fetch).toHaveBeenCalledWith(
|
||||||
initWorker(self)
|
`${process.env.BASE_URL}/api/subscription/5000`,
|
||||||
|
{
|
||||||
const messageHandler = getHandler("message")
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ subscription: "HI" }),
|
||||||
const messageEvent = createEvent({
|
|
||||||
data: {
|
|
||||||
subscription: { subscription: "YO" },
|
|
||||||
type: "subscribed",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
messageHandler(messageEvent)
|
|
||||||
await waitUntilCalls(messageEvent)
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`${process.env.BASE_URL}/api/subscription/`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ subscription: "YO" }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("subscription id is saved on first subscription change", async () => {
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const messageHandler = getHandler("message")
|
|
||||||
|
|
||||||
const messageEvent = createEvent({
|
|
||||||
data: {
|
|
||||||
subscription: { subscription: "YO" },
|
|
||||||
type: "subscribed",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
messageHandler(messageEvent)
|
|
||||||
await waitUntilCalls(messageEvent)
|
|
||||||
|
|
||||||
expect(await getSavedSubscription()).toEqual({
|
|
||||||
id: 1,
|
|
||||||
subscriptionId: 123,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("on subscription change", () => {
|
|
||||||
test("puts push subscription if the subscription id is set", async () => {
|
|
||||||
initWorker(self)
|
|
||||||
await setSavedSubscription(5000)
|
|
||||||
|
|
||||||
const changeHandler = getHandler("pushsubscriptionchange")
|
|
||||||
|
|
||||||
const changeEvent = createEvent()
|
|
||||||
changeHandler(changeEvent)
|
|
||||||
await waitUntilCalls(changeEvent)
|
|
||||||
|
|
||||||
expect(self.registration.pushManager.subscribe).toHaveBeenCalledWith({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: expect.any(Uint8Array),
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`${process.env.BASE_URL}/api/subscription/5000`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({ subscription: "HI" }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("posts to subscriptions when there is no user set", async () => {
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const changeHandler = getHandler("pushsubscriptionchange")
|
|
||||||
|
|
||||||
const changeEvent = createEvent()
|
|
||||||
changeHandler(changeEvent)
|
|
||||||
await waitUntilCalls(changeEvent)
|
|
||||||
|
|
||||||
expect(self.registration.pushManager.subscribe).toHaveBeenCalledWith({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: expect.any(Uint8Array),
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(
|
|
||||||
`${process.env.BASE_URL}/api/subscription/`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ subscription: "HI" }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("subscription id is saved on first subscription change", async () => {
|
|
||||||
initWorker(self)
|
|
||||||
|
|
||||||
const changeHandler = getHandler("pushsubscriptionchange")
|
|
||||||
|
|
||||||
const changeEvent = createEvent()
|
|
||||||
changeHandler(changeEvent)
|
|
||||||
await waitUntilCalls(changeEvent)
|
|
||||||
|
|
||||||
expect(await getSavedSubscription()).toEqual({
|
|
||||||
id: 1,
|
|
||||||
subscriptionId: 123,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function addClient(url: string, type: ClientTypes, controlled: boolean) {
|
|
||||||
const matchAllMock = self.clients.matchAll as jest.Mock
|
|
||||||
|
|
||||||
matchAllMock.mockImplementation(
|
|
||||||
(args: { type: string; includeUncontrolled?: boolean }) => {
|
|
||||||
const clientList = args.includeUncontrolled
|
|
||||||
? [...uncontrolledClients, ...controlledClients]
|
|
||||||
: controlledClients
|
|
||||||
return clientList.filter((client) => client.type === args.type)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const clientList = controlled ? controlledClients : uncontrolledClients
|
test("posts to subscriptions when there is no user set", async () => {
|
||||||
|
initWorker(self)
|
||||||
|
|
||||||
const client: WindowClient = {
|
const changeHandler = getHandler("pushsubscriptionchange")
|
||||||
url,
|
|
||||||
type,
|
|
||||||
focus: jest.fn(() => Promise.resolve(client)) as WindowClient["focus"],
|
|
||||||
} as WindowClient
|
|
||||||
|
|
||||||
clientList.push(client)
|
const changeEvent = createEvent()
|
||||||
|
changeHandler(changeEvent)
|
||||||
|
await waitUntilCalls(changeEvent)
|
||||||
|
|
||||||
return client
|
expect(self.registration.pushManager.subscribe).toHaveBeenCalledWith({
|
||||||
}
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: expect.any(Uint8Array),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledWith(
|
||||||
|
`${process.env.BASE_URL}/api/subscription/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ subscription: "HI" }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("subscription id is saved on first subscription change", async () => {
|
||||||
|
initWorker(self)
|
||||||
|
|
||||||
|
const changeHandler = getHandler("pushsubscriptionchange")
|
||||||
|
|
||||||
|
const changeEvent = createEvent()
|
||||||
|
changeHandler(changeEvent)
|
||||||
|
await waitUntilCalls(changeEvent)
|
||||||
|
|
||||||
|
expect(await getSavedSubscription()).toEqual({ id: 1, subscriptionId: 123 })
|
||||||
|
})
|
||||||
|
|
||||||
function getHandler(eventName: string) {
|
function getHandler(eventName: string) {
|
||||||
expect(self.addEventListener).toHaveBeenCalledWith(
|
expect(self.addEventListener).toHaveBeenCalledWith(
|
||||||
|
|
@ -407,19 +193,8 @@ function createPushEvent(data: string) {
|
||||||
return createEvent(pushFields)
|
return createEvent(pushFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNotificationEvent(destination: string): NotificationEvent & {
|
function createMessageEvent(message: any) {
|
||||||
waitUntil: jest.Mock<void, Parameters<ExtendableEvent["waitUntil"]>>
|
return createEvent({ data: message })
|
||||||
} {
|
|
||||||
return createEvent({
|
|
||||||
notification: {
|
|
||||||
close: jest.fn(),
|
|
||||||
data: {
|
|
||||||
destination,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}) as any as NotificationEvent & {
|
|
||||||
waitUntil: jest.Mock<void, Parameters<ExtendableEvent["waitUntil"]>>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEvent(properties?: object) {
|
function createEvent(properties?: object) {
|
||||||
|
|
|
||||||
247
app/worker.ts
247
app/worker.ts
|
|
@ -16,49 +16,72 @@ export default function start(self: ServiceWorkerGlobalScope) {
|
||||||
event.waitUntil(self.clients.claim())
|
event.waitUntil(self.clients.claim())
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener("push", function (event: PushEvent) {
|
// self.addEventListener("push", function (event: PushEvent) {
|
||||||
const { title, body, badge, icon, destination } = event.data?.json()
|
// console.log("[Service Worker] Push Received.")
|
||||||
|
// console.log(`[Service Worker] Push had this data: "${event.data?.text()}"`)
|
||||||
|
|
||||||
event.waitUntil(
|
// const title = "Push Codelab"
|
||||||
self.registration.showNotification(title, {
|
// const options = {
|
||||||
body,
|
// body: "Yay it works.",
|
||||||
badge,
|
// icon: "images/icon.png",
|
||||||
icon,
|
// badge: "images/badge.png",
|
||||||
data: { destination },
|
// }
|
||||||
})
|
|
||||||
|
// event.waitUntil(self.registration.showNotification(title, options))
|
||||||
|
// })
|
||||||
|
|
||||||
|
// self.addEventListener("notificationclick", function (event) {
|
||||||
|
// console.log("[Service Worker] Notification click Received.")
|
||||||
|
|
||||||
|
// event.notification.close()
|
||||||
|
|
||||||
|
// event.waitUntil(
|
||||||
|
// self.clients.openWindow("https://developers.google.com/web/")
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
async function sendMessage(message: any) {
|
||||||
|
const clients = await self.clients.matchAll()
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
clients.map(async (client) => client.postMessage(message))
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
self.addEventListener("notificationclick", function (event) {
|
|
||||||
event.notification.close()
|
|
||||||
const destination = event.notification.data.destination
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
(async () => {
|
|
||||||
const clients = await self.clients.matchAll({
|
|
||||||
type: "window",
|
|
||||||
includeUncontrolled: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const existingClient = clients.find(
|
|
||||||
(client) => client.url === event.notification.data.destination
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingClient === undefined) {
|
|
||||||
await self.clients.openWindow(destination)
|
|
||||||
} else {
|
|
||||||
await existingClient.focus()
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
self.addEventListener("message", function (event) {
|
self.addEventListener("message", function (event) {
|
||||||
const waitEvent = event as ExtendableEvent
|
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") {
|
if (
|
||||||
waitEvent.waitUntil(submitSubscription(event.data.subscription))
|
"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) {
|
self.addEventListener("pushsubscriptionchange", function (event) {
|
||||||
|
|
@ -66,54 +89,128 @@ export default function start(self: ServiceWorkerGlobalScope) {
|
||||||
|
|
||||||
waitEvent.waitUntil(
|
waitEvent.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
const applicationServerKey = urlB64ToUint8Array(pushPublicKey)
|
const existingSubscription =
|
||||||
const newSubscription = await self.registration.pushManager.subscribe({
|
await self.registration.pushManager.getSubscription()
|
||||||
userVisibleOnly: true,
|
const newSubscription = await submitSubscription(
|
||||||
applicationServerKey: applicationServerKey,
|
self.registration,
|
||||||
})
|
existingSubscription
|
||||||
|
)
|
||||||
|
|
||||||
await submitSubscription(newSubscription.toJSON())
|
await sendMessage({ type: "sent subscription", newSubscription })
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
async function submitSubscription(subscription: PushSubscriptionJSON) {
|
async function submitSubscription(
|
||||||
const db = database()
|
registration: ServiceWorkerRegistration,
|
||||||
|
subscription: PushSubscription | null
|
||||||
|
) {
|
||||||
|
const db = database()
|
||||||
|
|
||||||
const existingSubscriptionId = await db.subscriptions.get(1)
|
await sendMessage({
|
||||||
|
message: `Is subscription null? ${subscription === null}`,
|
||||||
|
})
|
||||||
|
|
||||||
await (existingSubscriptionId === undefined
|
if (subscription === null) return
|
||||||
? postSubscription(subscription, db)
|
|
||||||
: putSubscription(subscription, existingSubscriptionId.subscriptionId))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postSubscription(
|
const existingSubscriptionId = await db.subscriptions.get(1)
|
||||||
subscription: PushSubscriptionJSON,
|
|
||||||
db: ReturnType<typeof database>
|
|
||||||
) {
|
|
||||||
const response = await fetch(`${process.env.BASE_URL}/api/subscription/`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(subscription),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
db.subscriptions.put({
|
await sendMessage({
|
||||||
id: 1,
|
message: `Existing subscription ID ${existingSubscriptionId}`,
|
||||||
subscriptionId: (await response.json()).subscriptionId,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function putSubscription(subscription: PushSubscriptionJSON, id: number) {
|
await sendMessage({
|
||||||
return fetch(`${process.env.BASE_URL}/api/subscription/${id}`, {
|
message: `pushPublicKey ${pushPublicKey}`,
|
||||||
method: "PUT",
|
})
|
||||||
body: JSON.stringify(subscription),
|
|
||||||
headers: {
|
let applicationServerKey
|
||||||
"Content-Type": "application/json",
|
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() {
|
function database() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: postgres:16
|
image: postgres
|
||||||
ports:
|
ports:
|
||||||
- "5434:5432"
|
- "5434:5432"
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ services:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "9000:3000"
|
- "9000:3000"
|
||||||
env_file: ".env"
|
|
||||||
database:
|
database:
|
||||||
image: postgres:16
|
image: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { test, expect, Page } from "@playwright/test"
|
import { test, expect, Page } from "@playwright/test"
|
||||||
import matchPath from "./urlMatcher"
|
import matchPath from "./urlMatcher"
|
||||||
import { messageSW } from "@remix-pwa/sw"
|
|
||||||
|
|
||||||
const { describe, beforeEach, skip, use } = test
|
const { describe, beforeEach, skip, use } = test
|
||||||
|
|
||||||
|
|
@ -90,23 +89,6 @@ describe("when user is on iOS", () => {
|
||||||
|
|
||||||
await expect(yourNotificationsHeading).toBeVisible()
|
await expect(yourNotificationsHeading).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("the subscription is submitted", async ({ page }) => {
|
|
||||||
await page.goto("/")
|
|
||||||
|
|
||||||
await page.getByText(/Enable Notifications/).click()
|
|
||||||
|
|
||||||
const postedMessages = await page.evaluate(() => {
|
|
||||||
return (window as any).postedMessages
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(postedMessages).toContainEqual({
|
|
||||||
type: "subscribed",
|
|
||||||
subscription: {
|
|
||||||
subscription: "HI",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -230,37 +212,20 @@ function stubNotifications(
|
||||||
|
|
||||||
function stubServiceWorker(page: Page) {
|
function stubServiceWorker(page: Page) {
|
||||||
return page.addInitScript(() => {
|
return page.addInitScript(() => {
|
||||||
function createSubscription(testToken = "HI") {
|
const serviceWorker = {}
|
||||||
return {
|
|
||||||
toJSON: () => ({
|
|
||||||
subscription: testToken,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let postedMessages: any[] = []
|
|
||||||
|
|
||||||
const serviceWorker = {
|
|
||||||
postMessage: (message: any) => postedMessages.push(message),
|
|
||||||
}
|
|
||||||
|
|
||||||
const registration = {
|
const registration = {
|
||||||
pushManager: {
|
pushManager: {
|
||||||
getSubscription() {
|
getSubscription() {
|
||||||
return Promise.resolve(createSubscription())
|
return Promise.resolve({ hi: "subscription" })
|
||||||
},
|
},
|
||||||
subscribe(args: Parameters<PushManager["subscribe"]>[0]) {
|
subscribe(args: Parameters<PushManager["subscribe"]>[0]) {
|
||||||
return Promise.resolve(createSubscription())
|
return Promise.resolve({ hi: "subscription" })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: serviceWorker,
|
active: serviceWorker,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, "postedMessages", {
|
|
||||||
value: postedMessages,
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
Object.defineProperty(navigator, "serviceWorker", {
|
Object.defineProperty(navigator, "serviceWorker", {
|
||||||
value: {
|
value: {
|
||||||
ready: Promise.resolve(registration),
|
ready: Promise.resolve(registration),
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
"@babel/preset-env": "^7.24.5",
|
"@babel/preset-env": "^7.24.5",
|
||||||
"@babel/preset-react": "^7.24.1",
|
"@babel/preset-react": "^7.24.1",
|
||||||
"@babel/preset-typescript": "^7.24.1",
|
"@babel/preset-typescript": "^7.24.1",
|
||||||
"@playwright/test": "^1.48.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@remix-pwa/dev": "^3.1.0",
|
"@remix-pwa/dev": "^3.1.0",
|
||||||
"@remix-run/dev": "^2.9.0",
|
"@remix-run/dev": "^2.9.0",
|
||||||
"@remix-run/testing": "^2.9.1",
|
"@remix-run/testing": "^2.9.1",
|
||||||
|
|
@ -3324,18 +3324,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.48.1",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
|
||||||
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.48.1"
|
"playwright": "1.44.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@remix-pwa/dev": {
|
"node_modules/@remix-pwa/dev": {
|
||||||
|
|
@ -13843,33 +13843,33 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.48.1",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
|
||||||
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.48.1"
|
"playwright-core": "1.44.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.48.1",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
|
||||||
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright/node_modules/fsevents": {
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@
|
||||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"start": "remix-serve ./build/server/index.js",
|
"start": "remix-serve ./build/server/index.js",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"watch": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d --remove-orphans && jest --watch --config=jest.config.ts",
|
"watch": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d && jest --watch --config=jest.config.ts",
|
||||||
"test": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d --remove-orphans && sleep 5 && jest --config=jest.config.ts",
|
"test": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d && jest --config=jest.config.ts"
|
||||||
"ci": "export $(cat .env.ci | xargs) && jest --config=jest.config.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-pwa/sw": "^3.0.9",
|
"@remix-pwa/sw": "^3.0.9",
|
||||||
|
|
@ -39,7 +38,7 @@
|
||||||
"@babel/preset-env": "^7.24.5",
|
"@babel/preset-env": "^7.24.5",
|
||||||
"@babel/preset-react": "^7.24.1",
|
"@babel/preset-react": "^7.24.1",
|
||||||
"@babel/preset-typescript": "^7.24.1",
|
"@babel/preset-typescript": "^7.24.1",
|
||||||
"@playwright/test": "^1.48.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@remix-pwa/dev": "^3.1.0",
|
"@remix-pwa/dev": "^3.1.0",
|
||||||
"@remix-run/dev": "^2.9.0",
|
"@remix-run/dev": "^2.9.0",
|
||||||
"@remix-run/testing": "^2.9.1",
|
"@remix-run/testing": "^2.9.1",
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export default "BNwr5jYXxBwSRGooQWx6nzsrG5XejgAskVqowkc_O0rutL9PgRIkQAnDh4nz1yoPQEw40juMuqL2NEHSaD3XcxA"
|
export default "BKc02U2-z7PkTbYgYLlxELWqzTVE631fs4IPuMLbxY_rxdo9VaduthqwkPOiblwjETl99uXes2Nc9EtPbS5x4uA"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import webPush from "web-push"
|
||||||
|
|
||||||
|
const subscription = {
|
||||||
|
endpoint:
|
||||||
|
"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABm9FlbY5vMro60hF2iJF3UUBzweuFcg5NRSHXPSpHfUpjo5jKGVRnUxR4ekg0-FsvdQCP89cu__IFd06Tu2TJZ649YyqivRUBnAav0DgOLHGx5-t943QLS-wLvBqyJRCuuLlM1bLz6S9ph9AWJ8CG7rQuTabsHvw--s_w2KDQo3GcQXIM",
|
||||||
|
expirationTime: null,
|
||||||
|
keys: {
|
||||||
|
auth: "dDUqtFo26ekGEAmNzvmJAw",
|
||||||
|
p256dh:
|
||||||
|
"BHIT3J6xSRiHfz0m-QRHagDThiOZGVIANtPzOasrOBYG0s_yUnshTVharX5dZcq8GA5OkyMm3mqmA7_o_lFR4WE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
webPush.setVapidDetails(
|
||||||
|
"https://tackupnow.com",
|
||||||
|
"BKc02U2-z7PkTbYgYLlxELWqzTVE631fs4IPuMLbxY_rxdo9VaduthqwkPOiblwjETl99uXes2Nc9EtPbS5x4uA",
|
||||||
|
"Xmv5Pc4mqr138V3sCxXq7UmsbL5UgSOY43UuJ50nxPw"
|
||||||
|
)
|
||||||
|
|
||||||
|
webPush
|
||||||
|
.sendNotification(subscription, "Hi what's up?", {})
|
||||||
|
.then((x) => console.log("It sent", x))
|
||||||
|
.catch((error) => console.log("It did not send", error))
|
||||||
5
start.sh
5
start.sh
|
|
@ -1,4 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -a
|
||||||
|
source ./.env
|
||||||
|
set +a
|
||||||
npx tsx api/data/upgrade.ts
|
npx tsx api/data/upgrade.ts
|
||||||
npx tsx server.ts
|
npx tsx server.ts
|
||||||
|
|
@ -12,7 +12,7 @@ export default defineConfig({
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
remixPWA({
|
remixPWA({
|
||||||
buildVariables: {
|
buildVariables: {
|
||||||
"process.env.BASE_URL": process.env.BASE_URL ?? "http://localhost:5173",
|
"process.env.BASE_URL": process.env.BASE_URL!,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue