Skip to content

Commit ab3ff84

Browse files
notif ios: Navigate when app launched from notification
Introduces a new Pigeon API file, and adds the corresponding bindings in Swift. Unlike the `pigeon/android_notifications.dart` API this doesn't use the ZulipPlugin hack, as that is only needed when we want the Pigeon functions to be available inside a background isolate (see doc in `zulip_plugin/pubspec.yaml`). Since the notification tap will trigger an app launch first (if not running already) anyway, we can be sure that these new functions won't be running on a Dart background isolate, thus not needing the ZulipPlugin hack.
1 parent a5a0fff commit ab3ff84

File tree

12 files changed

+735
-4
lines changed

12 files changed

+735
-4
lines changed

ios/Runner.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
1414
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
1515
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
16+
B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34E9F082D776BEB0009AED2 /* Notifications.g.swift */; };
1617
F311C174AF9C005CE4AADD72 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EAE3F3F518B95B7BFEB4FE7 /* Pods_Runner.framework */; };
1718
/* End PBXBuildFile section */
1819

@@ -48,6 +49,7 @@
4849
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
4950
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
5051
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
52+
B34E9F082D776BEB0009AED2 /* Notifications.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.g.swift; sourceTree = "<group>"; };
5153
B3AF53A72CA20BD10039801D /* Zulip.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Zulip.xcconfig; path = Flutter/Zulip.xcconfig; sourceTree = "<group>"; };
5254
/* End PBXFileReference section */
5355

@@ -115,6 +117,7 @@
115117
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
116118
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
117119
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
120+
B34E9F082D776BEB0009AED2 /* Notifications.g.swift */,
118121
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
119122
);
120123
path = Runner;
@@ -297,6 +300,7 @@
297300
buildActionMask = 2147483647;
298301
files = (
299302
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
303+
B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */,
300304
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
301305
);
302306
runOnlyForDeploymentPostprocessing = 0;

ios/Runner/AppDelegate.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ import Flutter
88
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
99
) -> Bool {
1010
GeneratedPluginRegistrant.register(with: self)
11+
guard let controller = window?.rootViewController as? FlutterViewController else {
12+
fatalError("rootViewController is not type FlutterViewController")
13+
}
14+
15+
// Retrieve the remote notification data from launch options,
16+
// this will be null if the launch wasn't triggered by a notification.
17+
let notificationData = launchOptions?[.remoteNotification] as? [AnyHashable : Any]
18+
let api = NotificationHostApiImpl(notificationData.map { NotificationPayloadForOpen(payload: $0) })
19+
NotificationHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
20+
1121
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1222
}
1323
}
24+
25+
private class NotificationHostApiImpl: NotificationHostApi {
26+
private let maybeNotifPayload: NotificationPayloadForOpen?
27+
28+
init(_ maybeNotifPayload: NotificationPayloadForOpen?) {
29+
self.maybeNotifPayload = maybeNotifPayload
30+
}
31+
32+
func getNotificationDataFromLaunch() -> NotificationPayloadForOpen? {
33+
maybeNotifPayload
34+
}
35+
}

