From cc6eaa652650bd6067a7af8a4320354abadbc38d Mon Sep 17 00:00:00 2001 From: phil Date: Mon, 14 Jul 2025 17:06:57 -0400 Subject: [PATCH] split up and nest the different runnign reqs --- atproto-notifications/package-lock.json | 38 +++ atproto-notifications/package.json | 1 + atproto-notifications/src/App.tsx | 240 ++---------------- .../src/components/Fetch.tsx | 27 ++ atproto-notifications/src/components/User.tsx | 13 + .../src/components/WhoAmI.tsx | 4 +- .../src/components/setup/Chrome.tsx | 71 ++++++ .../components/setup/WithFeatureChecks.tsx | 21 ++ .../setup/WithNotificationPermission.tsx | 36 +++ .../components/setup/WithPushSubscription.tsx | 77 ++++++ .../src/components/setup/WithServerHello.tsx | 59 +++++ .../components/setup/WithServiceWorker.tsx | 29 +++ atproto-notifications/src/context.ts | 4 + atproto-notifications/src/main.tsx | 12 +- server/index.js | 12 +- 15 files changed, 420 insertions(+), 224 deletions(-) create mode 100644 atproto-notifications/src/components/User.tsx create mode 100644 atproto-notifications/src/components/setup/Chrome.tsx create mode 100644 atproto-notifications/src/components/setup/WithFeatureChecks.tsx create mode 100644 atproto-notifications/src/components/setup/WithNotificationPermission.tsx create mode 100644 atproto-notifications/src/components/setup/WithPushSubscription.tsx create mode 100644 atproto-notifications/src/components/setup/WithServerHello.tsx create mode 100644 atproto-notifications/src/components/setup/WithServiceWorker.tsx create mode 100644 atproto-notifications/src/context.ts diff --git a/atproto-notifications/package-lock.json b/atproto-notifications/package-lock.json index dc0259b..5b6544a 100644 --- a/atproto-notifications/package-lock.json +++ b/atproto-notifications/package-lock.json @@ -14,6 +14,7 @@ "psl": "^1.15.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-router": "^7.6.3", "react-time-ago": "^7.3.3" }, "devDependencies": { @@ -2009,6 +2010,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3069,6 +3079,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", + "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-time-ago": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.3.3.tgz", @@ -3193,6 +3225,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/atproto-notifications/package.json b/atproto-notifications/package.json index 653bcb1..6797f36 100644 --- a/atproto-notifications/package.json +++ b/atproto-notifications/package.json @@ -17,6 +17,7 @@ "psl": "^1.15.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-router": "^7.6.3", "react-time-ago": "^7.3.3" }, "devDependencies": { diff --git a/atproto-notifications/src/App.tsx b/atproto-notifications/src/App.tsx index a5e88f9..8b31a6d 100644 --- a/atproto-notifications/src/App.tsx +++ b/atproto-notifications/src/App.tsx @@ -3,228 +3,36 @@ import { useLocalStorage } from "@uidotdev/usehooks"; import { GetJson } from './components/fetch'; import { WhoAmI } from './components/WhoAmI'; import { Feed } from './components/Feed'; +import { WithFeatureChecks } from './components/setup/WithFeatureChecks'; +import { WithServiceWorker } from './components/setup/WithServiceWorker'; +import { WithServerHello } from './components/setup/WithServerHello'; +import { UserContext } from './context'; +import { WithNotificationPermission } from './components/setup/WithNotificationPermission'; +import { WithPushSubscription } from './components/setup/WithPushSubscription'; import { urlBase64ToUint8Array } from './utils'; import './App.css' import lexicons from 'lexicons'; -const Problem = ({ children }) => ( -
-

Sorry, {children}

-
-); - -function requestPermission(pushServerHost, pushServerPubkey, setAsking, setPermissionError) { - return async () => { - setAsking(true); - let err; - try { - await Notification.requestPermission(); - const sub = await subscribeToPush(pushServerPubkey); - const res = await fetch(`${pushServerHost}/subscribe`, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ sub }), - credentials: 'include', - }); - if (!res.ok) { - let content; - try { - content = (await res.json()).reason; - } catch (_) { - content = await res.text(); - } - throw content; - } - } catch (e) { - setPermissionError(e); - err = e; - } - setAsking(false); - if (err) throw err; - } -} - -let autoreg; -async function subscribeToPush(pushServerPubkey) { - const registration = await navigator.serviceWorker.register('/service-worker.js'); - - // auto-update the service worker in case they keep it open in a tab for a long time - clearInterval(autoreg); - autoreg = setInterval(() => registration.update(), 4 * 60 * 60 * 1000); // every 4h - - return await registration.pushManager.subscribe({ - pushServerPubkey: urlBase64ToUint8Array(pushServerPubkey), - userVisibleOnly: true, - }); -} - -async function verifyUser(host, token) { - let res = await fetch(`${host}/verify`, { - method: 'POST', - headers: {'Content-Type': 'applicaiton/json'}, - body: JSON.stringify({ token }), - credentials: 'include', - }); - if (!res.ok) throw res; - return await res.json(); -} - -function App() { - const host = import.meta.env.VITE_NOTIFICATIONS_HOST; - const [pushPubkey, setPushPubkey] = useState(null); - const [whoamiHost, setWhoamiHost] = useState(null); - - const [role, setRole] = useLocalStorage('spacedust-notif-role', 'anonymous'); - const [user, setUser] = useLocalStorage('spacedust-notif-user', null); - const [verif, setVerif] = useState(null); - const [asking, setAsking] = useState(false); - const [permissionError, setPermissionError] = useState(null); - - const onIdentify = useCallback(async details => { - setVerif('verifying'); - try { - const info = await verifyUser(host, details.token); - setVerif('verified'); - setRole(info.role); - setUser(details); - } catch (e) { - console.error(e); - setVerif('failed'); - } - }, [host]); - - const logout = useCallback(async () => { - setRole('anonymous'); - setUser(null); - // TODO: clear indexeddb - await fetch(`${host}/logout`, { - method: 'POST', - credentials: 'include', - }); - }); - - let hasSW = 'serviceWorker' in navigator; - let hasPush = 'PushManager' in window; - let notifPerm = Notification?.permission ?? 'default'; - - function Blah({ info }) { - useEffect(() => { - setPushPubkey(info.webPushPublicKey); - setWhoamiHost(info.whoamiHost); - setRole(info.role); - if (info.role === 'anonymous') { - setUser(null); - } - }); - return <>Got server hello, updating app state…; - } - - let content; - if (!hasSW) { - content = your browser does not support the background task needd to deliver notifications; - } else if (!hasPush) { - content = your browser does not support registering push notifications. - } else if (!whoamiHost) { - content = } credentials /> - } else if (!user) { - if (verif === 'verifying') content =

verifying…

; - else { - content = ; - if (verif === 'failed') { - content = <>

Sorry, failed to verify that identity. please let us know!

{content}; - } - } - } else if (permissionError !== null || notifPerm !== 'granted' || role === 'anonymous') { - content = ( - <> -

Step 2: Allow notifications

-

To show notifications we need permission:

-

- -

- {notifPerm === 'denied' ? ( -

Notification permission was denied. You may need to clear the browser setting to try again.

- ) : permissionError ? ( -

Sorry, something went wrong: {permissionError}

- ) : ( -

You can revoke this any time

- ) - } - - ); - } else { - content = ; - } +export function App({ children }) { return ( - <> -
-

spacedust notifications demo!

- {user && ( -
-

- @{user.handle} - {/* TODO: clear *all* info on logout */} - -

-
- )} -
- -
- {content} -
- -
-

- This demo is part of - {' '} - - m - i - c - r - o - c - o - s - m - - -

-

- - 🦋 follow - - - 💸 support - - - 👩🏻‍💻 source - -

- -

- secret dev setting: - {' '} - -

-
- - ) + + + + + + + + + + + + ); } -export default App; +// const Problem = ({ children }) => ( +//
+//

