import type { PushNotificationAction, PushNotificationPayload, } from "../types"; import { mapSirrusPushDataPayload } from "../campaigns"; import type { NotificationPresenter } from "../types"; import type { NotificationProcessingResult } from "../types"; import type { SirrusReactNativeSDK } from "../SirrusReactNativeSDK"; interface FirebaseNotificationLike { title?: string; body?: string; imageUrl?: string; ios?: { imageUrl?: string; }; android?: { imageUrl?: string; }; } export interface FirebaseRemoteMessageLike { messageId?: string; notification?: FirebaseNotificationLike; data?: Record; } export interface FirebaseMessagingLike { onMessage( listener: (message: FirebaseRemoteMessageLike) => void | Promise ): () => void; onNotificationOpenedApp( listener: (message: FirebaseRemoteMessageLike) => void | Promise ): () => void; getInitialNotification(): Promise; } export interface FirebaseMessagingBridgeOptions { displayForegroundNotifications?: boolean; notificationPresenter?: NotificationPresenter; onUnhandledNotificationReceived?: ( message: FirebaseRemoteMessageLike, result: NotificationProcessingResult ) => void | Promise; onUnhandledNotificationOpened?: ( message: FirebaseRemoteMessageLike, result: NotificationProcessingResult ) => void | Promise; mapDataToPayload?: ( message: FirebaseRemoteMessageLike ) => Partial; } export interface FirebaseMessagingBridge { processInitialNotification(): Promise; unsubscribe(): void; } const toJsonObject = ( data?: Record ): Record | undefined => { if (!data) { return undefined; } return Object.fromEntries( Object.entries(data).filter((entry): entry is [string, string] => { return typeof entry[1] === "string"; }) ); }; const pickString = ( ...values: Array ): string | undefined => { for (const value of values) { if (typeof value === "string" && value.trim().length > 0) { return value; } } return undefined; }; const parseBoolean = (value: unknown): boolean | undefined => { if (typeof value === "boolean") { return value; } if (typeof value !== "string") { return undefined; } const normalized = value.trim().toLowerCase(); if (normalized === "true" || normalized === "1") { return true; } if (normalized === "false" || normalized === "0") { return false; } return undefined; }; const parseJsonValue = (value?: string): unknown => { if (typeof value !== "string" || value.trim().length === 0) { return undefined; } try { return JSON.parse(value); } catch { return undefined; } }; const mapPushAction = (value: unknown, index = 0): PushNotificationAction | null => { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } const action = value as Record; const id = pickString( action.id as string | undefined, action.actionId as string | undefined, action.action_id as string | undefined, action.identifier as string | undefined, action.postback as string | undefined, action.actionType as string | undefined ); const title = pickString( action.title as string | undefined, action.label as string | undefined, action.name as string | undefined, action.text as string | undefined ); const label = pickString( action.label as string | undefined, action.title as string | undefined, action.name as string | undefined, action.text as string | undefined ); const fallbackTitle = title ?? "Open"; return { id: id ?? `action_${index + 1}`, title: fallbackTitle, label: label ?? fallbackTitle, actionType: pickString(action.actionType as string | undefined), postback: pickString(action.postback as string | undefined), deeplinkUrl: pickString( action.deeplinkUrl as string | undefined, action.deepLinkUrl as string | undefined, action.deep_link_url as string | undefined ), destructive: parseBoolean(action.destructive), foreground: parseBoolean(action.foreground), authenticationRequired: parseBoolean( action.authenticationRequired ?? action.authentication_required ), }; }; const parseActions = ( data?: Record ): PushNotificationAction[] | undefined => { const fallbackActionId = pickString( data?.actionId, data?.action_id, data?.postback, data?.actionType ); const fallbackActionTitle = pickString( data?.actionTitle, data?.action_label, data?.actionLabel ); const fallbackActionDeeplinkUrl = pickString( data?.actionDeeplinkUrl, data?.actionDeepLinkUrl ); const parsed = parseJsonValue( pickString( data?.sirrusActions, data?.sirrus_actions, data?.actions ) ); if (!Array.isArray(parsed)) { if (!fallbackActionId && !fallbackActionTitle && !fallbackActionDeeplinkUrl) { return undefined; } const fallbackAction = mapPushAction({ id: fallbackActionId, title: fallbackActionTitle, label: pickString(data?.action_label, data?.actionLabel, data?.actionTitle), actionType: data?.actionType, postback: data?.postback, deeplinkUrl: pickString( fallbackActionDeeplinkUrl, data?.deeplinkUrl, data?.deepLinkUrl, data?.deepLinkUri, data?.sirrus_deeplink_url ), foreground: data?.actionForeground ?? "true", }); return fallbackAction ? [fallbackAction] : undefined; } const actions = parsed .map((value, index) => mapPushAction(value, index)) .filter((value): value is PushNotificationAction => value !== null); return actions.length > 0 ? actions : undefined; }; const defaultMapDataToPayload = ( message: FirebaseRemoteMessageLike ): Partial => { const data = message.data; const sirrusPayload = mapSirrusPushDataPayload(data ?? {}); return { ...sirrusPayload, title: pickString( sirrusPayload.title, message.notification?.title, data?.title, data?.sirrus_title ), body: pickString( sirrusPayload.body, message.notification?.body, data?.body, data?.sirrus_body ), subtitle: pickString( sirrusPayload.subtitle, data?.subtitle, data?.sirrus_subtitle ), imageUrl: pickString( sirrusPayload.imageUrl, message.notification?.imageUrl, message.notification?.ios?.imageUrl, message.notification?.android?.imageUrl, data?.imageUrl, data?.image_url, data?.sirrusImageUrl, data?.sirrus_image_url, data?.mediaUrl, data?.media_url ), gifUrl: pickString( sirrusPayload.gifUrl, data?.gifUrl, data?.gif_url, data?.sirrusGifUrl, data?.sirrus_gif_url ), videoUrl: pickString( sirrusPayload.videoUrl, data?.videoUrl, data?.video_url, data?.sirrusVideoUrl, data?.sirrus_video_url ), deeplinkUrl: pickString( sirrusPayload.deeplinkUrl, data?.deeplinkUrl, data?.deepLinkUrl, data?.deepLinkUri, data?.deep_link_url, data?.deep_link_uri, data?.sirrusDeeplinkUrl, data?.sirrus_deeplink_url, data?.click_action, data?.link ), campaignId: pickString( sirrusPayload.campaignId, data?.sirrusCampaignId, data?.sirrus_campaign_id, data?.campaignId, data?.campaign_id ), templateId: pickString( sirrusPayload.templateId, data?.sirrusTemplateId, data?.sirrus_template_id, data?.templateId, data?.template_id ), messageId: pickString( sirrusPayload.messageId, message.messageId, data?.messageId, data?.message_id, data?.sirrusMessageId, data?.sirrus_message_id, data?.["gcm.message_id"], data?.["google.message_id"] ), provider: "fcm", channelId: pickString( sirrusPayload.channelId, data?.channelId, data?.channel_id, data?.sirrusChannelId, data?.sirrus_channel_id, data?.android_channel_id ), categoryId: pickString( sirrusPayload.categoryId, data?.categoryId, data?.category_id, data?.sirrusCategoryId, data?.sirrus_category_id ), sound: pickString( sirrusPayload.sound, data?.sound, data?.sirrusSound, data?.sirrus_sound ), actions: sirrusPayload.actions ?? parseActions(data), data: toJsonObject(data), rawPayload: toJsonObject(data), }; }; export const mapFirebaseRemoteMessageToPushPayload = ( message: FirebaseRemoteMessageLike, mapDataToPayload: FirebaseMessagingBridgeOptions["mapDataToPayload"] = defaultMapDataToPayload ): PushNotificationPayload => { const mappedPayload = mapDataToPayload(message); return { title: message.notification?.title, body: message.notification?.body, provider: "fcm", messageId: message.messageId, data: toJsonObject(message.data), rawPayload: toJsonObject(message.data), ...mappedPayload, }; }; export const bindFirebaseMessaging = ( sdk: SirrusReactNativeSDK, messaging: FirebaseMessagingLike, options: FirebaseMessagingBridgeOptions = {} ): FirebaseMessagingBridge => { const { displayForegroundNotifications = false, notificationPresenter, onUnhandledNotificationReceived, onUnhandledNotificationOpened, mapDataToPayload = defaultMapDataToPayload, } = options; const handleReceived = async (message: FirebaseRemoteMessageLike): Promise => { const payload = mapFirebaseRemoteMessageToPushPayload(message, mapDataToPayload); const result = await sdk.processNotificationReceived(payload); if (!result.handled && onUnhandledNotificationReceived) { await onUnhandledNotificationReceived(message, result); return; } if (!result.handled && displayForegroundNotifications && notificationPresenter) { await notificationPresenter.show({ payload, source: "foreground_receive", }); } }; const handleOpened = async (message: FirebaseRemoteMessageLike): Promise => { const payload = mapFirebaseRemoteMessageToPushPayload(message, mapDataToPayload); const result = await sdk.processNotificationOpened(payload); if (!result.handled && onUnhandledNotificationOpened) { await onUnhandledNotificationOpened(message, result); } }; const messageUnsubscribe = messaging.onMessage((message) => { void handleReceived(message); }); const openUnsubscribe = messaging.onNotificationOpenedApp((message) => { void handleOpened(message); }); const processInitialNotification = async (): Promise => { const message = await messaging.getInitialNotification(); if (!message) { return; } await handleOpened(message); }; return { processInitialNotification, unsubscribe: () => { messageUnsubscribe(); openUnsubscribe(); }, }; };