SkateSpots/src/hooks/useBackgroundDelivery.ts
SkateSpots 4df961b015 feat(push): category-aware notifications (trades-only actually means trades-only)
- /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.
2026-10-01 20:01:07 +07:00

85 lines
3.6 KiB
TypeScript

/**
* useBackgroundDelivery — Android-only closed-app notifications, **without
* Google**. Registers a poll token with our `push-service` and hands the native
* `skatespots-background` plugin the base URL + token so its worker can ask
* "anything new since N?" and raise a local notification.
*
* Two modes (chosen in Settings): `standard` = WorkManager ~15 min (best-effort),
* `fast` = a foreground service ~5 min (sticky notification).
*/
import { useCallback, useMemo, useState } from 'react';
import { registerPlugin } from '@capacitor/core';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNotificationPrefs } from '@/hooks/useNotificationPrefs';
import { NOTIFICATION_CATEGORIES } from '@/lib/notificationPrefs';
import { isNativePlatform, getPlatform } from '@/lib/platform';
import { pushServiceBase, requestPushChallenge } from '@/lib/pushService';
type DeliveryMode = 'off' | 'standard' | 'fast';
/**
* Registered by NAME (`@capacitor/core`), not imported from the package — the
* plugin's TS source lives in `packages/skatespots-background` and is only a
* build-time dependency of the Android shell (Capacitor finds it via package.json).
*/
interface SkateSpotsBackgroundPlugin {
configure(options: { mode: DeliveryMode; intervalMinutes?: number }): Promise<void>;
seed(options: { baseUrl: string; token: string; since: number }): Promise<void>;
clearNotifications(): Promise<void>;
}
const SkateSpotsBackground = registerPlugin<SkateSpotsBackgroundPlugin>('SkateSpotsBackground');
export function useBackgroundDelivery() {
const { user } = useCurrentUser();
const supported = isNativePlatform() && getPlatform() === 'android';
const [mode, setMode] = useState<DeliveryMode>('off');
const [prefs] = useNotificationPrefs();
const categories = useMemo(
() => NOTIFICATION_CATEGORIES.map((c) => c.key).filter((k) => k !== 'reminders' && prefs[k]),
[prefs],
);
const enable = useCallback(
async (nextMode: Exclude<DeliveryMode, 'off'>, intervalMinutes = 5) => {
if (!supported) throw new Error('Background delivery is Android-only.');
if (!user?.pubkey || !user.signer) throw new Error('Log in first.');
const challenge = await requestPushChallenge(user.pubkey);
const proof = await user.signer.signEvent({
kind: 27235,
content: '',
created_at: Math.floor(Date.now() / 1000),
tags: [['challenge', challenge]],
});
const res = await fetch(`${pushServiceBase()}/native/poll/register`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ pubkey: user.pubkey, proof, categories }),
});
if (!res.ok) throw new Error('Could not register for background delivery.');
const { token, seq } = (await res.json()) as { token: string; seq: number };
await SkateSpotsBackground.seed({ baseUrl: pushServiceBase(), token, since: seq ?? 0 });
await SkateSpotsBackground.configure({ mode: nextMode, intervalMinutes });
setMode(nextMode);
},
[supported, user, categories],
);
/** Re-register with the current category choices (after a toggle change). */
const resubscribe = useCallback(async () => {
if (mode === 'off') return;
await enable(mode, mode === 'fast' ? 5 : undefined);
}, [mode, enable]);
const disable = useCallback(async () => {
if (!supported) return;
await SkateSpotsBackground.configure({ mode: 'off' });
await SkateSpotsBackground.clearNotifications();
setMode('off');
}, [supported]);
return { supported, mode, enable, disable, resubscribe, categories };
}