Compare commits

..

No commits in common. "987b419018292734123784cb2c4e14308929ad79" and "784eccb92a6ac36f5d97e7b03f3c484514b060ca" have entirely different histories.

14 changed files with 73 additions and 273 deletions

View File

@ -3,7 +3,6 @@
"tabWidth": 2, "tabWidth": 2,
"semi": false, "semi": false,
"singleQuote": false, "singleQuote": false,
"maxLineLength": 120,
"overrides": [ "overrides": [
{ {
"files": [ "files": [

View File

@ -1,5 +0,0 @@
import type { Response, Request } from "express"
export function subscribe(req: Request, res: Response) {
res.status(200).send()
}

View File

@ -1,61 +0,0 @@
import React from "react"
import { useEffect } from "react"
import { usePush } from "remix-pwa-monorepo/packages/push/client/hook"
export default function EnableNotifications({
onSubscribe,
}: {
onSubscribe: () => void
}) {
return (
<div>
<h1>Allow Notifications</h1>
<div>
Tack Up Now requires your permission to send notifications in order to
function properly
</div>
<EnableButton onSubscribe={onSubscribe} />
</div>
)
}
function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
const { subscribeToPush, requestPermission, canSendPush } = usePush()
function subscribe() {
requestPermission()
}
useEffect(() => {
if (!canSendPush) return
subscribeToPush(
urlB64ToUint8Array(applicationServerPublicKey) as any,
(subscription) => {
fetch("/api/subscribe", {
method: "POST",
body: JSON.stringify(subscription),
})
onSubscribe()
}
)
}, [canSendPush])
return <button onClick={subscribe}>Enable Notifications</button>
}
const applicationServerPublicKey =
"BDTbzdtzJxwV0sscdsXla-GKvlcxqQr7edEfkX8-papwvvV1UVc3IMyRacl1BbgTi31nWPji2wKCZkjf1l5iX7Y"
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 outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}

View File

@ -1,32 +0,0 @@
import React from "react"
import shareIcon from "../../public/images/safari-share-icon.png"
export default function InstallPWA() {
return (
<div>
<h1>Install Tack Up Now!</h1>
<div>
Install Tack Up Now on your device to get notified when it's time to
tack up
</div>
<div>
<ul>
<span>Tap </span>
<img style={{ width: "2rem" }} src={shareIcon} />
<span> and choose </span>
<span className="bold">Add to Home Screen</span>
</ul>
<ul>
<span>On the next screen, tap </span>
<span className="bold">Add</span>
</ul>
<ul>
<span>Then open </span>
<span className="bold">Tack Up Now</span>
<span> from your home screen</span>
</ul>
</div>
</div>
)
}

View File

@ -1,32 +0,0 @@
import React from "react"
import useInstallState from "../useInstallState"
import EnableNotifications from "./EnableNotifications"
import OpenSafari from "./OpenSafari"
import InstallPWA from "./InstallPWA"
import Unsupported from "./Unsupported"
interface InstallPromptsProps {
isMobileSafari: boolean
isSupported: boolean
notificationsEnabled: boolean
onInstallComplete: () => void
}
export default function InstallPrompts({
isSupported,
isMobileSafari,
onInstallComplete,
}: InstallPromptsProps) {
const { step } = useInstallState({ isSupported, isMobileSafari })
const steps = {
"open safari": <OpenSafari />,
install: <InstallPWA />,
"enable notifications": (
<EnableNotifications onSubscribe={onInstallComplete} />
),
unsupported: <Unsupported />,
}
return steps[step as keyof typeof steps] ?? null
}

View File

@ -1,11 +0,0 @@
import React from "react"
export default function OpenSafari() {
return (
<div>
<div>This device requires Tack Up Now to be installed using Safari</div>
<br />
<div>Open tackupnow.com in Safari to continue!</div>
</div>
)
}

View File

@ -1,22 +0,0 @@
import React, { useState } from "react"
export default function Unsupported() {
const [showWhy, setShowWhy] = useState(false)
return (
<div>
<h1>{"Sorry :("}</h1>
<br />
<div>Your device doesn't support Tack Up Now!</div>
{showWhy ? (
<div>
iOS 16.3 and under does not support notification delivery through web
apps, so Tack Up Now can't send notifications to your device.
</div>
) : (
<button onClick={() => setShowWhy(true)}>Why not?</button>
)}
</div>
)
}

View File

@ -7,10 +7,6 @@ import {
} from "@remix-run/react" } from "@remix-run/react"
import React from "react" import React from "react"
import { ManifestLink } from "@remix-pwa/sw" import { ManifestLink } from "@remix-pwa/sw"
import { LinksFunction } from "@remix-run/node"
import styles from "./styles/styles.css?url"
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
return ( return (

View File

@ -8,8 +8,7 @@ import React, { Suspense, useEffect, useState } from "react"
import coerceSemver from "semver/functions/coerce" import coerceSemver from "semver/functions/coerce"
import versionAtLeast from "semver/functions/gte" import versionAtLeast from "semver/functions/gte"
import UAParser from "ua-parser-js" import UAParser from "ua-parser-js"
import InstallPrompts from "../install/InstallPrompts" import { usePush } from "remix-pwa-monorepo/packages/push/client/hook"
import useInstallState from "../useInstallState"
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
@ -20,57 +19,101 @@ export const meta: MetaFunction = () => {
export const loader = async ({ request }: LoaderFunctionArgs) => { export const loader = async ({ request }: LoaderFunctionArgs) => {
const userAgent = request.headers.get("user-agent") const userAgent = request.headers.get("user-agent")
const parsedUserAgent = new UAParser(userAgent ?? "") const os = new UAParser(userAgent ?? "").getOS()
const os = parsedUserAgent.getOS()
const isMobileSafari = parsedUserAgent.getBrowser().name !== "Mobile Safari"
const isSupported = const isSupported =
os.name !== "iOS" || os.name !== "iOS" ||
versionAtLeast(coerceSemver(os.version) ?? "0.0.0", "16.4.0") versionAtLeast(coerceSemver(os.version) ?? "0.0.0", "16.4.0")
return json({ isSupported, isMobileSafari }) return json({ isSupported })
} }
export default function Index() { export default function Index() {
const { isSupported, isMobileSafari } = useLoaderData<typeof loader>() const { isSupported } = useLoaderData<typeof loader>()
return ( return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}> <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<Suspense> <Suspense>
<LandingMessage <LandingMessage isSupported={isSupported} />
isSupported={isSupported}
isMobileSafari={isMobileSafari}
/>
</Suspense> </Suspense>
</div> </div>
) )
} }
function LandingMessage({ function LandingMessage({ isSupported }: { isSupported: boolean }) {
isSupported, const isClient = typeof window !== "undefined"
isMobileSafari, const notificationsEnabled =
}: { isClient &&
isSupported: boolean "Notification" in window &&
isMobileSafari: boolean window.Notification.permission === "granted"
}) {
const { installed } = useInstallState({ isSupported, isMobileSafari }) const isRunningPWA =
isClient &&
(("standalone" in navigator && (navigator.standalone as boolean)) ||
matchMedia("(dislay-mode: standalone)").matches)
const [rendered, setRendered] = useState(false) const [rendered, setRendered] = useState(false)
const [isInstalled, setIsInstalled] = useState(installed) const [isInstalled, setIsInstalled] = useState(notificationsEnabled)
useEffect(() => { useEffect(() => {
setRendered(true) setRendered(true)
}, []) }, [])
return !rendered ? ( return !isClient || !rendered ? (
<div>Loading</div> <div>Loading</div>
) : isInstalled ? ( ) : isInstalled ? (
<div>Your Notifications</div> <div>Your Notifications</div>
) : isRunningPWA && !notificationsEnabled ? (
<EnableButton onSubscribe={() => setIsInstalled(true)} />
) : isSupported ? (
<div>Install Tack Up Now!</div>
) : ( ) : (
<InstallPrompts <div>{"Sorry, your device doesn't support Tack Up Now! :("}</div>
isMobileSafari={isMobileSafari}
isSupported={isSupported}
notificationsEnabled={false}
onInstallComplete={() => setIsInstalled(true)}
/>
) )
} }
function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
const { subscribeToPush, requestPermission, canSendPush } = usePush()
async function subscribe() {
console.log("Hey the thing was clicked wow")
requestPermission()
}
useEffect(() => {
if (!canSendPush) return
subscribeToPush(
urlB64ToUint8Array(applicationServerPublicKey) as any,
(subscription) => {
fetch("/subscribe", {
method: "POST",
body: JSON.stringify(subscription),
})
onSubscribe()
},
(error) => {
const errorDiv = document.createElement("div")
errorDiv.innerText = "Error thingy" + JSON.stringify(error)
document.appendChild(errorDiv)
}
)
}, [canSendPush])
return <button onClick={subscribe}>Enable notifications</button>
}
const applicationServerPublicKey =
"BDTbzdtzJxwV0sscdsXla-GKvlcxqQr7edEfkX8-papwvvV1UVc3IMyRacl1BbgTi31nWPji2wKCZkjf1l5iX7Y"
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 outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}

View File

@ -1,10 +0,0 @@
.bold {
font-weight: bold;
}
ul {
display: flex;
flex-direction: row;
align-items: center;
white-space: pre-wrap;
}

View File

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

View File

@ -18,23 +18,6 @@ describe("when user is on iOS", () => {
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1",
}) })
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",
})
test("the user is told they need to install through Safari, because reasons, I guess", async ({
page,
}) => {
await page.goto("/")
const installText = await page.getByText(/Safari/)
await expect(installText).toBeAttached()
})
})
test("and tack up now is not running as a PWA, they are instructed to install tack up now", async ({ test("and tack up now is not running as a PWA, they are instructed to install tack up now", async ({
page, page,
}) => { }) => {
@ -69,7 +52,7 @@ describe("when user is on iOS", () => {
permission: "default", permission: "default",
requestPermissionResult: "granted", requestPermissionResult: "granted",
}) })
await page.route("/api/subscribe", async (route) => { await page.route("/subscribe", async (route) => {
await route.fulfill() await route.fulfill()
}) })
}) })
@ -79,7 +62,7 @@ describe("when user is on iOS", () => {
}) => { }) => {
await page.goto("/") await page.goto("/")
const requestPromise = page.waitForRequest("/api/subscribe") const requestPromise = page.waitForRequest("/subscribe")
await page.getByText(/Enable notifications/).click() await page.getByText(/Enable notifications/).click()
const request = await requestPromise const request = await requestPromise
@ -91,7 +74,7 @@ describe("when user is on iOS", () => {
}) => { }) => {
await page.goto("/") await page.goto("/")
const requestPromise = page.waitForRequest("/api/subscribe") const requestPromise = page.waitForRequest("/subscribe")
await page.getByText(/Enable notifications/).click() await page.getByText(/Enable notifications/).click()
await requestPromise await requestPromise

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -3,12 +3,10 @@ import express from "express"
// notice that the result of `remix vite:build` is "just a module" // notice that the result of `remix vite:build` is "just a module"
import * as build from "./build/server/index.js" import * as build from "./build/server/index.js"
import { subscribe } from "./api/subscribe.js"
const app = express() const app = express()
app.use(express.static("build/client")) app.use(express.static("build/client"))
app.get("/api", (req, res) => res.send("HI")) app.get("/api", (req, res) => res.send("HI"))
app.post("/api/subscribe", subscribe)
// and your app is "just a request handler" // and your app is "just a request handler"
app.all("*", createRequestHandler({ build })) app.all("*", createRequestHandler({ build }))