package com.fireworksdk.bridge.reactnative.utils import android.util.Log import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.Event import com.firework.analyticsevents.VideoInfo import com.firework.common.product.Product import com.fireworksdk.bridge.models.* import com.fireworksdk.bridge.models.enums.FWCircleStoryEventName import com.fireworksdk.bridge.models.enums.FWEventName import com.fireworksdk.bridge.models.enums.FWFeedViewEventName import com.fireworksdk.bridge.models.enums.FWSDKInitSubEventName import com.fireworksdk.bridge.models.enums.FWPlayerDeckEventName import com.fireworksdk.bridge.models.enums.FWStoryBlockEventName import com.fireworksdk.bridge.models.enums.FWVideoShoppingEventName import com.fireworksdk.bridge.utils.FWDateUtils import com.fireworksdk.bridge.utils.FWLogUtils import com.fireworksdk.bridge.utils.FWModelUtils import java.util.* /** * View level event dispatched through [EventDispatcher]. * * Since RN 0.83 `getJSModule(RCTEventEmitter)` logs a soft exception under the new architecture and * stops working once the interop layer is disabled, because `RCTEventEmitter` is not a registered * callable module there. On the old architecture the dispatcher still ends up calling * `RCTEventEmitter.receiveEvent(viewTag, eventName, data)`, so behaviour is unchanged. */ private class FWViewEvent( surfaceId: Int, viewTag: Int, private val name: String, private val data: WritableMap?, ) : Event(surfaceId, viewTag) { override fun getEventName(): String = name override fun getEventData(): WritableMap? = data /** * Events must never be coalesced. A single event name carries more than one meaning here, e.g. * `onVideoFeedLoadFinished` is emitted both for success (no payload) and for failure (name and * reason), so the default coalescing would silently drop the earlier of the two whenever they * land in the same frame. */ override fun canCoalesce(): Boolean = false } object FWEventUtils { fun sendInitSuccessEvent(reactContext: ReactContext) { sendEvent(reactContext, FWEventName.SDKInit.rawValue, Arguments.createMap()) } fun sendInitFailedEvent(reactContext: ReactContext, reason: String?) { val eventMap = Arguments.createMap() val contentMap = Arguments.createMap() contentMap.putString(FWSDKInitSubEventName.InitFailed.rawValue, reason?:"unknown") eventMap.putMap("error", contentMap) sendEvent(reactContext, FWEventName.SDKInit.rawValue, eventMap) } fun sendVideoFeedClickEvent(reactContext: ReactContext, info: FWVideoFeedItemDetailsModel) { val eventMap = Arguments.createMap() val contentMap = Arguments.createMap() contentMap.putInt("index", info.index?:0) contentMap.putString("id", info.id) contentMap.putInt("duration", info.duration?:0) contentMap.putString("source", info.source) contentMap.putString("title", info.title) contentMap.putString("playlistGroup", info.playlistGroup) contentMap.putString("playlist", info.playlist) contentMap.putString("channel", info.channel) contentMap.putString("feedId", info.feedId) if (!info.videoType.isNullOrBlank()) { contentMap.putString("videoType", info.videoType) } if (!info.liveStreamStatus.isNullOrBlank()) { contentMap.putString("liveStreamStatus", info.liveStreamStatus) } if (!info.dynamicContentParameters.isNullOrEmpty()) { val dynamicContentParametersMap = Arguments.createMap() for (key in info.dynamicContentParameters.keys) { val value = info.dynamicContentParameters[key] val array = Arguments.createArray() if (!value.isNullOrEmpty()) { for(item in value) { array.pushString(item) } } dynamicContentParametersMap.putArray(key, array) } contentMap.putMap("dynamicContentParameters", dynamicContentParametersMap) } eventMap.putMap("info", contentMap) sendLogMessageEvent(reactContext, "[Android] Send VideoFeedClick event ${info.id} ${FWDateUtils.getDateString(Date())}") sendEvent(reactContext, FWEventName.VideoFeedClick.rawValue, eventMap) } fun sendCustomCTAClickEvent(reactContext: ReactContext, url: String?, info: FWVideoPlaybackDetails?) { val eventMap = Arguments.createMap() eventMap.putString("url", url) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) sendEvent(reactContext, FWEventName.CustomCTAClick.rawValue, eventMap) } fun sendVideoPlaybackEvent(reactContext: ReactContext, eventName: String, info: FWVideoPlaybackDetails?) { val eventMap = Arguments.createMap() eventMap.putString("eventName", eventName) eventMap.putMap("info", convertVideoPlaybackDetails2Map(info)) sendEvent(reactContext, FWEventName.VideoPlayback.rawValue, eventMap) } private fun convertLiveStreamDetails2Map(info: FWLiveStreamEventDetailsModel?): WritableMap { val contentMap = Arguments.createMap() if (!info?.id.isNullOrBlank()) { contentMap.putString("id", info?.id) } if (!info?.feedId.isNullOrBlank()) { contentMap.putString("feedId", info?.feedId) } if (!info?.videoType.isNullOrBlank()) { contentMap.putString("videoType", info?.videoType) } if (!info?.liveStreamStatus.isNullOrBlank()) { contentMap.putString("liveStreamStatus",info?.liveStreamStatus) } if (info?.playbackDetails != null) { contentMap.putMap("video", convertVideoPlaybackDetails2Map(info?.playbackDetails)) } return contentMap } private fun convertCustomCta2Map(info: FWShoppingCustomCta): WritableMap { val contentMap = Arguments.createMap() contentMap.putString("url", info.url) contentMap.putString("titleKey", info.titleKey) contentMap.putString("title", info.title) return contentMap } private fun convertVideoPlaybackDetails2Map(info: FWVideoPlaybackDetails?): WritableMap { val contentMap = Arguments.createMap() if (!info?.id.isNullOrBlank()) { contentMap.putString("videoId", info?.id) } if (info?.duration != null) { contentMap.putDouble("duration", info.duration) } if (!info?.caption.isNullOrBlank()) { contentMap.putString("caption", info?.caption) } if (!info?.badge.isNullOrBlank() && !info?.badge.equals("null")) { contentMap.putString("badge", info?.badge) } // Always emit hashtags, as an empty array when the video has none, so the JS side gets a // stable string[] rather than undefined. if (info != null) { val hashtagArray = Arguments.createArray() info.hashtags?.forEach { hashtagArray.pushString(it) } contentMap.putArray("hashtags", hashtagArray) } if (info?.playerWidth != null || info?.playerHeight != null) { val playerSizeMap = Arguments.createMap() if (info.playerWidth != null) { playerSizeMap.putInt("width", info.playerWidth) } if (info.playerHeight != null) { playerSizeMap.putInt("height", info.playerHeight) } contentMap.putMap("playerSize", playerSizeMap) } if (info?.hasCta != null) { contentMap.putBoolean("hasCTA", info.hasCta) } if (info?.progress != null) { contentMap.putDouble("progress", info.progress) } if (!info?.actionUrl.isNullOrBlank()) { contentMap.putString("ctaActionURL", info?.actionUrl) } if (!info?.ctaActionType.isNullOrBlank()) { contentMap.putString("ctaActionType", info?.ctaActionType) } if (!info?.feedId.isNullOrBlank()) { contentMap.putString("feedId", info?.feedId) } if (!info?.videoType.isNullOrBlank()) { contentMap.putString("videoType", info?.videoType) } if (!info?.liveStreamStatus.isNullOrBlank()) { contentMap.putString("liveStreamStatus", info?.liveStreamStatus) } return contentMap; } /** * Converts a batch of loaded videos into the `videos` payload of the videos-loaded events. * * Only the newly loaded items of a single load request are converted; the caller is not expected * to accumulate pages. */ private fun convertVideoInfoList2Array(videos: List): WritableArray { val videoArray = Arguments.createArray() for (videoInfo in videos) { videoArray.pushMap( convertVideoPlaybackDetails2Map(FWModelUtils.convertVideoInfo2PlaybackDetails(videoInfo)) ) } return videoArray } fun sendShoppingCtaButtonClickEvent( reactContext: ReactContext, productId: String?, unitId: String?, url: String?, info: FWVideoPlaybackDetails?, customCta: FWShoppingCustomCta?, callbackId: Int? ) { val eventMap = Arguments.createMap() eventMap.putString("productId", productId) eventMap.putString("unitId", unitId) eventMap.putString("url", url) eventMap.putInt("callbackId", callbackId?:0) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) customCta?.let { eventMap.putMap("customCTA", convertCustomCta2Map(it)) } sendEvent(reactContext, FWVideoShoppingEventName.CtaButtonClick.rawValue, eventMap) } fun sendCartIconClickEvent(reactContext: ReactContext, info: FWVideoPlaybackDetails?, callbackId: Int?) { val eventMap = Arguments.createMap() eventMap.putInt("callbackId", callbackId?:0) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) sendEvent(reactContext, FWVideoShoppingEventName.ClickCartIcon.rawValue, eventMap) } // Multiple selected units can produce several display items for one product. // Hydration requests are product-level; keep the original native display items intact. internal fun productHydrationIds(products: List?): List = products.orEmpty() .mapNotNull { it.id } .filter { it.isNotBlank() } .distinct() fun sendUpdateProductsDetailsEvent(reactContext: ReactContext, products: List?, info: FWVideoPlaybackDetails?, callbackId: String?) { val eventMap = Arguments.createMap() val productArray = Arguments.createArray() productHydrationIds(products).forEach { productArray.pushString(it) } eventMap.putArray("productIds", productArray) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) eventMap.putString("callbackId", callbackId) sendLogMessageEvent(reactContext, "[Android] Send UpdateProductDetails event callbackId $callbackId ${FWDateUtils.getDateString(Date())}") sendEvent(reactContext, FWVideoShoppingEventName.UpdateProductDetails.rawValue, eventMap) } private fun sendEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } fun sendLogMessageEvent(reactContext: ReactContext, message: String?) { FWLogUtils.d { message } if (Log.DEBUG >= FWLogUtils.logLevel) { val eventMap = Arguments.createMap() eventMap.putString("message", message) sendEvent(reactContext, FWEventName.LogMessage.rawValue, eventMap) } } fun receiveVideoFeedLoadFinishedSuccessEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWFeedViewEventName.VideoFeedLoadFinished.rawValue, null) } fun receiveVideoFeedEmptyEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWFeedViewEventName.VideoFeedEmpty.rawValue, null) } fun receiveVideoFeedLoadFinishedFailedEvent(reactContext: ReactContext, targetTag: Int, name: String?, reason: String?, action: String) { val eventMap = Arguments.createMap() eventMap.putString("name", name ?: "VideoFeedLoadFailed") eventMap.putString("reason", reason ?: "VideoFeedLoadFailed") eventMap.putString("action", action) receiveEvent(reactContext, targetTag, FWFeedViewEventName.VideoFeedLoadFinished.rawValue, eventMap) } fun receiveVideoFeedGetFeedIdEvent(reactContext: ReactContext, targetTag: Int, feedId: String?) { val eventMap = Arguments.createMap() eventMap.putString("feedId", feedId) receiveEvent(reactContext, targetTag, FWFeedViewEventName.VideoFeedGetFeedId.rawValue, eventMap) } /** * Feeds the autosized feed's content height back to JS, which applies it as the component's Yoga * height. React Native does not read a native view's own measurement, so this round trip is the * only way a native view can drive its own height. */ fun receiveVideoFeedContentSizeChangeEvent(reactContext: ReactContext, targetTag: Int, height: Float) { val eventMap = Arguments.createMap() eventMap.putDouble("height", height.toDouble()) receiveEvent(reactContext, targetTag, FWFeedViewEventName.VideoFeedContentSizeChange.rawValue, eventMap) } fun receiveVideoFeedVideosLoadedEvent(reactContext: ReactContext, targetTag: Int, videos: List) { val eventMap = Arguments.createMap() eventMap.putArray("videos", convertVideoInfoList2Array(videos)) receiveEvent(reactContext, targetTag, FWFeedViewEventName.VideoFeedVideosLoaded.rawValue, eventMap) } fun receiveStoryBlockLoadFinishedSuccessEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWStoryBlockEventName.StoryBlockLoadFinished.rawValue, null) } fun receiveStoryBlockEmptyEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWStoryBlockEventName.StoryBlockEmpty.rawValue, null) } fun receiveStoryBlockLoadFinishedFailedEvent(reactContext: ReactContext, targetTag: Int, name: String?, reason: String?) { val eventMap = Arguments.createMap() eventMap.putString("name", name ?: "StoryBlockLoadFailed") eventMap.putString("reason", reason ?: "StoryBlockLoadFailed") receiveEvent(reactContext, targetTag, FWStoryBlockEventName.StoryBlockLoadFinished.rawValue, eventMap) } fun receiveStoryBlockFullScreenStateChangedEvent(reactContext: ReactContext, targetTag: Int, isFullScreen: Boolean) { val eventMap = Arguments.createMap() eventMap.putBoolean("isFullScreen", isFullScreen) receiveEvent(reactContext, targetTag, FWStoryBlockEventName.StoryBlockFullScreenStateChanged.rawValue, eventMap) } fun receiveStoryBlockGetFeedIdEvent(reactContext: ReactContext, targetTag: Int, feedId: String?) { val eventMap = Arguments.createMap() eventMap.putString("feedId", feedId) receiveEvent(reactContext, targetTag, FWStoryBlockEventName.StoryBlockGetFeedId.rawValue, eventMap) } fun receiveCircleStoryLoadFinishedSuccessEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWCircleStoryEventName.CircleStoryLoadFinished.rawValue, null) } fun receiveCircleStoryEmptyEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWCircleStoryEventName.CircleStoryEmpty.rawValue, null) } fun receiveCircleStoryLoadFinishedFailedEvent(reactContext: ReactContext, targetTag: Int, name: String?, reason: String?, action: String) { val eventMap = Arguments.createMap() eventMap.putString("name", name ?: "CircleStoryLoadFailed") eventMap.putString("reason", reason ?: "CircleStoryLoadFailed") eventMap.putString("action", action) receiveEvent(reactContext, targetTag, FWCircleStoryEventName.CircleStoryLoadFinished.rawValue, eventMap) } fun receiveCircleStoryGetFeedIdEvent(reactContext: ReactContext, targetTag: Int, feedId: String?) { val eventMap = Arguments.createMap() eventMap.putString("feedId", feedId) receiveEvent(reactContext, targetTag, FWCircleStoryEventName.CircleStoryGetFeedId.rawValue, eventMap) } fun receivePlayerDeckLoadFinishedSuccessEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWPlayerDeckEventName.PlayerDeckLoadFinished.rawValue, null) } fun receivePlayerDeckEmptyEvent(reactContext: ReactContext, targetTag: Int) { receiveEvent(reactContext, targetTag, FWPlayerDeckEventName.PlayerDeckEmpty.rawValue, null) } fun receivePlayerDeckLoadFinishedFailedEvent(reactContext: ReactContext, targetTag: Int, name: String?, reason: String?) { val eventMap = Arguments.createMap() eventMap.putString("name", name ?: "PlayerDeckLoadFailed") eventMap.putString("reason", reason ?: "PlayerDeckLoadFailed") receiveEvent(reactContext, targetTag, FWPlayerDeckEventName.PlayerDeckLoadFinished.rawValue, eventMap) } fun receivePlayerDeckGetFeedIdEvent(reactContext: ReactContext, targetTag: Int, feedId: String?) { val eventMap = Arguments.createMap() eventMap.putString("feedId", feedId) receiveEvent(reactContext, targetTag, FWPlayerDeckEventName.PlayerDeckGetFeedId.rawValue, eventMap) } fun receivePlayerDeckVideosLoadedEvent(reactContext: ReactContext, targetTag: Int, videos: List) { val eventMap = Arguments.createMap() eventMap.putArray("videos", convertVideoInfoList2Array(videos)) receiveEvent(reactContext, targetTag, FWPlayerDeckEventName.PlayerDeckVideosLoaded.rawValue, eventMap) } private fun receiveEvent(reactContext: ReactContext, targetTag: Int, eventName: String, event: WritableMap?) { val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, targetTag) if (eventDispatcher == null) { FWLogUtils.w { "[Android] No EventDispatcher for tag $targetTag, dropping event $eventName" } return } eventDispatcher.dispatchEvent( FWViewEvent(UIManagerHelper.getSurfaceId(reactContext), targetTag, eventName, event) ) } // live stream fun sendLiveStreamEvent(reactContext: ReactContext, eventName: String, info: FWLiveStreamEventDetailsModel?) { val eventMap = Arguments.createMap() eventMap.putString("eventName", eventName) eventMap.putMap("info", convertLiveStreamDetails2Map(info)) sendEvent(reactContext, FWEventName.LiveStream.rawValue, eventMap) } fun sendLiveStreamChatEvent(reactContext: ReactContext, eventName: String, message: FWLiveStreamMessageDetailsModel?, liveStream: FWLiveStreamEventDetailsModel?) { val eventMap = Arguments.createMap() eventMap.putString("eventName", eventName) // chat map val chatMap = Arguments.createMap() if (!message?.messageId.isNullOrBlank()) { chatMap.putString("messageId", message?.messageId) } if (!message?.username.isNullOrBlank()) { chatMap.putString("username", message?.username) } if (!message?.text.isNullOrBlank()) { chatMap.putString("text", message?.text) } eventMap.putMap("message", chatMap) // live stream map eventMap.putMap("liveStream", convertLiveStreamDetails2Map(liveStream)) sendEvent(reactContext, FWEventName.LiveStreamChat.rawValue, eventMap) } fun sendLiveStreamLinkInteractionClickEvent(reactContext: ReactContext, title: String?, url: String?, info: FWLiveStreamEventDetailsModel?) { val eventMap = Arguments.createMap() eventMap.putString("title", title) eventMap.putString("url", url) eventMap.putMap("info", convertLiveStreamDetails2Map(info)) sendEvent(reactContext, FWEventName.LiveStreamLinkInteractionClick.rawValue, eventMap) } fun sendProductCardClickEvent(reactContext: ReactContext, url: String?, productId: String?, unitId: String?, info: FWVideoPlaybackDetails?, callbackId: Int?) { val eventMap = Arguments.createMap() eventMap.putString("url", url ?: "") eventMap.putString("productId", productId) eventMap.putString("unitId", unitId) eventMap.putInt("callbackId", callbackId?:0) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) sendEvent(reactContext, FWVideoShoppingEventName.CustomProductCardTap.rawValue, eventMap) } fun sendProductClickEvent(reactContext: ReactContext, url: String?, productId: String?, unitId: String?, info: FWVideoPlaybackDetails?) { val eventMap = Arguments.createMap() eventMap.putString("url", url?:"") eventMap.putString("productId", productId) eventMap.putString("unitId", unitId) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) sendEvent(reactContext, FWVideoShoppingEventName.ProductClick.rawValue, eventMap) } fun sendCustomLinkButtonClickEvent(reactContext: ReactContext, url: String?, productId: String?, unitId: String?, info: FWVideoPlaybackDetails?, callbackId: Int?) { val eventMap = Arguments.createMap() eventMap.putString("url", url ?: "") eventMap.putString("productId", productId) eventMap.putString("unitId", unitId) eventMap.putInt("callbackId", callbackId?:0) eventMap.putMap("video", convertVideoPlaybackDetails2Map(info)) sendEvent(reactContext, FWVideoShoppingEventName.CustomLinkButtonClick.rawValue, eventMap) } }