Skip to content

Commit ca941d1

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 00127ad commit ca941d1

15 files changed

+758
-2
lines changed

ios/Runner.xcodeproj/project.pbxproj

+4
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

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ import Flutter
88
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
99
) -> Bool {
1010
GeneratedPluginRegistrant.register(with: self)
11+
let controller = window?.rootViewController as! FlutterViewController
12+
13+
// Retrieve the remote notification payload from launch options;
14+
// this will be null if the launch wasn't triggered by a notification.
15+
let notificationPayload = launchOptions?[.remoteNotification] as? [AnyHashable : Any]
16+
let api = NotificationHostApiImpl(notificationPayload.map { NotificationDataFromLaunch(payload: $0) })
17+
NotificationHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
18+
1119
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1220
}
1321
}
22+
23+
private class NotificationHostApiImpl: NotificationHostApi {
24+
private let maybeDataFromLaunch: NotificationDataFromLaunch?
25+
26+
init(_ maybeDataFromLaunch: NotificationDataFromLaunch?) {
27+
self.maybeDataFromLaunch = maybeDataFromLaunch
28+
}
29+
30+
func getNotificationDataFromLaunch() -> NotificationDataFromLaunch? {
31+
maybeDataFromLaunch
32+
}
33+
}

ios/Runner/Notifications.g.swift

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Autogenerated from Pigeon (v25.0.0), 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+
/// Generated class from Pigeon that represents data sent in messages.
68+
struct NotificationDataFromLaunch {
69+
/// The raw payload that is attached to the notification,
70+
/// holding the information required to carry out the navigation.
71+
///
72+
/// See [NotificationHostApi.getNotificationDataFromLaunch].
73+
var payload: [AnyHashable?: Any?]
74+
75+
76+
// swift-format-ignore: AlwaysUseLowerCamelCase
77+
static func fromList(_ pigeonVar_list: [Any?]) -> NotificationDataFromLaunch? {
78+
let payload = pigeonVar_list[0] as! [AnyHashable?: Any?]
79+
80+
return NotificationDataFromLaunch(
81+
payload: payload
82+
)
83+
}
84+
func toList() -> [Any?] {
85+
return [
86+
payload
87+
]
88+
}
89+
}
90+
91+
private class NotificationsPigeonCodecReader: FlutterStandardReader {
92+
override func readValue(ofType type: UInt8) -> Any? {
93+
switch type {
94+
case 129:
95+
return NotificationDataFromLaunch.fromList(self.readValue() as! [Any?])
96+
default:
97+
return super.readValue(ofType: type)
98+
}
99+
}
100+
}
101+
102+
private class NotificationsPigeonCodecWriter: FlutterStandardWriter {
103+
override func writeValue(_ value: Any) {
104+
if let value = value as? NotificationDataFromLaunch {
105+
super.writeByte(129)
106+
super.writeValue(value.toList())
107+
} else {
108+
super.writeValue(value)
109+
}
110+
}
111+
}
112+
113+
private class NotificationsPigeonCodecReaderWriter: FlutterStandardReaderWriter {
114+
override func reader(with data: Data) -> FlutterStandardReader {
115+
return NotificationsPigeonCodecReader(data: data)
116+
}
117+
118+
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
119+
return NotificationsPigeonCodecWriter(data: data)
120+
}
121+
}
122+
123+
class NotificationsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
124+
static let shared = NotificationsPigeonCodec(readerWriter: NotificationsPigeonCodecReaderWriter())
125+
}
126+
127+
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
128+
protocol NotificationHostApi {
129+
/// Retrieves notification data if the app was launched by tapping on a notification.
130+
///
131+
/// Returns `launchOptions.remoteNotification`,
132+
/// which is the raw APNs data dictionary
133+
/// if the app launch was opened by a notification tap,
134+
/// else null. See Apple doc:
135+
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
136+
func getNotificationDataFromLaunch() throws -> NotificationDataFromLaunch?
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+
/// Returns `launchOptions.remoteNotification`,
148+
/// which is the raw APNs data dictionary
149+
/// if the app launch was opened by a notification tap,
150+
/// else null. See Apple doc:
151+
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
152+
let getNotificationDataFromLaunchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
153+
if let api = api {
154+
getNotificationDataFromLaunchChannel.setMessageHandler { _, reply in
155+
do {
156+
let result = try api.getNotificationDataFromLaunch()
157+
reply(wrapResult(result))
158+
} catch {
159+
reply(wrapError(error))
160+
}
161+
}
162+
} else {
163+
getNotificationDataFromLaunchChannel.setMessageHandler(nil)
164+
}
165+
}
166+
}

