Skip to content

Commit ca86fa8

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 595abb6 commit ca86fa8

File tree

12 files changed

+709
-3
lines changed

12 files changed

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

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

lib/main.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter/widgets.dart';
35

46
import 'licenses.dart';
57
import 'log.dart';
68
import 'model/binding.dart';
9+
import 'notifications/open.dart';
710
import 'notifications/receive.dart';
811
import 'widgets/app.dart';
912

10-
void main() {
13+
Future<void> main() async {
1114
assert(() {
1215
debugLogEnabled = true;
1316
return true;
1417
}());
1518
LicenseRegistry.addLicense(additionalLicenses);
1619
WidgetsFlutterBinding.ensureInitialized();
1720
LiveZulipBinding.ensureInitialized();
18-
NotificationService.instance.start();
21+
22+
// TODO remove this await here
23+
// TODO move this initialization to NotificationService.instance.start()
24+
await NotificationOpenManager.instance.init();
25+
26+
unawaited(NotificationService.instance.start());
1927
runApp(const ZulipApp());
2028
}

0 commit comments

Comments
 (0)