Skip to content

Commit e5b78c6

Browse files
Foreground notifications (#160)
* Foreground notifications * Change code signing * Revert code signing changes
1 parent badce07 commit e5b78c6

7 files changed

Lines changed: 107 additions & 27 deletions

File tree

applogic/src/nse/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct IncomingNotificationContent {
1515

1616
#[derive(Serialize, Deserialize)]
1717
struct NotificationBatch {
18+
badge_count: u32,
1819
removals: Vec<String>,
1920
additions: Vec<NotificationContent>,
2021
}
@@ -24,6 +25,7 @@ struct NotificationContent {
2425
identifier: String,
2526
title: String,
2627
body: String,
28+
data: String,
2729
}
2830

2931
#[no_mangle]
@@ -37,41 +39,49 @@ pub extern "C" fn process_new_messages(content: *const c_char) -> *mut c_char {
3739
let incoming_content: IncomingNotificationContent = serde_json::from_str(json_str).unwrap();
3840

3941
// Test notifictaions only for now
40-
let (removals, additions) = match &incoming_content.data[..] {
42+
let (badge_count, removals, additions) = match &incoming_content.data[..] {
4143
"add" => (
44+
1,
4245
vec!["documentation".to_string()],
4346
vec![
4447
NotificationContent {
4548
identifier: "documentation".to_string(),
4649
title: "Added the placeholder notification".to_string(),
4750
body: "Add operation".to_string(),
51+
data: incoming_content.data.clone(),
4852
},
4953
NotificationContent {
5054
identifier: "placeholder".to_string(),
5155
title: "Placeholder notification".to_string(),
5256
body: "This is a placeholder notification".to_string(),
57+
data: incoming_content.data.clone(),
5358
},
5459
],
5560
),
5661
"remove" => (
62+
0,
5763
vec!["documentation".to_string(), "placeholder".to_string()],
5864
vec![NotificationContent {
5965
identifier: "documentation".to_string(),
6066
title: "Removed the placeholder notification".to_string(),
6167
body: "Remove operation".to_string(),
68+
data: incoming_content.data.clone(),
6269
}],
6370
),
6471
_ => (
72+
0,
6573
vec!["documentation".to_string()],
6674
vec![NotificationContent {
6775
identifier: "documentation".to_string(),
6876
title: "Could not process command".to_string(),
6977
body: format!("Unknown command: {}", incoming_content.data),
78+
data: incoming_content.data.clone(),
7079
}],
7180
),
7281
};
7382

7483
let batch = NotificationBatch {
84+
badge_count,
7585
removals,
7686
additions,
7787
};

prototype/ios/NotificationService/NotificationService.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// NotificationService.swift
33
// NotificationService
44
//
5-
// Created by Raphael Robert on 17.07.2024.
6-
//
75

86
import UserNotifications
97
import Foundation
@@ -14,6 +12,7 @@ struct IncomingNotificationContent: Codable {
1412
}
1513

1614
struct NotificationBatch: Codable {
15+
let badge_count: UInt32
1716
let removals: [String]
1817
let additions: [NotificationContent]
1918
}
@@ -22,7 +21,8 @@ struct NotificationContent: Codable {
2221
let identifier: String
2322
let title: String
2423
let body: String
25-
}
24+
let data: String
25+
}
2626

2727
class NotificationService: UNNotificationServiceExtension {
2828

@@ -62,7 +62,7 @@ class NotificationService: UNNotificationServiceExtension {
6262
if let responseData = responseString.data(using: .utf8),
6363
let notificationBatch = try? JSONDecoder().decode(NotificationBatch.self, from: responseData) {
6464

65-
handleNotificationBatch(notificationBatch, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
65+
handleNotificationBatch(notificationBatch, contentHandler: contentHandler)
6666
} else {
6767
contentHandler(request.content)
6868
}
@@ -84,7 +84,7 @@ class NotificationService: UNNotificationServiceExtension {
8484
}
8585
}
8686

87-
func handleNotificationBatch(_ batch: NotificationBatch, contentHandler: @escaping (UNNotificationContent) -> Void, bestAttemptContent: UNMutableNotificationContent) {
87+
func handleNotificationBatch(_ batch: NotificationBatch, contentHandler: @escaping (UNNotificationContent) -> Void) {
8888
let center = UNUserNotificationCenter.current()
8989
let dispatchGroup = DispatchGroup()
9090

@@ -102,6 +102,8 @@ class NotificationService: UNNotificationServiceExtension {
102102
let newContent = UNMutableNotificationContent()
103103
newContent.title = notificationContent.title
104104
newContent.body = notificationContent.body
105+
newContent.sound = UNNotificationSound.default
106+
newContent.userInfo["customData"] = notificationContent.data
105107
let request = UNNotificationRequest(identifier: notificationContent.identifier, content: newContent, trigger: nil)
106108
center.add(request) { error in
107109
if let error = error {
@@ -114,15 +116,18 @@ class NotificationService: UNNotificationServiceExtension {
114116

115117
// Notify when all notifications are added
116118
dispatchGroup.notify(queue: DispatchQueue.main) {
119+
var content = UNMutableNotificationContent()
117120
if let lastNotification = lastNotification {
118-
bestAttemptContent.title = lastNotification.title
119-
bestAttemptContent.body = lastNotification.body
120-
contentHandler(bestAttemptContent)
121-
} else {
122-
// Delay the callback by 1 second so that the notifications can be removed
123-
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
124-
contentHandler(UNNotificationContent())
125-
}
121+
content.title = lastNotification.title
122+
content.body = lastNotification.body
123+
content.sound = UNNotificationSound.default
124+
content.userInfo["customData"] = lastNotification.data
125+
}
126+
// Add the badge number
127+
content.badge = NSNumber(value: batch.badge_count)
128+
// Delay the callback by 1 second so that the notifications can be removed
129+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
130+
contentHandler(content)
126131
}
127132
}
128133
}

prototype/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ SPEC CHECKSUMS:
4242
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
4343
phnxapplogic: 3a80c8eb90a59223e9add14dde8a0447b069b0b3
4444

45-
PODFILE CHECKSUM: 0125abbd476297c108cff54855f8852db903c646
45+
PODFILE CHECKSUM: 1220b9aeaff430464a6e8d836cc72e2101e24e90
4646

4747
COCOAPODS: 1.15.2

prototype/ios/Runner/AppDelegate.swift

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import UIKit
44
@UIApplicationMain
55
@objc class AppDelegate: FlutterAppDelegate {
66
private var deviceToken: String?
7+
private let notificationChannelName: String = "im.phnx.prototype/channel"
78

89
override func application(
910
_ application: UIApplication,
@@ -20,15 +21,11 @@ import UIKit
2021

2122
// Set up the method channel to retrieve the token from Flutter
2223
let controller = window?.rootViewController as! FlutterViewController
23-
let deviceTokenChannel = FlutterMethodChannel(name: "im.phnx.prototype/channel",
24+
let methodChannel = FlutterMethodChannel(name: notificationChannelName,
2425
binaryMessenger: controller.binaryMessenger)
25-
deviceTokenChannel.setMethodCallHandler { [weak self] (call, result) in
26-
if call.method == "devicetoken" {
27-
self?.getDeviceToken(result: result)
28-
} else {
29-
result(FlutterMethodNotImplemented)
30-
}
31-
}
26+
27+
// Set the handler function for the method channel
28+
methodChannel.setMethodCallHandler(handleMethodCall)
3229

3330
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
3431
}
@@ -43,9 +40,47 @@ import UIKit
4340
}
4441

4542
override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
46-
print("Failed to register: \(error)")
43+
NSLog("Failed to register: \(error)")
4744
}
4845

46+
// This method will be called when app received push notifications in foreground
47+
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
48+
NSLog("Foreground notification received")
49+
let userInfo = notification.request.content.userInfo
50+
if let customData = userInfo["customData"] as? String {
51+
notifyFlutter(customData: customData, method: "receivedNotification")
52+
}
53+
completionHandler([.alert, .sound])
54+
}
55+
56+
// This method will be called when the user taps on the notification
57+
override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
58+
NSLog("User opened notification")
59+
let userInfo = response.notification.request.content.userInfo
60+
if let customData = userInfo["customData"] as? String {
61+
notifyFlutter(customData: customData, method: "openedNotification")
62+
}
63+
completionHandler()
64+
}
65+
66+
private func notifyFlutter(customData: String, method: String) {
67+
let controller = window?.rootViewController as! FlutterViewController
68+
let channel = FlutterMethodChannel(name: notificationChannelName, binaryMessenger: controller.binaryMessenger)
69+
let arguments: [String: String] = ["customData": customData]
70+
channel.invokeMethod(method, arguments: arguments)
71+
}
72+
73+
// Define the handler function
74+
private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
75+
if call.method == "devicetoken" {
76+
self.getDeviceToken(result: result)
77+
} else {
78+
NSLog("Unknown method called: \(call.method)")
79+
result(FlutterMethodNotImplemented)
80+
}
81+
}
82+
83+
// Get device token
4984
private func getDeviceToken(result: FlutterResult) {
5085
if let token = deviceToken {
5186
result(token)

prototype/lib/homescreen.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
88
import 'package:permission_handler/permission_handler.dart';
99
import 'package:prototype/core_client.dart';
1010
import 'package:prototype/messenger_view.dart';
11+
import 'package:prototype/platform.dart';
1112
import 'package:prototype/registration/server_choice.dart';
1213
import 'package:prototype/settings/developer.dart';
1314
import 'package:prototype/styles.dart';
@@ -34,10 +35,12 @@ class _HomeScreenState extends State<HomeScreen> {
3435
statusText = "Initializing core client...";
3536
});
3637

37-
await coreClient.init();
38-
39-
// Ask for notification permission on iOS
38+
// iOS specific initialization
4039
if (Platform.isIOS) {
40+
// Initialize the method channel
41+
initMethodChannel();
42+
43+
// Ask for notification permission
4144
var status = await Permission.notification.status;
4245
switch (status) {
4346
case PermissionStatus.denied:

prototype/lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
// SPDX-License-Identifier: AGPL-3.0-or-later
44

55
import 'package:flutter/material.dart';
6+
import 'package:prototype/core_client.dart';
67
import 'package:prototype/homescreen.dart';
78
import 'package:prototype/styles.dart';
89

910
void main() async {
11+
// Initialize the FRB
12+
await coreClient.init();
13+
1014
runApp(const MyApp());
1115
}
1216

prototype/lib/platform.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ import 'package:flutter/services.dart';
77

88
const platform = MethodChannel('im.phnx.prototype/channel');
99

10+
void initMethodChannel() {
11+
platform.setMethodCallHandler(_handleMethod);
12+
}
13+
14+
Future<void> _handleMethod(MethodCall call) async {
15+
switch (call.method) {
16+
case 'receivedNotification':
17+
// Handle notification data
18+
final String data = call.arguments["customData"];
19+
print('Notification data: $data');
20+
// Do something with the data
21+
break;
22+
case 'openedNotification':
23+
// Handle notification opened
24+
final String data = call.arguments["customData"];
25+
print('Notification opened: $data');
26+
// Do something with the data
27+
break;
28+
default:
29+
print('Unknown method called: ${call.method}');
30+
}
31+
}
32+
1033
Future<String?> getDeviceToken() async {
1134
// Make sure we are on iOS
1235
if (!Platform.isIOS) {

0 commit comments

Comments
 (0)