lib/host/notifications.dart

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export './notifications.g.dart';

lib/host/notifications.g.dart

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Autogenerated from Pigeon (v25.0.0), 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+
class NotificationDataFromLaunch {
19+
NotificationDataFromLaunch({
20+
required this.payload,
21+
});
22+
23+
/// The raw payload that is attached to the notification,
24+
/// holding the information required to carry out the navigation.
25+
///
26+
/// See [NotificationHostApi.getNotificationDataFromLaunch].
27+
Map<Object?, Object?> payload;
28+
29+
Object encode() {
30+
return <Object?>[
31+
payload,
32+
];
33+
}
34+
35+
static NotificationDataFromLaunch decode(Object result) {
36+
result as List<Object?>;
37+
return NotificationDataFromLaunch(
38+
payload: (result[0] as Map<Object?, Object?>?)!.cast<Object?, Object?>(),
39+
);
40+
}
41+
}
42+
43+
44+
class _PigeonCodec extends StandardMessageCodec {
45+
const _PigeonCodec();
46+
@override
47+
void writeValue(WriteBuffer buffer, Object? value) {
48+
if (value is int) {
49+
buffer.putUint8(4);
50+
buffer.putInt64(value);
51+
} else if (value is NotificationDataFromLaunch) {
52+
buffer.putUint8(129);
53+
writeValue(buffer, value.encode());
54+
} else {
55+
super.writeValue(buffer, value);
56+
}
57+
}
58+
59+
@override
60+
Object? readValueOfType(int type, ReadBuffer buffer) {
61+
switch (type) {
62+
case 129:
63+
return NotificationDataFromLaunch.decode(readValue(buffer)!);
64+
default:
65+
return super.readValueOfType(type, buffer);
66+
}
67+
}
68+
}
69+
70+
class NotificationHostApi {
71+
/// Constructor for [NotificationHostApi]. The [binaryMessenger] named argument is
72+
/// available for dependency injection. If it is left null, the default
73+
/// BinaryMessenger will be used which routes to the host platform.
74+
NotificationHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
75+
: pigeonVar_binaryMessenger = binaryMessenger,
76+
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
77+
final BinaryMessenger? pigeonVar_binaryMessenger;
78+
79+
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
80+
81+
final String pigeonVar_messageChannelSuffix;
82+
83+
/// Retrieves notification data if the app was launched by tapping on a notification.
84+
///
85+
/// Returns `launchOptions.remoteNotification`,
86+
/// which is the raw APNs data dictionary
87+
/// if the app launch was opened by a notification tap,
88+
/// else null. See Apple doc:
89+
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
90+
Future<NotificationDataFromLaunch?> getNotificationDataFromLaunch() async {
91+
final String pigeonVar_channelName = 'dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$pigeonVar_messageChannelSuffix';
92+
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
93+
pigeonVar_channelName,
94+
pigeonChannelCodec,
95+
binaryMessenger: pigeonVar_binaryMessenger,
96+
);
97+
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
98+
final List<Object?>? pigeonVar_replyList =
99+
await pigeonVar_sendFuture as List<Object?>?;
100+
if (pigeonVar_replyList == null) {
101+
throw _createConnectionError(pigeonVar_channelName);
102+
} else if (pigeonVar_replyList.length > 1) {
103+
throw PlatformException(
104+
code: pigeonVar_replyList[0]! as String,
105+
message: pigeonVar_replyList[1] as String?,
106+
details: pigeonVar_replyList[2],
107+
);
108+
} else {
109+
return (pigeonVar_replyList[0] as NotificationDataFromLaunch?);
110+
}
111+
}
112+
}

lib/model/binding.dart

+14
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 _hostApi = notif_pigeon.NotificationHostApi();
319+
320+
Future<notif_pigeon.NotificationDataFromLaunch?> getNotificationDataFromLaunch() =>
321+
_hostApi.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)