Reputation: 549
I am trying to add a chat ability in my app. I wanted to have all the messages and chats locally saved on the Android/iOS device where the app can load from when being opened. The idea is that I will need my Laravel app to notify other devices if a new message is sent, or deleted, or a user being removed/added to a group and so on. I tried using silent notifications (data messages).
On Android, because there is no much throttling or battery optimizations, every data message is being delivered successfully but on iOS, messages are being throttled due to battery optimizations and the OS can't let my app run in the background more than few times before a cooldown is being put on the app, so updating the local data on iOS cannot depend on data messages at all. I need a robust way of communicating between Laravel and Flutter.
I tried asking on apple developers forums and got this answer from one of apple's engineers:
Background notifications to wake up your app to run some code will be throttled. I suggest looking into using a Notification Service Extension as discussed at https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension. The NotificationService Extension will be executed for every visible push notification. So, it could serve your needs, as long as the user has not disabled the visibility of your notifications through various settings. The service extension will not be executed for push notifications that will not be presented visually. Although the designed purpose of this expansion is to modify the incoming messages, so whether it will fit your use case or not, you will have to decide yourself. The extension will be limited to 30 seconds and 24 MB total memory. You cannot directly communicate with your main app except via a shared container you can write and read. And you cannot send silent notifications.
this gave me an idea of sending normal notifications with unique ids to avoid notifications collapsing and ensure delivery reliability. The other problem is to save the data locally where I can use shared prefrences to save data and then when the app starts, I will convert these data and add them into my local database. I am not familar with swift so I had chatgpt for help in the code but didn't know why is it not working.
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
print("NSE: didReceive triggered")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else {
print("NSE: bestAttemptContent is nil")
contentHandler(request.content)
return
}
print("NSE: Notification Payload - \(request.content.userInfo)")
if let userInfo = request.content.userInfo as? [String: Any],
let suppressFlag = userInfo["suppress"] as? Bool,
let cleanData = userInfo["data"] as? [String: Any] {
print("NSE: Suppress flag = \(suppressFlag)")
if suppressFlag {
print("NSE: Suppressing notification")
bestAttemptContent.title = ""
bestAttemptContent.body = ""
bestAttemptContent.sound = nil
} else {
print("NSE: Displaying notification")
bestAttemptContent.title = cleanData["title"] as? String ?? "New Message"
bestAttemptContent.body = cleanData["body"] as? String ?? "You have a new message."
}
print("NSE: Saving notification to shared container")
saveNotificationToSharedContainer(data: cleanData)
}
print("NSE: Passing modified content to handler")
contentHandler(bestAttemptContent)
}
override func serviceExtensionTimeWillExpire() {
// Called when time runs out
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
private func saveNotificationToSharedContainer(data: [String: Any]) {
// Access shared container
let sharedDefaults = UserDefaults(suiteName: "group.com.egensolve.demo")
var notifications = sharedDefaults?.array(forKey: "stored_notifications") as? [[String: Any]] ?? []
// Append the new notification data
notifications.append(data)
sharedDefaults?.set(notifications, forKey: "stored_notifications")
sharedDefaults?.synchronize()
}
}
for more clarification, the code was supposed to see if a suppress key is sent with value of true/false. if true, then I wanted to NOT show the notification as it is supposedly of no importance to view it. if false, then show it. ChatGPT also guided me to create an app group and said to add it to both the NSE and Main targets, so I did.
code for sending a notification for an Android/iOS device:
public function sendDataMessage($deviceTokens, $data = [], $priority = "high")
{
$messaging = app('firebase.messaging');
$clean_data = $data;
if (isset($data["type"]) && $data["type"] == "chat" && $data["action"] == "receiveMessage") {
unset($clean_data["fcm_message"]);
}
$messages = [];
foreach ($deviceTokens as $token) {
$aps = [];
try {
if (isset($data["type"]) && $data["type"] == "chat") {
$chat_message = $data["fcm_message"];
$clean_data["title"] = Chats::select("name")->find($data["chat_id"])->name;
if ($data["action"] == "receiveMessage") {
$clean_data["body"] = $chat_message["sender"]["name"] . ": " . ($chat_message["content"]["type"] == "Text" ? $chat_message["content"]["body"] : $chat_message["content"]["type"]);
$aps = [
"alert" => [
"title" => $clean_data["title"],
"body" => $clean_data["body"]
],
"content-available" => 1,
"mutable-content" => 1
];
$clean_data["suppress"] = true;
$aps["sound"] = "default";
} else {
$clean_data["suppress"] = true;
}
}
$messages[] = CloudMessage::withTarget('token', $token)
->withData($clean_data)
->withAndroidConfig(AndroidConfig::fromArray([
'priority' => 'normal', // Use high priority
'ttl' => 86400 * 28, // 4 weeks in seconds
]))
->withApnsConfig(ApnsConfig::fromArray([
'headers' => [
'apns-collapse-id' => uniqid(),
'apns-expiration' => strval(time() + 86400 * 28), // Expiry timestamp
'apns-priority' => '10',
],
'payload' => [
'aps' => $aps,
],
]));
} catch (Exception $e) {
return $e;
}
}
try {
$report = $messaging->sendAll($messages);
} catch (NotFound $e) {
return $e;
} catch (Exception $e) {
return $e;
}
return $report->successes()->count();
}
Reading from shared prefs:
final notifications = prefs?.getStringList('stored_notifications') ?? [];
print(notifications);
Upvotes: 0
Views: 36