New Arrival Alerts

Notifying BookNest Readers of New Arrivals

The practice API gained GET /arrivals?after=<id>, POST /push-tokens for the app's token, and a test hook, POST /arrivals, that adds a book. In production the server would push to every stored token; BookNest also checks whenever it comes to the foreground and turns each new book into a local notification:

src/notify/arrivals.ts: one notification per new book (excerpt)TypeScript
let pending: Promise<void> | null = null;
export const checkArrivals = () => (pending ??= check().finally(() => { pending = null; }));
async function check() {
  const after = prefs.getNumber('lastArrival') ?? 60;
  const { books } = (await (await fetch(`${API}/arrivals?after=${after}`)).json()) as
    { books: Book[] };
  for (const b of books) {
    await Notifications.scheduleNotificationAsync({
      // ...title 'New at BookNest', data: { url: `/book/${b.id}` }, trigger: { channelId }
    });
    prefs.set('lastArrival', b.id);
  }

The shared pending promise fixes a bug the first run showed: the startup check and an AppState "active" event (after the permission dialog) ran together, which would have announced a new book twice. After curl 3,008 -X POST localhost:8150/arrivals, bringing BookNest to the front and tapping the notification:

The notification permission prompt, and a new arrival in the notification shade
The notification permission prompt, and a new arrival in the notification shade
Output of 209
'arrivals after', 60, ':', 1
'in the foreground:', 'New at BookNest'
'tapped:', '/book/61', '| action:', 'expo.modules.notifications.actions.DEFAULT'
'path:', '/book/61'
'arrivals after', 61, ':', 0

The tap opened the new book's screen; lastArrival in MMKV 18,748 had moved to 61.