Fix bug in usePush, also some bugs in install state
Test / test (push) Failing after 24s Details

This commit is contained in:
Jeff 2024-09-25 01:23:39 -04:00
parent e2cb0eb214
commit e09769e1ba
8 changed files with 252 additions and 128 deletions

19
app/ClientOnly.tsx Normal file
View File

@ -0,0 +1,19 @@
import { ReactNode, useEffect, useState } from "react"
export default function ClientOnly({
fallback,
children,
}: {
fallback: ReactNode
children(): ReactNode
}) {
const isClient = typeof window !== "undefined"
const [rendered, setRendered] = useState(false)
useEffect(() => {
setRendered(true)
}, [])
return isClient && rendered ? children() : fallback
}

View File

@ -1,6 +1,6 @@
import React from "react"
import { useEffect } from "react"
import { usePush } from "remix-pwa-monorepo/packages/push/client/hook"
import { usePush } from "../usePush"
export default function EnableNotifications({
onSubscribe,

View File

@ -10,6 +10,7 @@ import versionAtLeast from "semver/functions/gte"
import UAParser from "ua-parser-js"
import InstallPrompts from "../install/InstallPrompts"
import useInstallState from "../useInstallState"
import ClientOnly from "../ClientOnly"
export const meta: MetaFunction = () => {
return [
@ -22,16 +23,24 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const userAgent = request.headers.get("user-agent")
const parsedUserAgent = new UAParser(userAgent ?? "")
const os = parsedUserAgent.getOS()
const isMobileSafari = parsedUserAgent.getBrowser().name !== "Mobile Safari"
const isMobileSafari = parsedUserAgent.getBrowser().name === "Mobile Safari"
const isSupported =
os.name !== "iOS" ||
versionAtLeast(coerceSemver(os.version) ?? "0.0.0", "16.4.0")
return json({ isSupported, isMobileSafari })
return json({
isSupported,
isMobileSafari,
name: os.name,
version: os.version,
})
}
export default function Index() {
const { isSupported, isMobileSafari } = useLoaderData<typeof loader>()
const { isSupported, isMobileSafari, name, version } =
useLoaderData<typeof loader>()
console.log(name, version)
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
@ -54,23 +63,22 @@ function LandingMessage({
}) {
const { installed } = useInstallState({ isSupported, isMobileSafari })
const [rendered, setRendered] = useState(false)
const [isInstalled, setIsInstalled] = useState(installed)
useEffect(() => {
setRendered(true)
}, [])
return !rendered ? (
<div>Loading</div>
) : isInstalled ? (
<div>Your Notifications</div>
) : (
<InstallPrompts
isMobileSafari={isMobileSafari}
isSupported={isSupported}
notificationsEnabled={false}
onInstallComplete={() => setIsInstalled(true)}
/>
return (
<ClientOnly fallback={<div>Loading</div>}>
{() =>
isInstalled ? (
<div>Your Notifications</div>
) : (
<InstallPrompts
isMobileSafari={isMobileSafari}
isSupported={isSupported}
notificationsEnabled={false}
onInstallComplete={() => setIsInstalled(true)}
/>
)
}
</ClientOnly>
)
}

View File

@ -1,4 +1,4 @@
import { usePush } from "remix-pwa-monorepo/packages/push/client/hook"
import { usePush } from "./usePush"
type IOSInstallStep =
| "loading"
@ -14,7 +14,7 @@ export default function useInstallState({
isSupported: boolean
isMobileSafari: boolean
}) {
const isClient = typeof window !== undefined
const isClient = typeof window !== "undefined"
if (!isClient)
return {
@ -34,13 +34,15 @@ export default function useInstallState({
matchMedia("(dislay-mode: standalone)").matches
return {
step: !isMobileSafari
? "open safari"
: !isRunningPWA && isMobileSafari
? "install"
: !notificationsEnabled
? "enable notifications"
: (null as IOSInstallStep | null),
step: !isSupported
? "unsupported"
: !isMobileSafari
? "open safari"
: !isRunningPWA && isMobileSafari
? "install"
: !notificationsEnabled
? "enable notifications"
: (null as IOSInstallStep | null),
installed: notificationsEnabled,
}
}

183
app/usePush.ts Normal file
View File

@ -0,0 +1,183 @@
import { useEffect, useState } from "react"
export type PushObject = {
/**
* Boolean state indicating whether the user is subscribed to push notifications or not.
*/
isSubscribed: boolean
/**
* The push subscription object
*/
pushSubscription: PushSubscription | null
/**
* Request permission for push notifications
* @returns The permission status of the push notifications
*/
requestPermission: () => NotificationPermission
/**
* Utility to subscribe to push notifications
* @param publicKey the public vapid key
* @param callback a callback function to be called when the subscription is successful
* @param errorCallback a callback function to be called if the subscription fails
*/
subscribeToPush: (
publicKey: string,
callback?: (subscription: PushSubscription) => void,
errorCallback?: (error: any) => void
) => void
/**
* Utility to unsubscribe from push notifications
* @param callback a callback function to be called when the unsubscription is successful
* @param errorCallback a callback function to be called if the unsubscription fails
*/
unsubscribeFromPush: (
callback?: () => void,
errorCallback?: (error: any) => void
) => void
/**
* Boolean state indicating whether the user has allowed sending of push notifications or not.
*/
canSendPush: boolean
}
/**
* Push API hook - contains all your necessities for handling push notifications in the client
*/
export const usePush = (): PushObject => {
const [swRegistration, setSWRegistration] =
useState<ServiceWorkerRegistration | null>(null)
const [isSubscribed, setIsSubscribed] = useState<boolean>(false)
const [pushSubscription, setPushSubscription] =
useState<PushSubscription | null>(null)
const [canSendPush, setCanSendPush] = useState<boolean>(false)
const requestPermission = () => {
if (canSendPush) return "granted"
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
setCanSendPush(true)
return permission
} else {
setCanSendPush(false)
return permission
}
})
return "default"
}
const subscribeToPush = (
publicKey: string,
callback?: (subscription: PushSubscription) => void,
errorCallback?: (error: any) => void
) => {
if (swRegistration === null || swRegistration.pushManager === undefined)
return
swRegistration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey,
})
.then(
(subscription) => {
setIsSubscribed(true)
setPushSubscription(subscription)
callback && callback(subscription)
},
(error) => {
errorCallback && errorCallback(error)
}
)
}
const unsubscribeFromPush = (
callback?: () => void,
errorCallback?: (error: any) => void
) => {
if (swRegistration === null || swRegistration.pushManager === undefined)
return
swRegistration.pushManager
.getSubscription()
.then((subscription) => {
if (subscription) {
subscription.unsubscribe().then(
() => {
setIsSubscribed(false)
setPushSubscription(null)
callback && callback()
},
(error) => {
errorCallback && errorCallback(error)
}
)
}
})
.catch((error) => {
errorCallback && errorCallback(error)
})
}
useEffect(() => {
if (typeof window === "undefined") return
const getRegistration = async () => {
if ("serviceWorker" in navigator) {
try {
const _registration = await navigator.serviceWorker.getRegistration()
setSWRegistration(_registration ?? null)
} catch (err) {
console.error("Error getting service worker registration:", err)
}
} else {
console.warn("Service Workers are not supported in this browser.")
}
}
const handleControllerChange = () => {
getRegistration()
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener(
"controllerchange",
handleControllerChange
)
}
getRegistration()
return () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.removeEventListener(
"controllerchange",
handleControllerChange
)
}
}
}, [])
useEffect(() => {
if (swRegistration && swRegistration.pushManager !== undefined) {
swRegistration.pushManager.getSubscription().then((subscription) => {
setIsSubscribed(!!subscription)
setPushSubscription(subscription)
})
Notification.permission === "granted"
? setCanSendPush(true)
: setCanSendPush(false)
}
}, [swRegistration])
return {
isSubscribed,
pushSubscription,
requestPermission,
subscribeToPush,
unsubscribeFromPush,
canSendPush,
}
}

View File

@ -1,5 +1,4 @@
import { test, expect, Page } from "@playwright/test"
import { before } from "node:test"
const { describe, beforeEach, skip, use } = test
@ -21,7 +20,7 @@ describe("when user is on iOS", () => {
describe("and the browser is not safari", () => {
use({
userAgent:
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en-gb) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3",
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 16_4 like Mac OS X; en-gb) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3",
})
test("the user is told they need to install through Safari, because reasons, I guess", async ({
@ -29,9 +28,9 @@ describe("when user is on iOS", () => {
}) => {
await page.goto("/")
const installText = await page.getByText(/Safari/)
const safariText = await page.getByText(/Open tackupnow.com in Safari/)
await expect(installText).toBeAttached()
await expect(safariText).toBeAttached()
})
})
@ -58,7 +57,7 @@ describe("when user is on iOS", () => {
await stubNotifications(page, { permission: "default" })
await page.goto("/")
const enableButton = await page.getByText(/Enable notifications/)
const enableButton = await page.getByText(/Enable Notifications/)
await expect(enableButton).toBeAttached()
})
@ -80,7 +79,7 @@ describe("when user is on iOS", () => {
await page.goto("/")
const requestPromise = page.waitForRequest("/api/subscribe")
await page.getByText(/Enable notifications/).click()
await page.getByText(/Enable Notifications/).click()
const request = await requestPromise
await expect(request.postDataJSON()).toEqual({ hi: "subscription" })
@ -92,7 +91,7 @@ describe("when user is on iOS", () => {
await page.goto("/")
const requestPromise = page.waitForRequest("/api/subscribe")
await page.getByText(/Enable notifications/).click()
await page.getByText(/Enable Notifications/).click()
await requestPromise
const yourNotificationsHeading =
@ -112,7 +111,7 @@ describe("when user is on iOS", () => {
await page.goto("/")
await page.evaluate(async () => await navigator.serviceWorker.ready)
const notificationText = await page.getByText(/Enable notifications/)
const notificationText = await page.getByText(/Enable Notifications/)
await expect(notificationText).not.toBeAttached()
})

88
package-lock.json generated
View File

@ -5,6 +5,7 @@
"packages": {
"": {
"name": "site",
"hasInstallScript": true,
"dependencies": {
"@remix-pwa/sw": "^3.0.9",
"@remix-pwa/worker-runtime": "^2.1.4",
@ -17,8 +18,6 @@
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remix-pwa-monorepo": "github:remix-pwa/monorepo#main",
"remix-utils": "^7.6.0",
"semver": "^7.6.3",
"ua-parser-js": "^1.0.39"
},
@ -14298,91 +14297,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remix-pwa-monorepo": {
"resolved": "git+ssh://git@github.com/remix-pwa/monorepo.git#dda9d68b1c69642679d6ff17658f21fe24c668d6",
"license": "MIT",
"workspaces": [
"packages/cli",
"packages/client",
"packages/dev",
"packages/eslint-config",
"packages/lint-staged-config",
"packages/push",
"packages/sw",
"packages/sync",
"packages/worker-runtime",
"playground"
],
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/remix-utils": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/remix-utils/-/remix-utils-7.6.0.tgz",
"integrity": "sha512-BPhCUEy+nwrhDDDg2v3+LFSszV6tluMbeSkbffj2o4tqZxt5Kn69Y9sNpGxYLAj8gjqeYDuxjv55of+gYnnykA==",
"dependencies": {
"type-fest": "^4.3.3"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@remix-run/cloudflare": "^2.0.0",
"@remix-run/deno": "^2.0.0",
"@remix-run/node": "^2.0.0",
"@remix-run/react": "^2.0.0",
"@remix-run/router": "^1.7.2",
"crypto-js": "^4.1.1",
"intl-parse-accept-language": "^1.0.0",
"is-ip": "^5.0.1",
"react": "^18.0.0",
"zod": "^3.22.4"
},
"peerDependenciesMeta": {
"@remix-run/cloudflare": {
"optional": true
},
"@remix-run/deno": {
"optional": true
},
"@remix-run/node": {
"optional": true
},
"@remix-run/react": {
"optional": true
},
"@remix-run/router": {
"optional": true
},
"crypto-js": {
"optional": true
},
"intl-parse-accept-language": {
"optional": true
},
"is-ip": {
"optional": true
},
"react": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/remix-utils/node_modules/type-fest": {
"version": "4.26.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz",
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View File

@ -10,7 +10,8 @@
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"watch": "jest --watch --config=jest.config.ts",
"test": "jest --config=jest.config.ts"
"test": "jest --config=jest.config.ts",
"postinstall": "patch-package"
},
"dependencies": {
"@remix-pwa/sw": "^3.0.9",
@ -24,8 +25,6 @@
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remix-pwa-monorepo": "github:remix-pwa/monorepo#main",
"remix-utils": "^7.6.0",
"semver": "^7.6.3",
"ua-parser-js": "^1.0.39"
},
@ -76,4 +75,4 @@
"engines": {
"node": ">=18.0.0"
}
}
}