Skip to content

Commit 3109ff1

Browse files
notif ios: Navigate when app running but in background
1 parent 330c6c2 commit 3109ff1

File tree

8 files changed

+292
-3
lines changed

8 files changed

+292
-3
lines changed

ios/Runner/AppDelegate.swift

+39
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Flutter
33

44
@main
55
@objc class AppDelegate: FlutterAppDelegate {
6+
private var notificationTapEventListener: NotificationTapEventListener?
7+
68
override func application(
79
_ application: UIApplication,
810
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@@ -18,8 +20,26 @@ import Flutter
1820
let api = NotificationHostApiImpl(notificationData.map { NotificationPayloadForOpen(payload: $0) })
1921
NotificationHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
2022

23+
notificationTapEventListener = NotificationTapEventListener()
24+
NotificationTapEventsStreamHandler.register(with: controller.binaryMessenger, streamHandler: notificationTapEventListener!)
25+
26+
// Setup handler for notification tap while the app is running.
27+
UNUserNotificationCenter.current().delegate = self
28+
2129
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
2230
}
31+
32+
override func userNotificationCenter(
33+
_ center: UNUserNotificationCenter,
34+
didReceive response: UNNotificationResponse,
35+
withCompletionHandler completionHandler: @escaping () -> Void
36+
) {
37+
if let listener = notificationTapEventListener {
38+
let userInfo = response.notification.request.content.userInfo
39+
listener.onNotificationTapEvent(data: NotificationPayloadForOpen(payload: userInfo))
40+
completionHandler()
41+
}
42+
}
2343
}
2444

2545
private class NotificationHostApiImpl: NotificationHostApi {
@@ -33,3 +53,22 @@ private class NotificationHostApiImpl: NotificationHostApi {
3353
maybeNotifPayload
3454
}
3555
}
56+
57+
class NotificationTapEventListener: NotificationTapEventsStreamHandler {
58+
var eventSink: PigeonEventSink<NotificationPayloadForOpen>?
59+
60+
override func onListen(withArguments arguments: Any?, sink: PigeonEventSink<NotificationPayloadForOpen>) {
61+
eventSink = sink
62+
}
63+
64+
func onNotificationTapEvent(data: NotificationPayloadForOpen) {
65+
if let eventSink = eventSink {
66+
eventSink.success(data)
67+
}
68+
}
69+
70+
func onEventsDone() {
71+
eventSink?.endOfStream()
72+
eventSink = nil
73+
}
74+
}

ios/Runner/Notifications.g.swift

+66
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ class NotificationsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
120120
static let shared = NotificationsPigeonCodec(readerWriter: NotificationsPigeonCodecReaderWriter())
121121
}
122122

123+
var notificationsPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: NotificationsPigeonCodecReaderWriter());
124+
123125
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
124126
protocol NotificationHostApi {
125127
/// Retrieves notification data if the app was launched by tapping on a notification.
@@ -154,3 +156,67 @@ class NotificationHostApiSetup {
154156
}
155157
}
156158
}
159+
160+
private class PigeonStreamHandler<ReturnType>: NSObject, FlutterStreamHandler {
161+
private let wrapper: PigeonEventChannelWrapper<ReturnType>
162+
private var pigeonSink: PigeonEventSink<ReturnType>? = nil
163+
164+
init(wrapper: PigeonEventChannelWrapper<ReturnType>) {
165+
self.wrapper = wrapper
166+
}
167+
168+
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
169+
-> FlutterError?
170+
{
171+
pigeonSink = PigeonEventSink<ReturnType>(events)
172+
wrapper.onListen(withArguments: arguments, sink: pigeonSink!)
173+
return nil
174+
}
175+
176+
func onCancel(withArguments arguments: Any?) -> FlutterError? {
177+
pigeonSink = nil
178+
wrapper.onCancel(withArguments: arguments)
179+
return nil
180+
}
181+
}
182+
183+
class PigeonEventChannelWrapper<ReturnType> {
184+
func onListen(withArguments arguments: Any?, sink: PigeonEventSink<ReturnType>) {}
185+
func onCancel(withArguments arguments: Any?) {}
186+
}
187+
188+
class PigeonEventSink<ReturnType> {
189+
private let sink: FlutterEventSink
190+
191+
init(_ sink: @escaping FlutterEventSink) {
192+
self.sink = sink
193+
}
194+
195+
func success(_ value: ReturnType) {
196+
sink(value)
197+
}
198+
199+
func error(code: String, message: String?, details: Any?) {
200+
sink(FlutterError(code: code, message: message, details: details))
201+
}
202+
203+
func endOfStream() {
204+
sink(FlutterEndOfEventStream)
205+
}
206+
207+
}
208+
209+
class NotificationTapEventsStreamHandler: PigeonEventChannelWrapper<NotificationPayloadForOpen> {
210+
static func register(with messenger: FlutterBinaryMessenger,
211+
instanceName: String = "",
212+
streamHandler: NotificationTapEventsStreamHandler) {
213+
var channelName = "dev.flutter.pigeon.zulip.NotificationHostEvents.notificationTapEvents"
214+
if !instanceName.isEmpty {
215+
channelName += ".\(instanceName)"
216+
}
217+
let internalStreamHandler = PigeonStreamHandler<NotificationPayloadForOpen>(wrapper: streamHandler)
218+
let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: notificationsPigeonMethodCodec)
219+
channel.setStreamHandler(internalStreamHandler)
220+
}
221+
}
222+

lib/host/notifications.g.dart

