diff --git a/keyserver/src/push/providers.js b/keyserver/src/push/providers.js --- a/keyserver/src/push/providers.js +++ b/keyserver/src/push/providers.js @@ -7,11 +7,20 @@ import invariant from 'invariant'; import webpush from 'web-push'; +import type { PlatformDetails } from 'lib/types/device-types'; + import { importJSON } from '../utils/import-json.js'; type APNPushProfile = 'apn_config' | 'comm_apn_config'; -function getAPNPushProfileForCodeVersion(codeVersion: ?number): APNPushProfile { - return codeVersion && codeVersion >= 87 ? 'comm_apn_config' : 'apn_config'; +function getAPNPushProfileForCodeVersion( + platformDetails: PlatformDetails, +): APNPushProfile { + if (platformDetails.platform === 'macos') { + return 'comm_apn_config'; + } + return platformDetails.codeVersion && platformDetails.codeVersion >= 87 + ? 'comm_apn_config' + : 'apn_config'; } type FCMPushProfile = 'fcm_config' | 'comm_fcm_config'; @@ -77,8 +86,13 @@ } } -function getAPNsNotificationTopic(codeVersion: ?number): string { - return codeVersion && codeVersion >= 87 ? 'app.comm' : 'org.squadcal.app'; +function getAPNsNotificationTopic(platformDetails: PlatformDetails): string { + if (platformDetails.platform === 'macos') { + return 'app.comm.macos'; + } + return platformDetails.codeVersion && platformDetails.codeVersion >= 87 + ? 'app.comm' + : 'org.squadcal.app'; } type WebPushConfig = { +publicKey: string, +privateKey: string }; diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js --- a/keyserver/src/push/rescind.js +++ b/keyserver/src/push/rescind.js @@ -61,7 +61,7 @@ deliveryPromises[id] = apnPush({ notification, deviceTokens: delivery.iosDeviceTokens, - codeVersion: null, + platformDetails: { platform: 'ios' }, }); } else if (delivery.androidID) { // Old Android @@ -87,7 +87,7 @@ deliveryPromises[id] = apnPush({ notification, deviceTokens, - codeVersion, + platformDetails: { platform: 'ios', codeVersion }, }); } else if (delivery.deviceType === 'android') { // New Android @@ -162,7 +162,10 @@ ): apn.Notification { const notification = new apn.Notification(); notification.contentAvailable = true; - notification.topic = getAPNsNotificationTopic(codeVersion); + notification.topic = getAPNsNotificationTopic({ + platform: 'ios', + codeVersion: codeVersion ?? undefined, + }); notification.priority = 5; notification.pushType = 'background'; notification.payload = diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -22,7 +22,7 @@ rawThreadInfoFromServerThreadInfo, threadInfoFromRawThreadInfo, } from 'lib/shared/thread-utils.js'; -import type { Platform } from 'lib/types/device-types.js'; +import type { Platform, PlatformDetails } from 'lib/types/device-types.js'; import { type RawMessageInfo, type MessageInfo, @@ -173,28 +173,34 @@ if (iosVersionsToTokens) { for (const [codeVer, deviceTokens] of iosVersionsToTokens) { const codeVersion = parseInt(codeVer, 10); // only for Flow + const platformDetails = { platform: 'ios', codeVersion }; const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( newRawMessageInfos, - { platform: 'ios', codeVersion }, + platformDetails, ); const deliveryPromise = (async () => { - const notification = await prepareIOSNotification({ + const notification = await prepareAPNsNotification({ allMessageInfos, newRawMessageInfos: shimmedNewRawMessageInfos, threadInfo, collapseKey: notifInfo.collapseKey, badgeOnly, unreadCount: unreadCounts[userID], - codeVersion, + platformDetails, notifTargetUserInfo: { id: userID, username, }, }); - return await sendIOSNotification(notification, [...deviceTokens], { - ...notificationInfo, - codeVersion, - }); + return await sendAPNsNotification( + 'ios', + notification, + [...deviceTokens], + { + ...notificationInfo, + codeVersion, + }, + ); })(); deliveryPromises.push(deliveryPromise); } @@ -203,9 +209,10 @@ if (androidVersionsToTokens) { for (const [codeVer, deviceTokens] of androidVersionsToTokens) { const codeVersion = parseInt(codeVer, 10); // only for Flow + const platformDetails = { platform: 'android', codeVersion }; const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( newRawMessageInfos, - { platform: 'android', codeVersion }, + platformDetails, ); const deliveryPromise = (async () => { const notification = await prepareAndroidNotification({ @@ -215,7 +222,7 @@ collapseKey: notifInfo.collapseKey, badgeOnly, unreadCount: unreadCounts[userID], - codeVersion, + platformDetails, notifTargetUserInfo: { id: userID, username, @@ -540,18 +547,18 @@ return byPlatform; } -type IOSNotifInputData = { +type APNsNotifInputData = { +allMessageInfos: MessageInfo[], +newRawMessageInfos: RawMessageInfo[], +threadInfo: ThreadInfo, +collapseKey: ?string, +badgeOnly: boolean, +unreadCount: number, - +codeVersion: number, + +platformDetails: PlatformDetails, +notifTargetUserInfo: UserInfo, }; -async function prepareIOSNotification( - inputData: IOSNotifInputData, +async function prepareAPNsNotification( + inputData: APNsNotifInputData, ): Promise { const { allMessageInfos, @@ -560,14 +567,13 @@ collapseKey, badgeOnly, unreadCount, - codeVersion, + platformDetails, notifTargetUserInfo, } = inputData; const uniqueID = uuidv4(); const notification = new apn.Notification(); - notification.topic = getAPNsNotificationTopic(codeVersion); - + notification.topic = getAPNsNotificationTopic(platformDetails); const { merged, ...rest } = await notifTextsForMessageInfo( allMessageInfos, threadInfo, @@ -589,7 +595,7 @@ notification.pushType = 'alert'; notification.payload.id = uniqueID; notification.payload.threadID = threadInfo.id; - if (codeVersion > 1000) { + if (platformDetails.codeVersion && platformDetails.codeVersion > 1000) { notification.mutableContent = true; } if (collapseKey) { @@ -612,14 +618,15 @@ const notificationCopy = _cloneDeep(notification); if (notificationCopy.length() > apnMaxNotificationPayloadByteSize) { console.warn( - `iOS notification ${uniqueID} exceeds size limit, even with messageInfos omitted`, + `${platformDetails.platform} notification ${uniqueID} ` + + `exceeds size limit, even with messageInfos omitted`, ); } return notification; } type AndroidNotifInputData = { - ...IOSNotifInputData, + ...APNsNotifInputData, +dbID: string, }; async function prepareAndroidNotification( @@ -632,7 +639,7 @@ collapseKey, badgeOnly, unreadCount, - codeVersion, + platformDetails: { codeVersion }, notifTargetUserInfo, dbID, } = inputData; @@ -658,7 +665,7 @@ // the notif has a full payload, and then crash when trying to parse it. // By skipping `id` we allow old clients to still handle in-app notifs and // badge updating. - if (!badgeOnly || codeVersion >= 69) { + if (!badgeOnly || (codeVersion && codeVersion >= 69)) { notification.data = { ...notification.data, id: notifID, @@ -734,29 +741,34 @@ +codeVersion: number, }; -type IOSDelivery = { +type APNsDelivery = { source: $PropertyType, - deviceType: 'ios', + deviceType: 'ios' | 'macos', iosID: string, deviceTokens: $ReadOnlyArray, codeVersion: number, errors?: $ReadOnlyArray, }; -type IOSResult = { +type APNsResult = { info: NotificationInfo, - delivery: IOSDelivery, + delivery: APNsDelivery, invalidTokens?: $ReadOnlyArray, }; -async function sendIOSNotification( +async function sendAPNsNotification( + platform: 'ios' | 'macos', notification: apn.Notification, deviceTokens: $ReadOnlyArray, notificationInfo: NotificationInfo, -): Promise { +): Promise { const { source, codeVersion } = notificationInfo; - const response = await apnPush({ notification, deviceTokens, codeVersion }); - const delivery: IOSDelivery = { + const response = await apnPush({ + notification, + deviceTokens, + platformDetails: { platform, codeVersion }, + }); + const delivery: APNsDelivery = { source, - deviceType: 'ios', + deviceType: platform, iosID: notification.id, deviceTokens, codeVersion, @@ -764,7 +776,7 @@ if (response.errors) { delivery.errors = response.errors; } - const result: IOSResult = { + const result: APNsResult = { info: notificationInfo, delivery, }; @@ -774,8 +786,8 @@ return result; } -type PushResult = AndroidResult | IOSResult | WebResult; -type PushDelivery = AndroidDelivery | IOSDelivery | WebDelivery; +type PushResult = AndroidResult | APNsResult | WebResult; +type PushDelivery = AndroidDelivery | APNsDelivery | WebDelivery; type AndroidDelivery = { source: $PropertyType, deviceType: 'android', @@ -964,11 +976,14 @@ for (const [codeVer, deviceTokens] of iosVersionsToTokens) { const codeVersion = parseInt(codeVer, 10); // only for Flow const notification = new apn.Notification(); - notification.topic = getAPNsNotificationTopic(codeVersion); + notification.topic = getAPNsNotificationTopic({ + platform: 'ios', + codeVersion, + }); notification.badge = unreadCount; notification.pushType = 'alert'; deliveryPromises.push( - sendIOSNotification(notification, [...deviceTokens], { + sendAPNsNotification('ios', notification, [...deviceTokens], { source, dbID, userID, diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js --- a/keyserver/src/push/utils.js +++ b/keyserver/src/push/utils.js @@ -6,6 +6,7 @@ import invariant from 'invariant'; import webpush from 'web-push'; +import type { PlatformDetails } from 'lib/types/device-types.js'; import type { WebNotification } from 'lib/types/notif-types.js'; import { threadSubscriptions } from 'lib/types/subscription-types.js'; import { threadPermissions } from 'lib/types/thread-types.js'; @@ -39,13 +40,13 @@ async function apnPush({ notification, deviceTokens, - codeVersion, + platformDetails, }: { +notification: apn.Notification, +deviceTokens: $ReadOnlyArray, - +codeVersion: ?number, + +platformDetails: PlatformDetails, }): Promise { - const pushProfile = getAPNPushProfileForCodeVersion(codeVersion); + const pushProfile = getAPNPushProfileForCodeVersion(platformDetails); const apnProvider = await getAPNProvider(pushProfile); if (!apnProvider && process.env.NODE_ENV === 'development') { console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`);