Skip to main content

Firebase Cloud Messaging

Đăng ký/Huỷ đăng ký FCM

Hãy thêm PSFirebaseMessagingProxy vào phía dưới PSChat như sau:

import 'package:piscale_chat_flutter/piscale_chat_flutter.dart';

class App extends StatelessWidget {
const App({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
builder: (context, child) {
return PSChat(
userId: 'USER_ID',
deviceId: 'DEVICE_ID',
apiClientOptions: PSChatApiClientOptions(
appId: 'APP_ID',
fetchToken: fetchToken,
),
child: PSFirebaseMessagingProxy(child: child),
);
},
);
}
}

Hiển thị thông báo

Payload FCM sẽ có cấu trúc như sau:

{
"Tokens": [
"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..."
],
"Data": {
"avatar_url": "https://thumb-i-1.piscale.com/64x64/smart/100001/c1cb0279-8286-40b8-9082-66f8593316c2/IMG_6546.jpeg",
"is_rtf": "false",
"ps_message_id": "537",
"ps_thread_id": "37791233165968",
"sender_id": "6872016393687471",
"sender_name": "Nghĩa",
"text": "",
"title": "🎧 Zalo - Tran Nghia"
},
"Notification": {
"title": "🎧 Zalo - Tran Nghia",
"body": "Nghĩa: Bạn nhận được video"
},
"Android": {
"collapse_key": "37791233165968",
"priority": "high"
},
"Webpush": null,
"APNS": {
"payload": {
"aps": {
"category": "communications",
"content-available": 1,
"mutable-content": 1
},
"notifee_options": {
"ios": {
"communicationInfo": {
"conversationId": "37791233165968",
"messageId": "537",
"sender": {
"avatar": "https://thumb-i-1.piscale.com/64x64/smart/100001/c1cb0279-8286-40b8-9082-66f8593316c2/IMG_6546.jpeg",
"displayName": "🎧 Zalo - Tran Nghia",
"id": "6872016393687471"
}
}
}
}
}
},
"FCMOptions": {}
}

Do trong payload có khối notification nên hệ thống sẽ tự động hiển thị thông báo trên thiết bị của người dùng theo mặc định. Nếu ứng dụng của bạn có implement hàm FirebaseMessaging.onBackgroundMessage() để show thông báo local thì cần chú ý ignore với payload của chat.

Khi nhấn vào thông báo

  void _handleMessage(RemoteMessage message) {
final data = message.data;
final String threadId = data['ps_thread_id'] ?? '';
final String messageId = data['ps_message_id'] ?? '';
if (threadId.isNotEmpty) {
final context = rootNavigatorKey.currentContext;
if (context != null) {
try {
Utils.popUntilPath(context, AppPage.threads.toPath);
context.push(
AppPage.messages.toPath,
extra: MessagesPageExtra(
targetThreadId: threadId,
targetMessageId:
messageId.isEmpty ? null : int.parse(messageId),
),
);
} catch (e) {
logger.e("KKK: pushReplacement: ", error: e);
}
} else {
logger.e("KKK: rootNavigatorKey.currentContext null");
}
}
}

Communication Notifications

Trên ios có hỗ trợ tính năng Communication Notifications. Thông báo kiểu dạng đó sẽ có dạng như sau:

Để hiển thị thông báo dạng này bạn có thể tham khảo hướng dẫn.

Về cơ bản bạn phải làm những bước như sau:

  • Mở project ios (.xcworkspace) bằng xcode
  • Thêm capability Communication Notifications
  • Updated App's Info.plist với key NSUserActivityTypes và type Array. This array should contain INSendMessageIntent to support messaging.
  • Tạo Notification Service Extension và chỉnh sửa trong đây. Ví dụ như sau:
NotificationService.swift
import UserNotifications
import Intents

class NotificationService: UNNotificationServiceExtension {

var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?

/// This is called when a notification is received.
/// - Parameters:
/// - request: The notification request.
/// - contentHandler: The callback that needs to be called when the notification is ready to be displayed.
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler


// the OS already did some work for us so we do make a work copy. If something does not go the way we expect or we run in a timeout we still have an attempt that can be displayed.
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

// unwrapping makes the compiler happy
if(bestAttemptContent == nil) {
return;
}

// this is the FCM / APNS payload defined by the server / caller.
// Its the custom '"data"' object you provide in the FCM json.
let payload: [AnyHashable : Any] = bestAttemptContent!.userInfo

let psThreadId: String? = payload["ps_thread_id"] as? String

// this is set by the server to indicate that this is a chat message
if(psThreadId != nil) {
_handleChatMessage(payload: payload)

return;
}

// if we do not know the type we just pass the notification through
// this is the case when we get a plain FCM / APNS notification
if let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}

