From 7008b95e52f14caa2b6ca38ad6fefb0d7ed4ee94 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 29 Jan 2026 00:40:31 +0000 Subject: [PATCH] Add Expo access token support Co-authored-by: reece --- README.md | 9 ++++ src/client/index.ts | 16 ++++++- src/component/_generated/component.ts | 44 +++++++++++++++---- src/component/functions.ts | 63 ++++++++++++++++++++++----- src/component/helpers.ts | 1 + src/component/internal.ts | 19 +++++--- 6 files changed, 126 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6fc82a4..b5db61b 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,15 @@ const pushNotifications = new PushNotifications(components.pushNotifications, { }); ``` +If you have an Expo access token, pass it as `expoAccessToken` so requests to +the Expo push API include the `Authorization: Bearer ` header: + +```ts +const pushNotifications = new PushNotifications(components.pushNotifications, { + expoAccessToken: process.env.EXPO_ACCESS_TOKEN, +}); +``` + The push notification sender can be shutdown gracefully, and then restarted using the `shutdown` and `restart` methods. diff --git a/src/client/index.ts b/src/client/index.ts index 1aac44e..b72abf0 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -21,17 +21,19 @@ import type { ComponentApi } from "../component/_generated/component.js"; export class PushNotifications> { private config: { logLevel: LogLevel; + expoAccessToken?: string; }; constructor( public component: ComponentApi, config?: { logLevel?: LogLevel; + expoAccessToken?: string; }, ) { this.component = component; this.config = { - ...(config ?? {}), logLevel: config?.logLevel ?? "ERROR", + expoAccessToken: config?.expoAccessToken, }; } @@ -47,6 +49,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.recordPushNotificationToken, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -59,6 +62,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.removePushNotificationToken, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -69,6 +73,7 @@ export class PushNotifications> { return ctx.runQuery(this.component.public.getStatusForUser, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -97,6 +102,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.sendPushNotification, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -120,6 +126,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.sendPushNotificationBatch, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -131,6 +138,7 @@ export class PushNotifications> { return ctx.runQuery(this.component.public.getNotification, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -144,6 +152,7 @@ export class PushNotifications> { return ctx.runQuery(this.component.public.getNotificationsForUser, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -154,6 +163,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.deleteNotificationsForUser, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -167,6 +177,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.pauseNotificationsForUser, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -180,6 +191,7 @@ export class PushNotifications> { return ctx.runMutation(this.component.public.unpauseNotificationsForUser, { ...args, logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -195,6 +207,7 @@ export class PushNotifications> { shutdown(ctx: RunMutationCtx) { return ctx.runMutation(this.component.public.shutdown, { logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } @@ -208,6 +221,7 @@ export class PushNotifications> { restart(ctx: RunMutationCtx): Promise { return ctx.runMutation(this.component.public.restart, { logLevel: this.config.logLevel, + expoAccessToken: this.config.expoAccessToken, }); } } diff --git a/src/component/_generated/component.ts b/src/component/_generated/component.ts index 615e85c..483589e 100644 --- a/src/component/_generated/component.ts +++ b/src/component/_generated/component.ts @@ -27,14 +27,22 @@ export type ComponentApi = deleteNotificationsForUser: FunctionReference< "mutation", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string }, + { + expoAccessToken?: string; + logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; + userId: string; + }, null, Name >; getNotification: FunctionReference< "query", "internal", - { id: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" }, + { + expoAccessToken?: string; + id: string; + logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; + }, null | { _contentAvailable?: boolean; _creationTime: number; @@ -71,6 +79,7 @@ export type ComponentApi = "query", "internal", { + expoAccessToken?: string; limit?: number; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string; @@ -111,14 +120,22 @@ export type ComponentApi = getStatusForUser: FunctionReference< "query", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string }, + { + expoAccessToken?: string; + logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; + userId: string; + }, { hasToken: boolean; paused: boolean }, Name >; pauseNotificationsForUser: FunctionReference< "mutation", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string }, + { + expoAccessToken?: string; + logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; + userId: string; + }, null, Name >; @@ -126,6 +143,7 @@ export type ComponentApi = "mutation", "internal", { + expoAccessToken?: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; pushToken: string; userId: string; @@ -136,14 +154,18 @@ export type ComponentApi = removePushNotificationToken: FunctionReference< "mutation", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string }, + { + expoAccessToken?: string; + logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; + userId: string; + }, null, Name >; restart: FunctionReference< "mutation", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" }, + { expoAccessToken?: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" }, boolean, Name >; @@ -152,6 +174,7 @@ export type ComponentApi = "internal", { allowUnregisteredTokens?: boolean; + expoAccessToken?: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; notification: { _contentAvailable?: boolean; @@ -183,6 +206,7 @@ export type ComponentApi = "internal", { allowUnregisteredTokens?: boolean; + expoAccessToken?: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; notifications: Array<{ notification: { @@ -214,14 +238,18 @@ export type ComponentApi = shutdown: FunctionReference< "mutation", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" }, + { expoAccessToken?: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" }, { data?: any; message: string }, Name >; unpauseNotificationsForUser: FunctionReference< "mutation", "internal", - { logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string }, + { + expoAccessToken?: string; + logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; + userId: string; + }, null, Name >; diff --git a/src/component/functions.ts b/src/component/functions.ts index 487cdd1..356daf3 100644 --- a/src/component/functions.ts +++ b/src/component/functions.ts @@ -4,48 +4,87 @@ import { customMutation, customQuery, } from "convex-helpers/server/customFunctions"; +import { v } from "convex/values"; import * as VanillaConvex from "./_generated/server.js"; import { Logger, logLevelValidator } from "../logging/index.js"; +const expoAccessTokenValidator = v.optional(v.string()); + export const query = customQuery(VanillaConvex.query, { - args: { logLevel: logLevelValidator }, + args: { logLevel: logLevelValidator, expoAccessToken: expoAccessTokenValidator }, input: async (ctx, args) => { - return { ctx: { logger: new Logger(args.logLevel) }, args: {} }; + return { + ctx: { + logger: new Logger(args.logLevel), + expoAccessToken: args.expoAccessToken, + }, + args: {}, + }; }, }); export const mutation = customMutation(VanillaConvex.mutation, { - args: { logLevel: logLevelValidator }, + args: { logLevel: logLevelValidator, expoAccessToken: expoAccessTokenValidator }, input: async (ctx, args) => { - return { ctx: { logger: new Logger(args.logLevel) }, args: {} }; + return { + ctx: { + logger: new Logger(args.logLevel), + expoAccessToken: args.expoAccessToken, + }, + args: {}, + }; }, }); export const action = customAction(VanillaConvex.action, { - args: { logLevel: logLevelValidator }, + args: { logLevel: logLevelValidator, expoAccessToken: expoAccessTokenValidator }, input: async (ctx, args) => { - return { ctx: { logger: new Logger(args.logLevel) }, args: {} }; + return { + ctx: { + logger: new Logger(args.logLevel), + expoAccessToken: args.expoAccessToken, + }, + args: {}, + }; }, }); export const internalQuery = customQuery(VanillaConvex.internalQuery, { - args: { logLevel: logLevelValidator }, + args: { logLevel: logLevelValidator, expoAccessToken: expoAccessTokenValidator }, input: async (ctx, args) => { - return { ctx: { logger: new Logger(args.logLevel) }, args: {} }; + return { + ctx: { + logger: new Logger(args.logLevel), + expoAccessToken: args.expoAccessToken, + }, + args: {}, + }; }, }); export const internalMutation = customMutation(VanillaConvex.internalMutation, { - args: { logLevel: logLevelValidator }, + args: { logLevel: logLevelValidator, expoAccessToken: expoAccessTokenValidator }, input: async (ctx, args) => { - return { ctx: { logger: new Logger(args.logLevel) }, args: {} }; + return { + ctx: { + logger: new Logger(args.logLevel), + expoAccessToken: args.expoAccessToken, + }, + args: {}, + }; }, }); export const internalAction = customAction(VanillaConvex.internalAction, { - args: { logLevel: logLevelValidator }, + args: { logLevel: logLevelValidator, expoAccessToken: expoAccessTokenValidator }, input: async (ctx, args) => { - return { ctx: { logger: new Logger(args.logLevel) }, args: {} }; + return { + ctx: { + logger: new Logger(args.logLevel), + expoAccessToken: args.expoAccessToken, + }, + args: {}, + }; }, }); diff --git a/src/component/helpers.ts b/src/component/helpers.ts index 64833bf..93c1d61 100644 --- a/src/component/helpers.ts +++ b/src/component/helpers.ts @@ -41,6 +41,7 @@ export async function ensureCoordinator(ctx: MutationCtx) { internal.internal.coordinateSendingPushNotifications, { logLevel: ctx.logger.level, + expoAccessToken: ctx.expoAccessToken, }, ); const coordinatorId = await ctx.db.insert("senderCoordinator", { diff --git a/src/component/internal.ts b/src/component/internal.ts index 7ad86e0..04abb58 100644 --- a/src/component/internal.ts +++ b/src/component/internal.ts @@ -134,6 +134,7 @@ export const coordinateSendingPushNotifications = internalMutation({ { notificationIds: notificationsToSend.map((n) => n._id), logLevel: ctx.logger.level, + expoAccessToken: ctx.expoAccessToken, }, ); @@ -165,6 +166,7 @@ export const coordinateSendingPushNotifications = internalMutation({ }; }), logLevel: ctx.logger.level, + expoAccessToken: ctx.expoAccessToken, }, ); await ctx.db.insert("senders", { @@ -220,13 +222,17 @@ export const action_sendPushNotifications = internalAction({ let response: Response; try { // https://docs.expo.dev/push-notifications/sending-notifications/#http2-api + const headers: Record = { + Accept: "application/json", + "Accept-encoding": "gzip, deflate", + "Content-Type": "application/json", + }; + if (ctx.expoAccessToken) { + headers.Authorization = `Bearer ${ctx.expoAccessToken}`; + } response = await fetch("https://exp.host/--/api/v2/push/send", { method: "POST", - headers: { - Accept: "application/json", - "Accept-encoding": "gzip, deflate", - "Content-Type": "application/json", - }, + headers, body: JSON.stringify(args.notifications.map((n) => n.message)), }); } catch (_e) { @@ -248,6 +254,7 @@ export const action_sendPushNotifications = internalAction({ }), checkJobId: args.checkJobId, logLevel: ctx.logger.level, + expoAccessToken: ctx.expoAccessToken, }); return; } @@ -270,6 +277,7 @@ export const action_sendPushNotifications = internalAction({ }), checkJobId: args.checkJobId, logLevel: ctx.logger.level, + expoAccessToken: ctx.expoAccessToken, }); return; } @@ -309,6 +317,7 @@ export const action_sendPushNotifications = internalAction({ notifications: notificationStates, checkJobId: args.checkJobId, logLevel: ctx.logger.level, + expoAccessToken: ctx.expoAccessToken, }); }, });