- /subscribe + /native/poll/register carry the enabled categories; the server stores them per subscription/token and only pushes matching ones (DM wrap -> dms; offer/counter/accept/attestation/shipment -> trades). Absent = legacy (all), empty array = none. - /pending uses a per-category activity breakdown so it only moves for the token's categories; the smoke test proves a DM does NOT wake a trades-only token. - usePushNotifications/useBackgroundDelivery send categories (from device prefs) and expose resubscribe(); NotificationSettingsCard re-sends on toggle change.
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
/**
|
|
* Client for the SkateSpots push service (`push-service/`).
|
|
*
|
|
* The service watches relays for push-marked gift wraps and sends a CONTENT-FREE
|
|
* Web Push. It never decrypts; the app fetches + decrypts the named wrap itself.
|
|
*
|
|
* Base URL mirrors `sasquatchAiBase()`: production (and native builds) use the
|
|
* deployed host; dev points at the local service. `VITE_PUSH_SERVICE_BASE`
|
|
* overrides both.
|
|
*/
|
|
import { isNativePlatform } from '@/lib/platform';
|
|
|
|
const PRODUCTION_PUSH_BASE = 'https://push.skate-spots.org';
|
|
|
|
export function pushServiceBase(): string {
|
|
const override = (import.meta.env as Record<string, string | undefined>).VITE_PUSH_SERVICE_BASE;
|
|
if (override) return override.replace(/\/+$/, '');
|
|
if (import.meta.env.PROD || isNativePlatform()) return PRODUCTION_PUSH_BASE;
|
|
return 'http://localhost:8788';
|
|
}
|
|
|
|
/** Standard `PushSubscription.toJSON()` shape the service expects. */
|
|
export interface PushSubscriptionJSON {
|
|
endpoint: string;
|
|
keys: { p256dh: string; auth: string };
|
|
}
|
|
|
|
async function post<T>(path: string, body: unknown): Promise<T> {
|
|
const res = await fetch(`${pushServiceBase()}${path}`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const data = (await res.json().catch(() => ({}))) as T & { error?: string };
|
|
if (!res.ok) throw new Error(data.error || `Push service error (${res.status})`);
|
|
return data;
|
|
}
|
|
|
|
export async function fetchVapidPublicKey(): Promise<string> {
|
|
const res = await fetch(`${pushServiceBase()}/vapid-public-key`);
|
|
if (!res.ok) throw new Error('Could not reach the notification service.');
|
|
const data = (await res.json()) as { key?: string };
|
|
if (!data.key) throw new Error('Notification service returned no key.');
|
|
return data.key;
|
|
}
|
|
|
|
export async function requestPushChallenge(pubkey: string): Promise<string> {
|
|
const data = await post<{ challenge: string }>('/auth/challenge', { pubkey });
|
|
return data.challenge;
|
|
}
|
|
|
|
export async function subscribeToPush(
|
|
subscription: PushSubscriptionJSON,
|
|
proof: unknown,
|
|
categories?: string[],
|
|
): Promise<void> {
|
|
await post('/subscribe', { subscription, proof, categories });
|
|
}
|
|
|
|
export async function unsubscribeFromPush(endpoint: string, proof: unknown): Promise<void> {
|
|
await post('/unsubscribe', { endpoint, proof });
|
|
}
|