/// Handles a chat message notification. It tries to display it as a communication notification.
/// - Parameter payload: The FCM / APNS payload, defined by the server / caller.
func _handleChatMessage(payload: [AnyHashable : Any]) {
guard let content = bestAttemptContent else {
return
}

guard let contentHandler = contentHandler else {
return
}

let chatRoomName: String = payload["ps_thread_id"] as! String

let senderId: String = payload["sender_id"] as! String

let senderDisplayName: String = payload["title"] as! String

let senderThumbnail: String? = payload["avatar_url"] as? String
guard let senderThumbnail: String = senderThumbnail else {
return
}

var senderAvatar: INImage? = nil
if !senderThumbnail.isEmpty {
guard let senderThumbnailUrl: URL = URL(string: senderThumbnail) else {
return
}

let senderThumbnailFileName: String = senderThumbnailUrl.lastPathComponent

guard let senderThumbnailImageData: Data = try? Data(contentsOf: senderThumbnailUrl),
let senderThumbnailImageFileUrl: URL = downloadAttachment(data: senderThumbnailImageData, fileName: senderThumbnailFileName),
let senderThumbnailImageFileData: Data = try? Data(contentsOf: senderThumbnailImageFileUrl) else {

return
}

senderAvatar = INImage(imageData: senderThumbnailImageFileData)
}

// profile picture that will be displayed in the notification (left side)
// let senderAvatar: INImage = INImage(imageData: senderThumbnailImageFileData)

var personNameComponents = PersonNameComponents()
personNameComponents.nickname = senderDisplayName

// the person that sent the message
// we need that as it is used by the OS trying to identify/match the sender with a contact
// Setting ".unknown" as type will prevent the OS from trying to match the sender with a contact
// as here this is an internal identifier and not a phone number or email
let senderPerson = INPerson(
personHandle: INPersonHandle(
value: senderId,
type: .unknown
),
nameComponents: personNameComponents,
displayName: senderDisplayName,
image: senderAvatar,
contactIdentifier: nil,
customIdentifier: nil,
isMe: false, // this makes the OS recognize this as a sender
suggestionType: .none
)

// this is just a dummy person that will be used as the recipient
let selfPerson = INPerson(
personHandle: INPersonHandle(
value: "00000000-0000-0000-0000-000000000000", // no need to set a real value here
type: .unknown
),
nameComponents: nil,
displayName: nil,
image: nil,
contactIdentifier: nil,
customIdentifier: nil,
isMe: true, // this makes the OS recognize this as "US"
suggestionType: .none
)

// the actual message. We use the OS to send us ourselves a message.
let incomingMessagingIntent = INSendMessageIntent(
recipients: [selfPerson],
outgoingMessageType: .outgoingMessageText, // This marks the message as outgoing
content: content.body, // this will replace the content.body
speakableGroupName: nil,
conversationIdentifier: chatRoomName, // this will be used as the conversation title
serviceName: nil,
sender: senderPerson, // this marks the message sender as the person we defined above
attachments: []
)

if !senderThumbnail.isEmpty {
incomingMessagingIntent.setImage(senderAvatar, forParameterNamed: \.sender)
}

let interaction = INInteraction(intent: incomingMessagingIntent, response: nil)

interaction.direction = .incoming

do {
// we now update / patch / convert our attempt to a communication notification.
bestAttemptContent = try content.updating(from: incomingMessagingIntent) as? UNMutableNotificationContent

// everything went alright, we are ready to display our notification.
contentHandler(bestAttemptContent!)
} catch let error {
print("error \(error)")
}
}

/// Called just before the extension will be terminated by the system.
/// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}

/// Shorthand for creating a notification attachment.
/// - Parameters:
/// - identifier: Unique identifier for the attachment. So it can be referenced within a Notification Content extension for example.
/// - fileName: The name of the file. This is the name that will be used to store the name on disk.
/// - data: A Data object based on the remote url.
/// - options: A dictionary of options. See Apple's documentation for more information.
/// - Returns: A UNNotificationAttachment object.
func createNotificationAttachment(identifier: String, fileName: String, data: Data, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
do {
if let fileURL: URL = downloadAttachment(data: data, fileName: fileName) {
let attachment: UNNotificationAttachment = try UNNotificationAttachment.init(identifier: identifier, url: fileURL, options: options)

return attachment
}

return nil
} catch let error {
print("error \(error)")
}

return nil
}

/// Downloads a file from a remote url and stores it in a temporary folder.
/// - Parameters:
/// - data: A Data object based on the remote url.
/// - fileName: The name of the file. This is the name that will be used to store the name on disk.
/// - Returns: A URL object pointing to the temporary file on the phone. This can be used by a Notification Content extension for example.
func downloadAttachment(data: Data, fileName: String) -> URL? {
// Create a temporary file URL to write the file data to
let fileManager = FileManager.default
let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)

do {
// prepare temp subfolder
try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil)
let fileURL: URL = tmpSubFolderURL.appendingPathComponent(fileName)

// Save the image data to the local file URL
try data.write(to: fileURL)

return fileURL
} catch let error {
print("error \(error)")
}

return nil
}

}