- /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.
85 lines
3.6 KiB
TypeScript
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 };
|
|
}
|