Files
dumpsterChat/web/src/stores/push.ts
T
hobokenchicken bb0540b70c Finish Phase 6: Push frontend, WebAuthn frontend, OpenAPI/Swagger
Frontend:
- stores/push.ts: fixed VAPID key endpoint, added checkSubscription, proper subscription format
- UserSettings.tsx: added [ENABLE PUSH] toggle and [REGISTER PASSKEY] button
- LoginForm.tsx: added [SIGN IN WITH PASSKEY] button

Backend:
- cmd/server/main.go: added Swagger annotations, /docs/* route
- docs/: generated swagger.json, swagger.yaml, docs.go
- Makefile: added docs target for swag init
2026-06-28 18:53:05 -04:00

102 lines
3.1 KiB
TypeScript

import { create } from 'zustand';
import { api } from '../lib/api.ts';
interface PushState {
isSupported: boolean;
isSubscribed: boolean;
permission: NotificationPermission;
subscribe: () => Promise<void>;
unsubscribe: () => Promise<void>;
requestPermission: () => Promise<NotificationPermission>;
checkSubscription: () => Promise<void>;
}
function urlBase64ToUint8Array(base64String: string): BufferSource {
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;
}
export const usePushStore = create<PushState>((set, get) => ({
isSupported: 'serviceWorker' in navigator && 'PushManager' in window,
isSubscribed: false,
permission: typeof Notification !== 'undefined' ? Notification.permission : 'default',
requestPermission: async () => {
if (!get().isSupported) return 'denied';
const permission = await Notification.requestPermission();
set({ permission });
return permission;
},
checkSubscription: async () => {
if (!get().isSupported) return;
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
set({ isSubscribed: !!subscription });
} catch {
// ignore
}
},
subscribe: async () => {
if (!get().isSupported) return;
try {
// Request notification permission first
const permission = await get().requestPermission();
if (permission !== 'granted') return;
// Register service worker
const registration = await navigator.serviceWorker.register('/sw.js');
await navigator.serviceWorker.ready;
// Get VAPID public key from server
const { publicKey } = await api.get<{ publicKey: string }>('/push/vapid-public-key');
if (!publicKey) return;
// Subscribe to push
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
const sub = subscription.toJSON();
const keys = sub.keys as { p256dh: string; auth: string };
// Send subscription to server
await api.post('/push/subscribe', {
endpoint: sub.endpoint!,
p256dh: keys.p256dh,
auth: keys.auth,
});
set({ isSubscribed: true });
} catch (error) {
console.error('Failed to subscribe to push:', error);
}
},
unsubscribe: async () => {
if (!get().isSupported) return;
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await api.post('/push/unsubscribe', { endpoint: subscription.endpoint });
await subscription.unsubscribe();
}
set({ isSubscribed: false });
} catch (error) {
console.error('Failed to unsubscribe from push:', error);
}
},
}));