Sorry, {children}

+//
+// ); diff --git a/atproto-notifications/src/components/Fetch.tsx b/atproto-notifications/src/components/Fetch.tsx index f7269bf..c528a81 100644 --- a/atproto-notifications/src/components/Fetch.tsx +++ b/atproto-notifications/src/components/Fetch.tsx @@ -66,3 +66,30 @@ export function GetJson({ endpoint, params, credentials, ...forFetch }) { /> ); } + +async function postJson(url, body, credentials) { + const opts = { + method: 'POST', + headers: {'Content-Type': 'applicaiton/json'}, + body, + }; + if (credentials) opts.credentials = 'include'; + const res = await fetch(url, opts); + if (!res.ok) { + const m = await res.text(); + throw new Error(`Failed to fetch: ${m}`); + } + return await res.json(); +} + +export function PostJson({ endpoint, data, credentials, ...forFetch }) { + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; + const url = new URL(endpoint, host); + return ( + + ); +} diff --git a/atproto-notifications/src/components/User.tsx b/atproto-notifications/src/components/User.tsx new file mode 100644 index 0000000..05eb3c0 --- /dev/null +++ b/atproto-notifications/src/components/User.tsx @@ -0,0 +1,13 @@ +import { resolveDid } from '../atproto/resolve'; +import { Fetch } from './fetch'; + +export function Handle({ did }) { + return ( + <>@…} + ok={handle => <>@{handle}} + /> + ); +} diff --git a/atproto-notifications/src/components/WhoAmI.tsx b/atproto-notifications/src/components/WhoAmI.tsx index 1b026fb..c962c27 100644 --- a/atproto-notifications/src/components/WhoAmI.tsx +++ b/atproto-notifications/src/components/WhoAmI.tsx @@ -19,8 +19,8 @@ export function WhoAmI({ onIdentify, origin }) {