diff --git a/ios/ZulipMobile/ZLPNotifications.swift b/ios/ZulipMobile/ZLPNotifications.swift index 75698c379da..3824ea0a3a9 100644 --- a/ios/ZulipMobile/ZLPNotifications.swift +++ b/ios/ZulipMobile/ZLPNotifications.swift @@ -45,6 +45,30 @@ class ZLPNotificationsEvents: RCTEventEmitter { @objc(ZLPNotificationsStatus) class ZLPNotificationsStatus: NSObject { + // For why we include this, see + // https://reactnative.dev/docs/0.68/native-modules-ios#exporting-constants + @objc + static func requiresMainQueueSetup() -> Bool { + // From the RN doc linked above: + // > If your module does not require access to UIKit, then you should + // > respond to `+ requiresMainQueueSetup` with NO. + // + // The launchOptions dictionary (used in `constantsToExport`) is + // accessed via the UIApplicationDelegate protocol, which is part of + // UIKit: + // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application?language=objc + // + // So I think to follow RN's advice about accessing UIKit, it's probably + // right to return `true`. + return true + } + + // The bridge object, provided by RN's RCTBridgeModule (via + // RCT_EXTERN_MODULE in ZLPNotificationsBridge.m): + // https://github.com/facebook/react-native/blob/v0.68.7/React/Base/RCTBridgeModule.h#L152-L159 + @objc + var bridge: RCTBridge! + /// Whether the app can receive remote notifications. // Ideally we could subscribe to changes in this value, but there // doesn't seem to be an API for that. The caller can poll, e.g., by @@ -60,4 +84,29 @@ class ZLPNotificationsStatus: NSObject { resolve(settings.authorizationStatus == UNAuthorizationStatus.authorized) }) } + + @objc + func constantsToExport() -> [String: Any]! { + var result: [String: Any] = [:] + + // `launchOptions` comes via our AppDelegate's + // `application:didFinishLaunchingWithOptions:` method override. From + // the doc for that method (on UIApplicationDelegate): + // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application?language=objc + // > A dictionary indicating the reason the app was launched (if any). + // > The contents of this dictionary may be empty in situations where + // > the user launched the app directly. […] + // + // In particular, for our purpose here: if the app was launched from a + // notification, then it wasn't "launched […] directly". + // + // Empirically, launchOptions *itself* may be missing, which is + // different from being empty; the distinction matters in Swift-land + // where it's an error to access properties of nil. This doesn't seem + // quite covered by "[t]he contents of this dictionary may be empty", + // but anyway it explains our optional chaining on .launchOptions. + result["initialNotification"] = bridge.launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] ?? kCFNull + + return result + } } diff --git a/src/notification/extract.js b/src/notification/extract.js index 2a4422571e5..93e8b3af7a6 100644 --- a/src/notification/extract.js +++ b/src/notification/extract.js @@ -1,6 +1,4 @@ /* @flow strict-local */ -import PushNotificationIOS from '@react-native-community/push-notification-ios'; - import type { Notification } from './types'; import { makeUserId } from '../api/idTypes'; import type { JSONableDict, JSONableInput, JSONableInputDict } from '../utils/jsonable'; @@ -38,12 +36,6 @@ export const fromAPNsImpl = (data: ?JSONableDict): Notification | void => { // // For the format this parses, see `ApnsPayload` in src/api/notificationTypes.js . // - // Though in one case what it actually receives is more like this: - // $Rest - // That case is the "initial notification", a notification that launched - // the app by being tapped, because the `PushNotificationIOS` library - // parses the `ApnsPayload` and gives us (through `getData`) everything - // but the `aps` property. /** Helper function: fail. */ const err = (style: string) => @@ -176,20 +168,3 @@ export const fromAPNs = (data: ?JSONableDict): Notification | void => { // Despite the name `fromAPNs`, there is no parallel Android-side `fromFCM` // function here; the relevant task is performed in `FcmMessage.kt`. - -/** - * Extract Zulip notification data from the blob our iOS libraries give us. - * - * On validation error (indicating a bug in either client or server), - * logs a warning and returns void. - * - * On valid but unrecognized input (like a future, unknown type of - * notification event), returns void. - */ -export const fromPushNotificationIOS = (notification: PushNotificationIOS): Notification | void => { - // This is actually typed as ?Object (and so effectively `any`); but if - // present, it must be a JSONable dictionary. It's giving us the - // notification data, which was passed over APNs as JSON. - const data: ?JSONableDict = notification.getData(); - return fromAPNs(data); -}; diff --git a/src/notification/notifOpen.js b/src/notification/notifOpen.js index 268b7ea2a12..bc67c763e23 100644 --- a/src/notification/notifOpen.js +++ b/src/notification/notifOpen.js @@ -4,7 +4,6 @@ * @flow strict-local */ import { NativeModules, Platform } from 'react-native'; -import PushNotificationIOS from '@react-native-community/push-notification-ios'; import type { Notification } from './types'; import type { @@ -18,7 +17,7 @@ import type { } from '../types'; import { topicNarrow, pm1to1NarrowFromUser, pmNarrowFromRecipients } from '../utils/narrow'; import * as logging from '../utils/logging'; -import { fromPushNotificationIOS } from './extract'; +import { fromAPNs } from './extract'; import { isUrlOnRealm, tryParseUrl } from '../utils/url'; import { pmKeyRecipientsFromIds } from '../utils/recipient'; import { makeUserId } from '../api/idTypes'; @@ -29,6 +28,7 @@ import { doNarrow } from '../message/messagesActions'; import { accountSwitch } from '../account/accountActions'; import { getIsActiveAccount, tryGetActiveAccountState } from '../account/accountsSelectors'; import { identityOfAccount } from '../account/accountMisc'; +import type { JSONableDict } from '../utils/jsonable'; /** * Identify the account the notification is for, if possible. @@ -192,13 +192,13 @@ const readInitialNotification = async (): Promise => { const { Notifications } = NativeModules; return Notifications.readInitialNotification(); } - - const notification: ?PushNotificationIOS = await PushNotificationIOS.getInitialNotification(); + const { ZLPNotificationsStatus } = NativeModules; + const notification: JSONableDict | null = ZLPNotificationsStatus.initialNotification; if (!notification) { return null; } - return fromPushNotificationIOS(notification) || null; + return fromAPNs(notification) || null; }; export const narrowToNotification =