ios/Runner/Notifications.g.swift

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Autogenerated from Pigeon (v24.2.1), do not edit directly.
2+
// See also: https://pub.dev/packages/pigeon
3+
4+
import Foundation
5+
6+
#if os(iOS)
7+
import Flutter
8+
#elseif os(macOS)
9+
import FlutterMacOS
10+
#else
11+
#error("Unsupported platform.")
12+
#endif
13+
14+
/// Error class for passing custom error details to Dart side.
15+
final class PigeonError: Error {
16+
let code: String
17+
let message: String?
18+
let details: Sendable?
19+
20+
init(code: String, message: String?, details: Sendable?) {
21+
self.code = code
22+
self.message = message
23+
self.details = details
24+
}
25+
26+
var localizedDescription: String {
27+
return
28+
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
29+
}
30+
}
31+
32+
private func wrapResult(_ result: Any?) -> [Any?] {
33+
return [result]
34+
}
35+
36+
private func wrapError(_ error: Any) -> [Any?] {
37+
if let pigeonError = error as? PigeonError {
38+
return [
39+
pigeonError.code,
40+
pigeonError.message,
41+
pigeonError.details,
42+
]
43+
}
44+
if let flutterError = error as? FlutterError {
45+
return [
46+
flutterError.code,
47+
flutterError.message,
48+
flutterError.details,
49+
]
50+
}
51+
return [
52+
"\(error)",
53+
"\(type(of: error))",
54+
"Stacktrace: \(Thread.callStackSymbols)",
55+
]
56+
}
57+
58+
private func isNullish(_ value: Any?) -> Bool {
59+
return value is NSNull || value == nil
60+
}
61+
62+
private func nilOrValue<T>(_ value: Any?) -> T? {
63+
if value is NSNull { return nil }
64+
return value as! T?
65+
}
66+
67+
/// The payload that is attached to each notification and holds
68+
/// the information required to carry out the navigation.
69+
///
70+
/// Generated class from Pigeon that represents data sent in messages.
71+
struct NotificationPayloadForOpen {
72+
var payload: [AnyHashable?: Any?]
73+
74+
75+
// swift-format-ignore: AlwaysUseLowerCamelCase
76+
static func fromList(_ pigeonVar_list: [Any?]) -> NotificationPayloadForOpen? {
77+
let payload = pigeonVar_list[0] as! [AnyHashable?: Any?]
78+
79+
return NotificationPayloadForOpen(
80+
payload: payload
81+
)
82+
}
83+
func toList() -> [Any?] {
84+
return [
85+
payload
86+
]
87+
}
88+
}
89+
90+
private class NotificationsPigeonCodecReader: FlutterStandardReader {
91+
override func readValue(ofType type: UInt8) -> Any? {
92+
switch type {
93+
case 129:
94+
return NotificationPayloadForOpen.fromList(self.readValue() as! [Any?])
95+
default:
96+
return super.readValue(ofType: type)
97+
}
98+
}
99+
}
100+
101+
private class NotificationsPigeonCodecWriter: FlutterStandardWriter {
102+
override func writeValue(_ value: Any) {
103+
if let value = value as? NotificationPayloadForOpen {
104+
super.writeByte(129)
105+
super.writeValue(value.toList())
106+
} else {
107+
super.writeValue(value)
108+
}
109+
}
110+
}
111+
112+
private class NotificationsPigeonCodecReaderWriter: FlutterStandardReaderWriter {
113+
override func reader(with data: Data) -> FlutterStandardReader {
114+
return NotificationsPigeonCodecReader(data: data)
115+
}
116+
117+
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
118+
return NotificationsPigeonCodecWriter(data: data)
119+
}
120+
}
121+
122+
class NotificationsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
123+
static let shared = NotificationsPigeonCodec(readerWriter: NotificationsPigeonCodecReaderWriter())
124+
}
125+
126+
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
127+
protocol NotificationHostApi {
128+
/// Retrieves notification data if the app was launched by tapping on a notification.
129+
///
130+
/// On iOS, this checks and returns value for the `remoteNotification` key
131+
/// in the `launchOptions` map. The value could be either the raw APNs data
132+
/// dictionary, if the launch of the app was triggered by a notification tap,
133+
/// otherwise it will be null.
134+
///
135+
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
136+
func getNotificationDataFromLaunch() throws -> NotificationPayloadForOpen?
137+
}
138+
139+
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
140+
class NotificationHostApiSetup {
141+
static var codec: FlutterStandardMessageCodec { NotificationsPigeonCodec.shared }
142+
/// Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`.
143+
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
144+
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
145+
/// Retrieves notification data if the app was launched by tapping on a notification.
146+
///
147+
/// On iOS, this checks and returns value for the `remoteNotification` key
148+
/// in the `launchOptions` map. The value could be either the raw APNs data
149+
/// dictionary, if the launch of the app was triggered by a notification tap,
150+
/// otherwise it will be null.
151+
///
152+
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
153+
let getNotificationDataFromLaunchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
154+
if let api = api {
155+
getNotificationDataFromLaunchChannel.setMessageHandler { _, reply in
156+
do {
157+
let result = try api.getNotificationDataFromLaunch()
158+
reply(wrapResult(result))
159+
} catch {
160+
reply(wrapError(error))
161+
}
162+
}
163+
} else {
164+
getNotificationDataFromLaunchChannel.setMessageHandler(nil)
165+
}
166+
}
167+
}

lib/host/notifications.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export './notifications.g.dart';