+14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class _PigeonCodec extends StandardMessageCodec {
6363
}
6464
}
6565

66+
const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec());
67+
6668
class NotificationHostApi {
6769
/// Constructor for [NotificationHostApi]. The [binaryMessenger] named argument is
6870
/// available for dependency injection. If it is left null, the default
@@ -103,3 +105,15 @@ class NotificationHostApi {
103105
}
104106
}
105107
}
108+
109+
Stream<NotificationPayloadForOpen> notificationTapEvents( {String instanceName = ''}) {
110+
if (instanceName.isNotEmpty) {
111+
instanceName = '.$instanceName';
112+
}
113+
final EventChannel notificationTapEventsChannel =
114+
EventChannel('dev.flutter.pigeon.zulip.NotificationHostEvents.notificationTapEvents$instanceName', pigeonMethodCodec);
115+
return notificationTapEventsChannel.receiveBroadcastStream().map((dynamic event) {
116+
return event as NotificationPayloadForOpen;
117+
});
118+
}
119+

lib/model/binding.dart

+3
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ class NotificationPigeonApi {
319319

320320
Future<notif_pigeon.NotificationPayloadForOpen?> getNotificationDataFromLaunch() =>
321321
_notifInteractionHost.getNotificationDataFromLaunch();
322+
323+
Stream<notif_pigeon.NotificationPayloadForOpen> notificationTapEventsStream() =>
324+
notif_pigeon.notificationTapEvents();
322325
}
323326

324327
/// A concrete binding for use in the live application.

lib/notifications/open.dart

+21
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../host/notifications.dart';
1212
import '../log.dart';
1313
import '../model/binding.dart';
1414
import '../model/narrow.dart';
15+
import '../widgets/app.dart';
1516
import '../widgets/dialog.dart';
1617
import '../widgets/message_list.dart';
1718
import '../widgets/page.dart';
@@ -32,6 +33,8 @@ class NotificationOpenManager {
3233
switch (defaultTargetPlatform) {
3334
case TargetPlatform.iOS:
3435
_notifLaunchData = await _notifPigeonApi.getNotificationDataFromLaunch();
36+
_notifPigeonApi.notificationTapEventsStream()
37+
.listen(_navigateForNotification);
3538

3639
case TargetPlatform.android:
3740
case TargetPlatform.fuchsia:
@@ -79,6 +82,24 @@ class NotificationOpenManager {
7982
// TODO(#82): Open at specific message, not just conversation
8083
narrow: openData.narrow);
8184
}
85+
86+
/// Navigates to the [MessageListPage] of the specific conversation
87+
/// for the provided payload that was attached while creating the
88+
/// notification.
89+
Future<void> _navigateForNotification(NotificationPayloadForOpen payload) async {
90+
assert(debugLog('opened notif: ${jsonEncode(payload.payload)}'));
91+
92+
NavigatorState navigator = await ZulipApp.navigator;
93+
final context = navigator.context;
94+
assert(context.mounted);
95+
if (!context.mounted) return; // TODO(linter): this is impossible as there's no actual async gap, but the use_build_context_synchronously lint doesn't see that
96+
97+
final route = _routeForNotification(context, payload);
98+
if (route == null) return; // TODO(log)
99+
100+
// TODO(nav): Better interact with existing nav stack on notif open
101+
unawaited(navigator.push(route));
102+
}
82103
}
83104

84105
class NotificationDataForOpen {

pigeon/notifications.dart

+12-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import 'package:pigeon/pigeon.dart';
77
swiftOut: 'ios/Runner/Notifications.g.swift',
88
))
99

10+
/// The payload that is attached to each notification and holds
11+
/// the information required to carry out the navigation.
12+
///
13+
/// On iOS, the notification payload will be the APNs data from
14+
/// the server.
1015
class NotificationPayloadForOpen {
1116
const NotificationPayloadForOpen({required this.payload});
1217
final Map<Object?, Object?> payload;
@@ -15,8 +20,12 @@ class NotificationPayloadForOpen {
1520
@HostApi()
1621
abstract class NotificationHostApi {
1722
/// Retrieves notification data if the app was launched by tapping on a notification.
18-
///
19-
/// On iOS, the notification payload will be the APNs data from
20-
/// the server.
2123
NotificationPayloadForOpen? getNotificationDataFromLaunch();
2224
}
25+
26+
@EventChannelApi()
27+
abstract class NotificationHostEvents {
28+
/// An event stream that emits a notification payload when
29+
/// app encounters a notification tap, while the app is runnning.
30+
NotificationPayloadForOpen notificationTapEvents();
31+
}

test/model/binding.dart

+12
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,18 @@ class FakeNotificationPigeonApi implements NotificationPigeonApi {
744744
@override
745745
Future<NotificationPayloadForOpen?> getNotificationDataFromLaunch() async =>
746746
_notificationDataFromLaunch;
747+
748+
StreamController<NotificationPayloadForOpen>? _notificationTapEventsStreamController;
749+
750+
void addNotificationTapEvent(NotificationPayloadForOpen data) {
751+
_notificationTapEventsStreamController!.add(data);
752+
}
753+
754+
@override
755+
Stream<NotificationPayloadForOpen> notificationTapEventsStream() {
756+
_notificationTapEventsStreamController ??= StreamController();
757+
return _notificationTapEventsStreamController!.stream;
758+
}
747759
}
748760

749761
typedef AndroidNotificationHostApiNotifyCall = ({

0 commit comments

Comments
 (0)