Firebase Cloud Messaging
Đăng ký/Huỷ đăng ký FCM
- RN CLI
- Expo
import {
usePSChatApiClientContext,
PSChatNotificationDeviceType,
PSChatNotificationRequestDto,
PSResponseError
} from '@communi/chat-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import messaging from '@react-native-firebase/messaging';
const chatApiClient = usePSChatApiClientContext();
import {
usePSChatApiClientContext,
PSChatNotificationDeviceType,
PSChatNotificationRequestDto,
PSResponseError
} from '@piscale/chat-expo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import messaging from '@react-native-firebase/messaging';
const chatApiClient = usePSChatApiClientContext();
- Đăng ký
- Huỷ đăng ký
- createNotificationRequestBody
const registerFcmToken = async (token: string) => {
if (chatApiClient) {
try {
const body = await createNotificationRequestBody(token);
if (body) {
await chatApiClient.notificationApi.register(body);
await AsyncStorage.setItem("FCM_TOKEN", token);
}
} catch (error) {
console.log('registerFcmToken', error);
if (error && error instanceof PSResponseError) {
const messageCode = error.response?.data.message_code;
if (messageCode === 'M4016_DEVICE_TOKEN_INVALID') {
await messaging().deleteToken();
}
}
}
}
};
Cần unregister token FCM để không nhận được noti nữa (VD: khi logout, ...)
const unregisterFcmToken = async () => {
if (chatApiClient) {
try {
const token = await AsyncStorage.getItem("FCM_TOKEN");
if (token) {
const body = await createNotificationRequestBody(token);
if (body) {
await chatApiClient.notificationApi.unregister(body);
await AsyncStorage.removeItem("FCM_TOKEN");
}
}
await messaging().deleteToken();
} catch (error) {
console.log('unregisterFcmToken', error);
}
}
};
import {AuthStorageKey} from '../contexts';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {Platform} from 'react-native';
import {
getVersion,
getBuildNumber,
getDeviceName,
getSystemVersion,
} from 'react-native-device-info';
export const createNotificationRequestBody = async (token: string) => {
try {
const type = Platform.select({
android: PSChatNotificationDeviceType.ANDROID,
ios: PSChatNotificationDeviceType.IOS,
});
const deviceId = await AsyncStorage.getItem(AuthStorageKey.DEVICE_ID);
if (type && deviceId) {
return {
app_version: `${getVersion()}-${getBuildNumber()}`,
device_id: deviceId,
device_name: await getDeviceName(),
sdk_version: getSystemVersion(),
token: token,
type: type,
} as PSChatNotificationRequestDto;
} else {
return undefined;
}
} catch (error) {
return undefined;
}
};
Nếu với 1 appId của Communi bạn dùng cho nhiều dự án và ở những project Firebase khác nhau thì khi Đăng ký/ Huỷ đăng ký cần sử dụng:
await chatApiClient.notificationApi.registerWithFireBaseProjectNumber(body, fireBaseProjectNumber);
await chatApiClient.notificationApi.unregisterWithFireBaseProjectNumber(body, fireBaseProjectNumber);
Với fireBaseProjectNumber
lấy tại project Firebase của bạn vào Project Overview
-> Project settings
-> General
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 messaging().setBackgroundMessageHandler
để 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
const unsubscribeOnNotificationOpen = messaging().onNotificationOpenedApp(
remoteMessage => {
console.log(
`iOS onNotificationOpenedApp = ${JSON.stringify(remoteMessage)}`,
);
// Notification caused app to open from background state on iOS
const threadId = remoteMessage?.data?.ps_thread_id as
| string
| undefined;
const messageId = remoteMessage?.data?.ps_message_id as
| number
| undefined;
console.log(
`onNotificationOpenedApp: threadId = ${threadId}, messageId = ${messageId}`,
);
if (threadId) {
navigateToThreadDetails({
targetThreadId: threadId,
targetMessageId: messageId,
});
}
},
);
// Notification caused app to open from quit state on iOS
messaging()
.getInitialNotification()
.then(remoteMessage => {
console.log(
`messaging.getInitialNotification = ${JSON.stringify(remoteMessage)}`,
);
if (remoteMessage) {
console.log(
`getInitialNotification = ${JSON.stringify(remoteMessage)}`,
);
const threadId = remoteMessage.data?.ps_thread_id as
| string
| undefined;
const messageId = remoteMessage.data?.ps_message_id as
| number
| undefined;
console.log(
`messaging.getInitialNotification: threadId = ${threadId}, messageId = ${messageId}`,
);
if (threadId) {
navigateToThreadDetails({
targetThreadId: threadId,
targetMessageId: messageId,
});
}
}
});
Và nếu bạn sử dụng Notifee thì xử lý nhấn vào thông báo như sau:
import notifee, {EventType} from '@notifee/react-native';
// Handle notification clicks on foreground
const unsubscribeForegroundEvent = notifee.onForegroundEvent(
({detail, type}) => {
console.log(
`onForegroundEvent = ${JSON.stringify(
detail,
)}, type = ${JSON.stringify(type)}`,
);
if (type === EventType.PRESS || type === EventType.ACTION_PRESS) {
const threadId = (detail.notification?.data?.ps_thread_id ??
detail.notification?.ios?.communicationInfo?.conversationId) as
| string
| undefined;
const messageId = (detail.notification?.data?.ps_message_id ??
// @ts-ignore
detail.notification?.ios?.communicationInfo?.messageId) as
| number
| undefined;
console.log(
`onForegroundEvent: threadId = ${threadId}, messageId = ${messageId}`,
);
if (threadId) {
navigateToThreadDetails({
targetThreadId: threadId,
targetMessageId: messageId,
});
}
}
},
);
// Notification caused app to open from quit state on Android
notifee.getInitialNotification().then(initialNotification => {
console.log(
`notifee.getInitialNotification = ${JSON.stringify(
initialNotification,
)}`,
);
if (initialNotification) {
console.log(
`getInitialNotification = ${JSON.stringify(initialNotification)}`,
);
const threadId = initialNotification.notification.data?.ps_thread_id as
| string
| undefined;
const messageId = initialNotification.notification?.data
?.ps_message_id as number | undefined;
console.log(
`notifee.getInitialNotification: threadId = ${threadId}, messageId = ${messageId}`,
);
if (threadId) {
navigateToThreadDetails({
targetThreadId: threadId,
targetMessageId: messageId,
});
}
}
});
hàm xử lý navigateToThreadDetails
có thể tham khảo tại Navigation To Thread
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:
Nếu ứng dụng của bạn có sử dụng Notifee thì bạn có thể tham khảo hướng dẫn.
Nếu không sử dụng Notifee
thì cơ bản bạn cũng 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 keyNSUserActivityTypes
và typeArray
. This array should containINSendMessageIntent
to support messaging. - Tạo
Notification Service Extension
và chỉnh sửa trong đây. Ví dụ như sau:
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
}
}