Skip to content

Commit 330c6c2

Browse files
notif ios: Navigate when app launched from notification
1 parent 595abb6 commit 330c6c2

File tree

12 files changed

+705
-3
lines changed

12 files changed

+705
-3
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 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

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

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

lib/host/notifications.g.dart

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

lib/main.dart

+10-2
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
}

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 _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 [loadGlobalStore], 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)