From c1ee8106427835290697b6ef77c3c26d662d12b1 Mon Sep 17 00:00:00 2001 From: Lukas Haertel Date: Fri, 6 Sep 2024 19:50:54 +0200 Subject: [PATCH] feature: iOS push compat --- eas.json | 3 - .../types/NotificationTrigger.tsx | 49 ------------- .../useNotificationReceivedManager.tsx | 70 +++++++++---------- .../useNotificationRespondedManager.tsx | 12 ++-- src/init/BackgroundSyncGenerator.tsx | 20 +++--- src/init/NotificationHandler.tsx | 6 +- src/routes/pm/PmItem.tsx | 15 ++-- 7 files changed, 58 insertions(+), 117 deletions(-) delete mode 100644 src/hooks/notifications/types/NotificationTrigger.tsx diff --git a/eas.json b/eas.json index 3f2977cc..f0c8bb21 100644 --- a/eas.json +++ b/eas.json @@ -6,9 +6,6 @@ "build": { "development": { "developmentClient": true, - "android": { - "gradleCommand": ":app:assembleRelease --parallel --no-daemon" - }, "distribution": "internal" }, "preview": { diff --git a/src/hooks/notifications/types/NotificationTrigger.tsx b/src/hooks/notifications/types/NotificationTrigger.tsx deleted file mode 100644 index e29d8d50..00000000 --- a/src/hooks/notifications/types/NotificationTrigger.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FirebaseRemoteMessage, NotificationTrigger, PushNotificationTrigger } from "expo-notifications"; - -/** - * Notification type message trigger coming in as an FCM remote message. - */ -export type FirebaseMessageTrigger = PushNotificationTrigger & { remoteMessage: FirebaseRemoteMessage }; -/** - * FCM remote message with a notification payload. - */ -export type FirebaseNotificationTrigger = FirebaseMessageTrigger & { remoteMessage: { notification: Exclude } }; - -/** - * FCM remote message with a data payload. - */ -export type FirebaseDataTrigger = FirebaseMessageTrigger & { remoteMessage: { data: Exclude } }; - -/** - * Asserts object is a trigger. - * @param object The object to assert. - */ -export const isTrigger = (object: any): object is NotificationTrigger => typeof object?.type === "string"; - -/** - * Asserts remote message is notification, checks that type is push and - * that trigger.remoteMessage.notification is a non-null object. - * @param trigger The trigger to assert. - */ -export const isTriggerWithNotification = (trigger: NotificationTrigger): trigger is FirebaseNotificationTrigger => - // Is push trigger. - trigger.type === "push" && - // Is for android. - "remoteMessage" in trigger && - // Has non-null notification. - typeof trigger.remoteMessage?.notification === "object" && - trigger.remoteMessage.notification !== null; - -/** - * Asserts remote message is data, checks that type is push and - * that trigger.remoteMessage.data is a non-null object. - * @param trigger The trigger to assert. - */ -export const isTriggerWithData = (trigger: NotificationTrigger): trigger is FirebaseDataTrigger => - // Is push trigger. - trigger.type === "push" && - // Is for android. - "remoteMessage" in trigger && - // Has non-null data. - typeof trigger.remoteMessage?.data === "object" && - trigger.remoteMessage.data !== null; diff --git a/src/hooks/notifications/useNotificationReceivedManager.tsx b/src/hooks/notifications/useNotificationReceivedManager.tsx index b1567925..f893170f 100644 --- a/src/hooks/notifications/useNotificationReceivedManager.tsx +++ b/src/hooks/notifications/useNotificationReceivedManager.tsx @@ -3,26 +3,11 @@ import { addNotificationReceivedListener, Notification, removeNotificationSubscr import moment from "moment"; import { useEffect } from "react"; +import { Platform } from "react-native"; import { useSynchronizer } from "../../components/sync/SynchronizationProvider"; -import { conId } from "../../configuration"; -import { NotificationChannels } from "../../init/NotificationChannel"; import { captureNotificationException } from "../../sentryHelpers"; import { useAppDispatch } from "../../store"; import { logFCMMessage } from "../../store/background/slice"; -import { FirebaseNotificationTrigger, isTrigger, isTriggerWithData, isTriggerWithNotification } from "./types/NotificationTrigger"; - -const scheduleNotificationFromTrigger = (source: FirebaseNotificationTrigger, channelId: NotificationChannels = "default") => - scheduleNotificationAsync({ - identifier: source.remoteMessage.messageId ?? undefined, - content: { - title: source.remoteMessage.notification.title ?? undefined, - body: source.remoteMessage.notification.body ?? undefined, - data: source.remoteMessage.data, - }, - trigger: { - channelId, - }, - }); /** * Manages the foreground part of notification handling, as well as handling sync requests @@ -39,11 +24,7 @@ export const useNotificationReceivedManager = () => { // Setup notification received handler. useEffect(() => { const receive = addNotificationReceivedListener(({ request: { content, trigger, identifier } }: Notification) => { - // Prevent reentrant error when scheduling a notification locally from a remote message. - if (!isTrigger(trigger)) { - console.log("Skipping empty message from remote"); - return; - } + if (trigger?.type !== "push") return; // Track immediately when the message came in. const dateReceived = moment().toISOString(); @@ -51,39 +32,54 @@ export const useNotificationReceivedManager = () => { // Always log receiving of the message. console.log(`Received at ${dateReceived}:`, trigger); - // Always dispatch a state update tracking the message. - dispatch(logFCMMessage({ dateReceived, content, trigger, identifier })); + const data = (Platform.OS === "ios" ? trigger.payload : trigger.remoteMessage?.data) ?? {}; + const event = data.Event; + // const cid = data.CID; + const title: string = Platform.OS === "ios" ? (trigger.payload as any).aps?.alert?.title : trigger.remoteMessage?.notification?.title; + const body: string = Platform.OS === "ios" ? (trigger.payload as any).aps?.alert?.body : trigger.remoteMessage?.notification?.body; - // Check if data trigger. Otherwise, not actionable. - if (isTriggerWithData(trigger)) { - // Get CID and event type. - const cid = trigger.remoteMessage.data.CID; - const event = trigger.remoteMessage.data.Event; + console.log("Parsed as push notification:", { event, title, body, data }); - // Skip if not for this convention. - if (cid !== conId) return; + // // Check ID match. + // if (cid !== conId) return; - // Handle for sync, announcement, and notification. - if (event === "Sync") { + switch (event) { + case "Sync": { // Is sync, do synchronization silently. synchronize().catch(captureException); // Log sync. console.log("Synchronized for remote Sync request"); - } else if (event === "Announcement" && isTriggerWithNotification(trigger)) { + break; + } + + case "Announcement": { // Schedule it. - scheduleNotificationFromTrigger(trigger, "announcements").then( + scheduleNotificationAsync({ + content: { title, body, data }, + trigger: null, // { channelId: "announcements" }, + }).then( () => console.log("Announcement scheduled"), (e) => captureNotificationException("Unable to schedule announcement", e), ); - } else if (event === "Notification" && isTriggerWithNotification(trigger)) { + break; + } + + case "Notification": { // Schedule it. - scheduleNotificationFromTrigger(trigger, "private_messages").then( + scheduleNotificationAsync({ + content: { title, body, data }, + trigger: null, // { channelId: "private_messages" }, + }).then( () => console.log("Personal message scheduled"), - (e) => captureNotificationException("Unable to schedule personal message", e), + (e) => captureNotificationException("Unable to schedule announcement", e), ); + break; } } + + // Always dispatch a state update tracking the message. + dispatch(logFCMMessage({ dateReceived, content, trigger, identifier })); }); // Return removal of subscription. diff --git a/src/hooks/notifications/useNotificationRespondedManager.tsx b/src/hooks/notifications/useNotificationRespondedManager.tsx index 11ee89e2..0868eca4 100644 --- a/src/hooks/notifications/useNotificationRespondedManager.tsx +++ b/src/hooks/notifications/useNotificationRespondedManager.tsx @@ -1,12 +1,10 @@ import { useLastNotificationResponse } from "expo-notifications"; import moment from "moment"; import { useEffect } from "react"; - -import { conId } from "../../configuration"; import { useAppNavigation } from "../nav/useAppNavigation"; /** - * Manages the foreground part notification response handling. + * This handles interacting with scheduled notifications from the received manager. * @constructor */ export const useNotificationRespondedManager = () => { @@ -15,6 +13,8 @@ export const useNotificationRespondedManager = () => { // Setup handler for notification response. useEffect(() => { + if (!response) return; + // Track when the response was observed. const dateResponded = moment().toISOString(); @@ -23,12 +23,12 @@ export const useNotificationRespondedManager = () => { // Get the data object. Resolve CID, type, and related ID. const data = response?.notification?.request?.content?.data; - const cid = data?.CID; + // const cid = data?.CID; const event = data?.Event; const relatedId = data?.RelatedId; - // Check ID match. - if (cid !== conId) return; + // // Check ID match. + // if (cid !== conId) return; // Event is for an announcement. if (event === "Announcement") { diff --git a/src/init/BackgroundSyncGenerator.tsx b/src/init/BackgroundSyncGenerator.tsx index f19f3266..c64a5341 100644 --- a/src/init/BackgroundSyncGenerator.tsx +++ b/src/init/BackgroundSyncGenerator.tsx @@ -3,7 +3,6 @@ import { registerTaskAsync } from "expo-notifications"; import { defineTask, TaskManagerTaskBody } from "expo-task-manager"; import { Platform } from "react-native"; -import { conId } from "../configuration"; import { requestSyncFromBackground } from "../hooks/sync/useBackgroundSyncManager"; import { captureNotificationException } from "../sentryHelpers"; @@ -15,22 +14,25 @@ import { captureNotificationException } from "../sentryHelpers"; const BG_NOTIFICATIONS_NAME = "background_notifications"; // Define task for notification handling. -defineTask(BG_NOTIFICATIONS_NAME, ({ data, error, executionInfo }: TaskManagerTaskBody<{ notification?: any }>) => { +defineTask(BG_NOTIFICATIONS_NAME, (body: TaskManagerTaskBody) => { // Skip method if error was given to be handled. - if (error) { - captureEvent(error); + if (body.error) { + captureEvent(body.error); return; } + // Parse from proper source. + const data: Record = (Platform.OS === "ios" ? body.data : body.data?.notification?.data) ?? {}; + // Log that a notification was received. - console.log("Received data in background", data, "App state", executionInfo.appState); + console.log("Received data in background", data, "App state", body.executionInfo.appState); // Get event data. - const cid = data?.notification?.data?.CID; - const event = data?.notification?.data?.Event; + // const cid = data.CID; + const event = data.Event; - // Skip if not for this convention. - if (cid !== conId) return; + // // Skip if not for this convention. + // if (cid !== conId) return; // Handle for Sync events only. if (event === "Sync") { diff --git a/src/init/NotificationHandler.tsx b/src/init/NotificationHandler.tsx index e1a5f420..bba335a6 100644 --- a/src/init/NotificationHandler.tsx +++ b/src/init/NotificationHandler.tsx @@ -6,16 +6,16 @@ import { captureNotificationException } from "../sentryHelpers"; // Set general notification handling strategy. setNotificationHandler({ - handleNotification: ({ request: { content } }) => { + handleNotification: async ({ request: { content } }) => { // Mark handling notification. console.log("Handling notification", content); // Show if it's a notification trigger. - return Promise.resolve({ + return { shouldShowAlert: typeof content?.title === "string" || typeof content?.body === "string", shouldPlaySound: false, shouldSetBadge: false, - }); + }; }, handleSuccess: (id) => { // Log success. diff --git a/src/routes/pm/PmItem.tsx b/src/routes/pm/PmItem.tsx index dba37bf2..8999327a 100644 --- a/src/routes/pm/PmItem.tsx +++ b/src/routes/pm/PmItem.tsx @@ -48,21 +48,16 @@ export const PmItem = () => { }, [message, markRead]); // If no message currently displayable, check if fetching. If not fetching - if (!message) { - if (ready) { - return null; - } else { + useEffect(() => { + if (ready && !message) { navigation.pop(); - return null; } - } + }, [message, ready, navigation]); return ( -
{message.Subject}
- - {message.Message} - +
{!message ? "Viewing message" : message.Subject}
+ {!message ? null : {message.Message}}
); };