Skip to content

Commit 8a3ca1a

Browse files
committed
ios [nfc]: Cut notifications library out of handling "initial notification"
And handle it ourselves, through our dedicated iOS native module ZLPNotificationsStatus. (The "initial notification" is a notification that, by being tapped, launched the app from cold. PR #5676 was about notifications that are tapped when the app is already open, and that PR fixed a known bug.) This, like #5676, makes our iOS notification handling more transparent. My hope is to smooth the path to custom iOS notification code in zulip-flutter (zulip-flutter#122, etc.) by demonstrating a working implementation that's simple and avoids depending on the details of @react-native-community/push-notification-ios. Cutting the library out of this codepath makes one small difference in the payload we pass to `fromAPNs` on the JS side: the `aps` property is now present. (So, remove a comment saying it might be absent). We've never looked at that property, so this is NFC. Unlike in 2ae6f79 / #5676, there wasn't a call to the library's custom RCTConvert extensions to account for, as we did there with a cautious call to RCTJSONClean.
1 parent c979530 commit 8a3ca1a

File tree

3 files changed

+54
-11
lines changed

3 files changed

+54
-11
lines changed

ios/ZulipMobile/ZLPNotifications.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ class ZLPNotificationsEvents: RCTEventEmitter {
4545

4646
@objc(ZLPNotificationsStatus)
4747
class ZLPNotificationsStatus: NSObject {
48+
// For why we include this, see
49+
// https://reactnative.dev/docs/0.68/native-modules-ios#exporting-constants
50+
@objc
51+
static func requiresMainQueueSetup() -> Bool {
52+
// From the RN doc linked above:
53+
// > If your module does not require access to UIKit, then you should
54+
// > respond to `+ requiresMainQueueSetup` with NO.
55+
//
56+
// The launchOptions dictionary (used in `constantsToExport`) is
57+
// accessed via the UIApplicationDelegate protocol, which is part of
58+
// UIKit:
59+
// https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application?language=objc
60+
//
61+
// So I think to follow RN's advice about accessing UIKit, it's probably
62+
// right to return `true`.
63+
return true
64+
}
65+
66+
// The bridge object, provided by RN's RCTBridgeModule (via
67+
// RCT_EXTERN_MODULE in ZLPNotificationsBridge.m):
68+
// https://github.com/facebook/react-native/blob/v0.68.7/React/Base/RCTBridgeModule.h#L152-L159
69+
@objc
70+
var bridge: RCTBridge!
71+
4872
/// Whether the app can receive remote notifications.
4973
// Ideally we could subscribe to changes in this value, but there
5074
// doesn't seem to be an API for that. The caller can poll, e.g., by
@@ -60,4 +84,29 @@ class ZLPNotificationsStatus: NSObject {
6084
resolve(settings.authorizationStatus == UNAuthorizationStatus.authorized)
6185
})
6286
}
87+
88+
@objc
89+
func constantsToExport() -> [String: Any]! {
90+
var result: [String: Any] = [:]
91+
92+
// `launchOptions` comes via our AppDelegate's
93+
// `application:didFinishLaunchingWithOptions:` method override. From
94+
// the doc for that method (on UIApplicationDelegate):
95+
// https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application?language=objc
96+
// > A dictionary indicating the reason the app was launched (if any).
97+
// > The contents of this dictionary may be empty in situations where
98+
// > the user launched the app directly. […]
99+
//
100+
// In particular, for our purpose here: if the app was launched from a
101+
// notification, then it wasn't "launched […] directly".
102+
//
103+
// Empirically, launchOptions *itself* may be missing, which is
104+
// different from being empty; the distinction matters in Swift-land
105+
// where it's an error to access properties of nil. This doesn't seem
106+
// quite covered by "[t]he contents of this dictionary may be empty",
107+
// but anyway it explains our optional chaining on .launchOptions.
108+
result["initialNotification"] = bridge.launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] ?? kCFNull
109+
110+
return result
111+
}
63112
}

src/notification/extract.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,6 @@ export const fromAPNsImpl = (data: ?JSONableDict): Notification | void => {
3838
//
3939
// For the format this parses, see `ApnsPayload` in src/api/notificationTypes.js .
4040
//
41-
// Though in one case what it actually receives is more like this:
42-
// $Rest<ApnsPayload, {| aps: mixed |}>
43-
// That case is the "initial notification", a notification that launched
44-
// the app by being tapped, because the `PushNotificationIOS` library
45-
// parses the `ApnsPayload` and gives us (through `getData`) everything
46-
// but the `aps` property.
4741

4842
/** Helper function: fail. */
4943
const err = (style: string) =>

src/notification/notifOpen.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* @flow strict-local
55
*/
66
import { NativeModules, Platform } from 'react-native';
7-
import PushNotificationIOS from '@react-native-community/push-notification-ios';
87

98
import type { Notification } from './types';
109
import type {
@@ -18,7 +17,7 @@ import type {
1817
} from '../types';
1918
import { topicNarrow, pm1to1NarrowFromUser, pmNarrowFromRecipients } from '../utils/narrow';
2019
import * as logging from '../utils/logging';
21-
import { fromPushNotificationIOS } from './extract';
20+
import { fromAPNs } from './extract';
2221
import { isUrlOnRealm, tryParseUrl } from '../utils/url';
2322
import { pmKeyRecipientsFromIds } from '../utils/recipient';
2423
import { makeUserId } from '../api/idTypes';
@@ -29,6 +28,7 @@ import { doNarrow } from '../message/messagesActions';
2928
import { accountSwitch } from '../account/accountActions';
3029
import { getIsActiveAccount, tryGetActiveAccountState } from '../account/accountsSelectors';
3130
import { identityOfAccount } from '../account/accountMisc';
31+
import type { JSONableDict } from '../utils/jsonable';
3232

3333
/**
3434
* Identify the account the notification is for, if possible.
@@ -192,13 +192,13 @@ const readInitialNotification = async (): Promise<Notification | null> => {
192192
const { Notifications } = NativeModules;
193193
return Notifications.readInitialNotification();
194194
}
195-
196-
const notification: ?PushNotificationIOS = await PushNotificationIOS.getInitialNotification();
195+
const { ZLPNotificationsStatus } = NativeModules;
196+
const notification: JSONableDict | null = ZLPNotificationsStatus.initialNotification;
197197
if (!notification) {
198198
return null;
199199
}
200200

201-
return fromPushNotificationIOS(notification) || null;
201+
return fromAPNs(notification) || null;
202202
};
203203

204204
export const narrowToNotification =

0 commit comments

Comments
 (0)