lib/host/notifications.g.dart

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Autogenerated from Pigeon (v24.2.1), do not edit directly.
2+
// See also: https://pub.dev/packages/pigeon
3+
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
4+
5+
import 'dart:async';
6+
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
7+
8+
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
9+
import 'package:flutter/services.dart';
10+
11+
PlatformException _createConnectionError(String channelName) {
12+
return PlatformException(
13+
code: 'channel-error',
14+
message: 'Unable to establish connection on channel: "$channelName".',
15+
);
16+
}
17+
18+
/// The payload that is attached to each notification and holds
19+
/// the information required to carry out the navigation.
20+
class NotificationPayloadForOpen {
21+
NotificationPayloadForOpen({
22+
required this.payload,
23+
});
24+
25+
Map<Object?, Object?> payload;
26+
27+
Object encode() {
28+
return <Object?>[
29+
payload,
30+
];
31+
}
32+
33+
static NotificationPayloadForOpen decode(Object result) {
34+
result as List<Object?>;
35+
return NotificationPayloadForOpen(
36+
payload: (result[0] as Map<Object?, Object?>?)!.cast<Object?, Object?>(),
37+
);
38+
}
39+
}
40+
41+
42+
class _PigeonCodec extends StandardMessageCodec {
43+
const _PigeonCodec();
44+
@override
45+
void writeValue(WriteBuffer buffer, Object? value) {
46+
if (value is int) {
47+
buffer.putUint8(4);
48+
buffer.putInt64(value);
49+
} else if (value is NotificationPayloadForOpen) {
50+
buffer.putUint8(129);
51+
writeValue(buffer, value.encode());
52+
} else {
53+
super.writeValue(buffer, value);
54+
}
55+
}
56+
57+
@override
58+
Object? readValueOfType(int type, ReadBuffer buffer) {
59+
switch (type) {
60+
case 129:
61+
return NotificationPayloadForOpen.decode(readValue(buffer)!);
62+
default:
63+
return super.readValueOfType(type, buffer);
64+
}
65+
}
66+
}
67+
68+
class NotificationHostApi {
69+
/// Constructor for [NotificationHostApi]. The [binaryMessenger] named argument is
70+
/// available for dependency injection. If it is left null, the default
71+
/// BinaryMessenger will be used which routes to the host platform.
72+
NotificationHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
73+
: pigeonVar_binaryMessenger = binaryMessenger,
74+
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
75+
final BinaryMessenger? pigeonVar_binaryMessenger;
76+
77+
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
78+
79+
final String pigeonVar_messageChannelSuffix;
80+
81+
/// Retrieves notification data if the app was launched by tapping on a notification.
82+
///
83+
/// On iOS, this checks and returns value for the `remoteNotification` key
84+
/// in the `launchOptions` map. The value could be either the raw APNs data
85+
/// dictionary, if the launch of the app was triggered by a notification tap,
86+
/// otherwise it will be null.
87+
///
88+
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
89+
Future<NotificationPayloadForOpen?> getNotificationDataFromLaunch() async {
90+
final String pigeonVar_channelName = 'dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$pigeonVar_messageChannelSuffix';
91+
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
92+
pigeonVar_channelName,
93+
pigeonChannelCodec,
94+
binaryMessenger: pigeonVar_binaryMessenger,
95+
);
96+
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
97+
final List<Object?>? pigeonVar_replyList =
98+
await pigeonVar_sendFuture as List<Object?>?;
99+
if (pigeonVar_replyList == null) {
100+
throw _createConnectionError(pigeonVar_channelName);
101+
} else if (pigeonVar_replyList.length > 1) {
102+
throw PlatformException(
103+
code: pigeonVar_replyList[0]! as String,
104+
message: pigeonVar_replyList[1] as String?,
105+
details: pigeonVar_replyList[2],
106+
);
107+
} else {
108+
return (pigeonVar_replyList[0] as NotificationPayloadForOpen?);
109+
}
110+
}
111+
}

lib/model/binding.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher.dart' as url_launcher;
1111
import 'package:wakelock_plus/wakelock_plus.dart' as wakelock_plus;
1212

1313
import '../host/android_notifications.dart';
14+
import '../host/notifications.dart' as notif_pigeon;
1415
import '../log.dart';
1516
import '../widgets/store.dart';
1617
import 'store.dart';
@@ -168,6 +169,9 @@ abstract class ZulipBinding {
168169
/// Wraps the [AndroidNotificationHostApi] constructor.
169170
AndroidNotificationHostApi get androidNotificationHost;
170171

172+
/// Wraps the [notif_pigeon.NotificationHostApi] class.
173+
NotificationPigeonApi get notificationPigeonApi;
174+
171175
/// Pick files from the media library, via package:file_picker.
172176
///
173177
/// This wraps [file_picker.pickFiles].
@@ -310,6 +314,13 @@ class PackageInfo {
310314
});
311315
}
312316

317+
class NotificationPigeonApi {
318+
final _notifInteractionHost = notif_pigeon.NotificationHostApi();
319+
320+
Future<notif_pigeon.NotificationPayloadForOpen?> getNotificationDataFromLaunch() =>
321+
_notifInteractionHost.getNotificationDataFromLaunch();
322+
}
323+
313324
/// A concrete binding for use in the live application.
314325
///
315326
/// The global store returned by [getGlobalStore], and consequently by
@@ -442,6 +453,9 @@ class LiveZulipBinding extends ZulipBinding {
442453
@override
443454
AndroidNotificationHostApi get androidNotificationHost => AndroidNotificationHostApi();
444455

456+
@override
457+
NotificationPigeonApi get notificationPigeonApi => NotificationPigeonApi();
458+
445459
@override
446460
Future<file_picker.FilePickerResult?> pickFiles({
447461
bool allowMultiple = false,

0 commit comments

Comments
 (0)