diff --git a/src/lib/push-protocol.ts b/src/lib/push-protocol.ts index 710e6c0..3ceb2f0 100644 --- a/src/lib/push-protocol.ts +++ b/src/lib/push-protocol.ts @@ -16,6 +16,12 @@ import type { ClientMessage } from "@codeoid/protocol"; * only when a push transport is configured. Matches `CAPABILITIES.PUSH`. */ export const PUSH_CAPABILITY = "push"; +/** Advertised by the daemon when it delivers via NATIVE APNs/FCM tokens + * (transport `native` or `relay`) rather than Expo — the client then registers + * its `getDevicePushTokenAsync` token instead of the Expo token. Matches + * `CAPABILITIES.PUSH_NATIVE`. */ +export const PUSH_NATIVE_CAPABILITY = "push.native"; + export type PushPlatform = "ios" | "android"; export interface PushRegisterMsg { diff --git a/src/lib/push.ts b/src/lib/push.ts index 362688e..1bb1e2b 100644 --- a/src/lib/push.ts +++ b/src/lib/push.ts @@ -19,7 +19,12 @@ import * as Notifications from "expo-notifications"; import { Platform } from "react-native"; import type { Connection } from "./connection"; -import { PUSH_CAPABILITY, sendPushMsg, type PushPlatform } from "./push-protocol"; +import { + PUSH_CAPABILITY, + PUSH_NATIVE_CAPABILITY, + sendPushMsg, + type PushPlatform, +} from "./push-protocol"; /** Token registered this session, so sign-out can unregister exactly it. */ let registeredToken: string | null = null; @@ -52,9 +57,13 @@ export async function registerForPush( ): Promise { const platform = nativePlatform(); if (!platform) return; - // Only register when the daemon will actually deliver — avoids a needless - // permission prompt against a daemon with no push transport configured. - if (!daemonCapabilities?.includes(PUSH_CAPABILITY)) return; + // Which token type does the daemon want? `push.native` (transport native/relay + // → APNs/FCM directly) wants this device's NATIVE token; `push` (transport + // expo) wants an Expo token. Neither advertised → the daemon has no push + // transport configured, so skip (avoids a needless permission prompt). + const wantsNative = daemonCapabilities?.includes(PUSH_NATIVE_CAPABILITY) ?? false; + const wantsExpo = daemonCapabilities?.includes(PUSH_CAPABILITY) ?? false; + if (!wantsNative && !wantsExpo) return; if (!Device.isDevice) return; // simulators/emulators can't get a push token try { @@ -64,10 +73,18 @@ export async function registerForPush( } if (!perm.granted) return; - const projectId = easProjectId(); - const { data: token } = await Notifications.getExpoPushTokenAsync( - projectId ? { projectId } : undefined, - ); + let token: string; + if (wantsNative) { + // Raw APNs (iOS) / FCM (Android) device token — the self-hosted daemon or + // relay sends via APNs/FCM directly. No EAS projectId needed. + // `DevicePushToken.data` is typed `any` (the ImplicitlySupportedDevicePushToken + // union arm), not `string`, so coerce to guarantee a string at runtime. + const device = await Notifications.getDevicePushTokenAsync(); + token = typeof device.data === "string" ? device.data : String(device.data); + } else { + const projectId = easProjectId(); + token = (await Notifications.getExpoPushTokenAsync(projectId ? { projectId } : undefined)).data; + } if (conn.client.status.kind !== "connected") return; sendPushMsg(conn.client, { type: "push.register", id: conn.client.nextId(), token, platform }); registeredToken = token;