diff --git a/src/lib/notifications/senders/__tests__/apns.test.ts b/src/lib/notifications/senders/__tests__/apns.test.ts index 72b3072ed..d73764379 100644 --- a/src/lib/notifications/senders/__tests__/apns.test.ts +++ b/src/lib/notifications/senders/__tests__/apns.test.ts @@ -590,6 +590,25 @@ describe("sendViaApns — dispatcher fan-out", () => { expect(note.payload.eventType).toBe("MEDICATION_REMINDER"); }); + it("forwards reminderId on MEASUREMENT_REMINDER and strips unlisted keys", async () => { + // The iOS "Erledigt" action POSTs the reminder completion directly + // from a foreground / handled notification, so the reminder's id must + // ride the APNs payload; channel-specific extras stay stripped. + vi.mocked(prisma.device.findMany).mockResolvedValueOnce([ + { id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" }, + ] as never); + sendMock.mockResolvedValueOnce({ sent: [{ device: "tok-a" }], failed: [] }); + await sendViaApns("u-1", { + title: "t", + message: "m", + eventType: "MEASUREMENT_REMINDER", + metadata: { reminderId: "rem-1", replyMarkup: { inline: true } }, + }); + const note = sendMock.mock.calls[0][0]; + expect(note.payload.reminderId).toBe("rem-1"); + expect(note.payload.replyMarkup).toBeUndefined(); + }); + it("forwards MOOD_REMINDER eventType as aps.category too", async () => { // SR-2 contract: the mood-reminder daily job emits this event-type; // the iOS app registers a `MOOD_REMINDER` category whose only diff --git a/src/lib/notifications/senders/apns.ts b/src/lib/notifications/senders/apns.ts index 1e50797b9..56e01c0b3 100644 --- a/src/lib/notifications/senders/apns.ts +++ b/src/lib/notifications/senders/apns.ts @@ -904,6 +904,10 @@ const IOS_METADATA_ALLOWLIST = new Set([ "url", "runwayDays", "thresholdDays", + // v1.18.7 — MEASUREMENT_REMINDER carries the reminder's id so a + // foreground / handled notification can POST the completion directly + // instead of routing through the capture deep link. + "reminderId", ]); function pickIosMetadata(