From 57586e6b52014b0d47b49f6448dea21c3e0195aa Mon Sep 17 00:00:00 2001 From: Andrii Panasiuk Date: Tue, 20 Oct 2026 03:07:49 +0300 Subject: [PATCH] fix(server): send push notifications at high priority so Android delivers them on time --- .../src/server/push/push-service.test.ts | 42 +++++++++++++++++++ .../server/src/server/push/push-service.ts | 5 +++ 2 files changed, 47 insertions(+) create mode 100644 packages/server/src/server/push/push-service.test.ts diff --git a/packages/server/src/server/push/push-service.test.ts b/packages/server/src/server/push/push-service.test.ts new file mode 100644 index 0000000000..3616f5dc19 --- /dev/null +++ b/packages/server/src/server/push/push-service.test.ts @@ -0,0 +1,42 @@ +import type pino from "pino"; +import { afterEach, describe, expect, test, vi } from "vitest"; + +import { PushService } from "./push-service.js"; + +function createLogger(): pino.Logger { + const logger = { + child: () => logger, + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined, + }; + return logger as unknown as pino.Logger; +} + +describe("PushService", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + test("sends high priority so Android delivers while the device dozes", async () => { + const requests: unknown[] = []; + vi.stubGlobal( + "fetch", + vi.fn(async (_url: string, init: RequestInit) => { + requests.push(JSON.parse(String(init.body))); + return new Response(JSON.stringify({ data: [{ status: "ok", id: "ticket" }] })); + }), + ); + const service = new PushService(createLogger(), () => undefined); + + await service.sendPush(["ExponentPushToken[android-device]"], { + title: "Agent finished", + body: "Done", + }); + + expect(requests).toEqual([ + [expect.objectContaining({ to: "ExponentPushToken[android-device]", priority: "high" })], + ]); + }); +}); diff --git a/packages/server/src/server/push/push-service.ts b/packages/server/src/server/push/push-service.ts index b91de5fbd0..628b3a7f77 100644 --- a/packages/server/src/server/push/push-service.ts +++ b/packages/server/src/server/push/push-service.ts @@ -12,6 +12,7 @@ interface ExpoPushMessage { body: string; data?: Record; sound?: "default"; + priority?: "default" | "normal" | "high"; } interface ExpoPushTicket { @@ -42,12 +43,16 @@ export class PushService { return; } + // Expo defaults to "normal" priority on Android, which FCM holds back while + // the device dozes, so notifications arrive minutes late. iOS already + // defaults to "high". const messages: ExpoPushMessage[] = tokens.map((token) => ({ to: token, title: payload.title, body: payload.body, data: payload.data, sound: "default", + priority: "high", })); // Batch tokens (max 100 per request per Expo limits)