Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ios [nfc]: Cut notifications library out of handling "initial notification" #5740

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions ios/ZulipMobile/ZLPNotifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
25 changes: 0 additions & 25 deletions src/notification/extract.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<ApnsPayload, {| aps: mixed |}>
// 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) =>
Expand Down Expand Up @@ -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);
};
10 changes: 5 additions & 5 deletions src/notification/notifOpen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -192,13 +192,13 @@ const readInitialNotification = async (): Promise<Notification | null> => {
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 =
Expand Down