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 }) => (
-
-);
-
-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 (
- <>
-
-
-
- {content}
-
-
-
- >
- )
+
+
+
+
+
+
+
+
+
+
+
+ );
}
-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 }) {