Skip to content

Commit b9ae938

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 eb7a678 commit b9ae938

File tree

14 files changed

+760
-4
lines changed

14 files changed

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

+22
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 payload from launch options;
16+
// this will be null if the launch wasn't triggered by a notification.
17+
let notificationPayload = launchOptions?[.remoteNotification] as? [AnyHashable : Any]
18+
let api = NotificationHostApiImpl(notificationPayload.map { NotificationDataFromLaunch(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 maybeDataFromLaunch: NotificationDataFromLaunch?
27+
28+
init(_ maybeDataFromLaunch: NotificationDataFromLaunch?) {
29+
self.maybeDataFromLaunch = maybeDataFromLaunch
30+
}
31+
32+
func getNotificationDataFromLaunch() -> NotificationDataFromLaunch? {
33+
maybeDataFromLaunch
34+
}
35+
}

ios/Runner/Notifications.g.swift

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

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

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
Map<Object?, Object?> payload;
26+
27+
Object encode() {
28+
return <Object?>[
29+
payload,
30+
];
31+
}
32+
33+
static NotificationDataFromLaunch decode(Object result) {
34+
result as List<Object?>;
35+
return NotificationDataFromLaunch(
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 NotificationDataFromLaunch) {
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 NotificationDataFromLaunch.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+
/// Returns `launchOptions.remoteNotification`,
84+
/// which is the raw APNs data dictionary
85+
/// if the app launch was opened by a notification tap,
86+
/// else null. See Apple doc:
87+
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
88+
Future<NotificationDataFromLaunch?> getNotificationDataFromLaunch() async {
89+
final String pigeonVar_channelName = 'dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$pigeonVar_messageChannelSuffix';
90+
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
91+
pigeonVar_channelName,
92+
pigeonChannelCodec,
93+
binaryMessenger: pigeonVar_binaryMessenger,
94+
);
95+
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
96+
final List<Object?>? pigeonVar_replyList =
97+
await pigeonVar_sendFuture as List<Object?>?;
98+
if (pigeonVar_replyList == null) {
99+
throw _createConnectionError(pigeonVar_channelName);
100+
} else if (pigeonVar_replyList.length > 1) {
101+
throw PlatformException(
102+
code: pigeonVar_replyList[0]! as String,
103+
message: pigeonVar_replyList[1] as String?,
104+
details: pigeonVar_replyList[2],
105+
);
106+
} else {
107+
return (pigeonVar_replyList[0] as NotificationDataFromLaunch?);
108+
}
109+
}
110+
}

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)