import React from 'react'; import { type EmitterSubscription, findNodeHandle, type NativeSyntheticEvent, Platform, type StyleProp, UIManager, type ViewStyle, } from 'react-native'; import FireworkSDK from '../FireworkSDK'; import type AdConfiguration from '../models/AdConfiguration'; import type CircleStoryConfiguration from '../models/CircleStoryConfiguration'; import { FWComponentType } from '../models/FWComponentType'; import type FWError from '../models/FWError'; import { FWEventName } from '../models/FWEventName'; import type { VideoFeedSource } from '../models/VideoFeedSource'; import type VideoPlayerConfiguration from '../models/VideoPlayerConfiguration'; import type VideoPlayerNativeConfiguration from '../models/VideoPlayerNativeConfiguration'; import { FireworkSDKModuleEventEmitter } from '../modules/FireworkSDKModule'; import FWGlobalState from '../utils/FWGlobalState'; import gennerateJsonKey from '../utils/FWJsonUtil'; import FWLoggerUtil from '../utils/FWLoggerUtil'; import FWCircleStory from './FWCircleStory'; export interface ICircleStoryProps { style?: StyleProp; /** * One of available feed sources. * Defaults to discover. */ source: VideoFeedSource; /** * Channel id of the feed. Required when the source is channel, playlist, dynamicContent, hashtagPlaylist, or sku. */ channel?: string; /** * Playlist id of the feed. Please note channel id is necessary. Required when the source is playlist. */ playlist?: string; /** * PlaylistGroup id of the feed. Required when the source is playlistGroup. Only supported on iOS. */ playlistGroup?: string; /** * The dynamic content parameters of the feed. Required when the source is dynamicContent. */ dynamicContentParameters?: { [key: string]: string[] }; /** * Hashtag filter expression used to query feed by hashtags. */ hashtagFilterExpression?: string; /** * Product ids used to generate sku feed. */ productIds?: string[]; /** * The video or live stream id. */ contentId?: string; /** * Specifies if Picture in Picture is enabled. */ enablePictureInPicture?: boolean; /** * Controls whether the system (native iOS) Picture-in-Picture is enabled. * Defaults to true. Only supported on iOS. */ enableSystemPictureInPicture?: boolean; /** * Configuration of circle story. */ circleStoryConfiguration?: CircleStoryConfiguration; /** * Configuration of video player. */ videoPlayerConfiguration?: VideoPlayerConfiguration; /** * Ad configuration of the feed. */ adConfiguration?: AdConfiguration; /** * The feed loading result callback. */ onCircleStoryLoadFinished?: (error?: FWError) => void; /** * Triggered when there are no items in the feed. */ onCircleStoryEmpty?: (error?: FWError) => void; /** * Start PiP callback. Only supported on iOS. */ onCircleStoryDidStartPictureInPicture?: (error?: FWError) => void; /** * Stop PiP callback. Only supported on iOS. */ onCircleStoryDidStopPictureInPicture?: (error?: FWError) => void; /** * The host app could use this callback to get feed id for conversion tracking. */ onCircleStoryGetFeedId?: (feedId: string) => void; } interface ICircleStoryState { sdkInitSucceeded: boolean; } const NativeComponentName = 'FWCircleStory'; class CircleStory extends React.Component< ICircleStoryProps, ICircleStoryState > { private _nativeComponentRef = React.createRef(); private _subscriptions: EmitterSubscription[] = []; private _loaded: boolean = false; constructor(props: ICircleStoryProps) { super(props); this.state = { sdkInitSucceeded: FWGlobalState.getInstance().sdkInitSucceeded, }; } public refresh = () => { FWLoggerUtil.log(`CircleStory refresh ${JSON.stringify(this.props)}`); const nativeNodeHandle = findNodeHandle(this._nativeComponentRef.current); let commandId: string | number = UIManager.getViewManagerConfig(NativeComponentName).Commands.refresh ?? ''; if (Platform.OS === 'android') { commandId = commandId.toString(); } let reactTag: number | null = findNodeHandle(nativeNodeHandle); FWLoggerUtil.log( `CircleStory refresh commandId: ${commandId} nativeNodeHandle: ${nativeNodeHandle} reactTag: ${reactTag}` ); if (!nativeNodeHandle || !reactTag) { return; } UIManager.dispatchViewManagerCommand(reactTag, commandId, []); }; componentDidMount() { FWLoggerUtil.log( `CircleStory componentDidMount ${JSON.stringify(this.props)}` ); if (!this.state.sdkInitSucceeded) { FWGlobalState.getInstance().sdkInitSucceededPromise.then(() => { this.setState({ sdkInitSucceeded: true }); }); } const subscriptionOfShareBaseURLUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.ShareBaseURLUpdated, () => { FWLoggerUtil.log('Receive FWEventName.ShareBaseURLUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfShareBaseURLUpdated); const subscriptionOfAdBadgeConfigurationUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.AdBadgeConfigurationUpdated, () => { FWLoggerUtil.log('Receive FWEventName.AdBadgeConfigurationUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfAdBadgeConfigurationUpdated); const subscriptionOfVideoLaunchBehaviorUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.VideoLaunchBehaviorUpdated, () => { FWLoggerUtil.log('Receive FWEventName.VideoLaunchBehaviorUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfVideoLaunchBehaviorUpdated); const subscriptionOfAppLanguageUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.AppLanguageUpdated, () => { FWLoggerUtil.log('Receive FWEventName.AppLanguageUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfAppLanguageUpdated); const subscriptionOfDataTrackingLevelUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.DataTrackingLevelUpdated, () => { FWLoggerUtil.log('Receive FWEventName.DataTrackingLevelUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfDataTrackingLevelUpdated); const subscriptionOfLivestreamPlayerDesignVersionUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.LivestreamPlayerDesignVersionUpdated, () => { FWLoggerUtil.log( 'Receive FWEventName.LivestreamPlayerDesignVersionUpdated' ); this.setState({}); } ); this._subscriptions.push( subscriptionOfLivestreamPlayerDesignVersionUpdated ); const subscriptionOfShortVideoPlayerDesignVersionUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.ShortVideoPlayerDesignVersionUpdated, () => { FWLoggerUtil.log( 'Receive FWEventName.ShortVideoPlayerDesignVersionUpdated' ); this.setState({}); } ); this._subscriptions.push( subscriptionOfShortVideoPlayerDesignVersionUpdated ); } componentWillUnmount() { FWLoggerUtil.log( `CircleStory componentWillUnmount ${JSON.stringify(this.props)}` ); this._subscriptions.forEach((value) => { value.remove(); }); this._subscriptions = []; } private _generateDynamicContentParametersString(): string { const { dynamicContentParameters } = this.props; if (!dynamicContentParameters) { return ''; } let resultString = ''; const sortedKeyList = Object.keys(dynamicContentParameters).sort(); for (const key of sortedKeyList) { const value = dynamicContentParameters[key] ?? []; const valueString = value.join(','); if (resultString.length > 0) { resultString += '_'; } resultString += `${key}:${valueString}`; } return resultString; } private _parseErrorFromEvent( event: NativeSyntheticEvent ): FWError | undefined { const { name, reason } = event.nativeEvent; if (!name) { return undefined; } let error: FWError = { name }; if (reason) { error.reason = reason; } return error; } private _onCircleStoryLoadFinished = (event: NativeSyntheticEvent) => { const { onCircleStoryLoadFinished, onCircleStoryEmpty } = this.props; const error = this._parseErrorFromEvent(event); if (error) { onCircleStoryLoadFinished?.(error); if (!this._loaded) { onCircleStoryEmpty?.(error); } return; } onCircleStoryLoadFinished?.(); this._loaded = true; }; private _onCircleStoryEmpty = (event: NativeSyntheticEvent) => { FWLoggerUtil.log( `CircleStory _onCircleStoryEmpty ${JSON.stringify(event.nativeEvent)}` ); const { onCircleStoryEmpty } = this.props; const error = this._parseErrorFromEvent(event); onCircleStoryEmpty?.(error); }; private _onCircleStoryDidStartPictureInPicture = ( event: NativeSyntheticEvent ) => { FWLoggerUtil.log( `CircleStory _onCircleStoryDidStartPictureInPicture ${JSON.stringify( event.nativeEvent )}` ); const { onCircleStoryDidStartPictureInPicture } = this.props; const error = this._parseErrorFromEvent(event); onCircleStoryDidStartPictureInPicture?.(error); }; private _onCircleStoryDidStopPictureInPicture = ( event: NativeSyntheticEvent ) => { FWLoggerUtil.log( `CircleStory _onCircleStoryDidStopPictureInPicture ${JSON.stringify( event.nativeEvent )}` ); const { onCircleStoryDidStopPictureInPicture } = this.props; const error = this._parseErrorFromEvent(event); onCircleStoryDidStopPictureInPicture?.(error); }; private _onCircleStoryGetFeedId = (event: NativeSyntheticEvent) => { FWLoggerUtil.log( `CircleStory onCircleStoryGetFeedId ${JSON.stringify(event.nativeEvent)}` ); const { onCircleStoryGetFeedId } = this.props; const { feedId } = event.nativeEvent; if (feedId) { FWGlobalState.getInstance().componentTypeByFeedIdMap.set( feedId, FWComponentType.circleStory ); } onCircleStoryGetFeedId?.(feedId ?? ''); }; private _getVideoPlayerConfiguration(): | VideoPlayerNativeConfiguration | undefined { const { videoPlayerConfiguration, enablePictureInPicture, enableSystemPictureInPicture, } = this.props; let resultVideoPlayerConfiguration: | VideoPlayerNativeConfiguration | undefined = videoPlayerConfiguration; if (typeof enablePictureInPicture === 'boolean') { if (!resultVideoPlayerConfiguration) { resultVideoPlayerConfiguration = {}; } resultVideoPlayerConfiguration = { ...resultVideoPlayerConfiguration, enablePictureInPicture, }; } if (typeof enableSystemPictureInPicture === 'boolean') { if (!resultVideoPlayerConfiguration) { resultVideoPlayerConfiguration = {}; } resultVideoPlayerConfiguration = { ...resultVideoPlayerConfiguration, enableSystemPictureInPicture, }; } return resultVideoPlayerConfiguration; } render() { if (!this.state.sdkInitSucceeded) { return null; } const gShareBaseURL = FireworkSDK.getInstance().shareBaseURL; const appLanguage = FireworkSDK.getInstance().appLanguage; const videoLaunchBehavior = FireworkSDK.getInstance().videoLaunchBehavior; const adBadgeConfiguration = FireworkSDK.getInstance().adBadgeConfiguration; const adBadgeTextType = adBadgeConfiguration?.badgeTextType; const backgroundColorOfAdBadge = adBadgeConfiguration?.backgroundColor; const textColorOfAdBadge = adBadgeConfiguration?.textColor; const androidFontIsCustomOfAdBadge = adBadgeConfiguration?.androidFontInfo?.isCustom?.toString(); const androidFontTypefaceNameOfAdBadge = adBadgeConfiguration?.androidFontInfo?.typefaceName; const dataTrackingLevel = FireworkSDK.getInstance().dataTrackingLevel; const livestreamPlayerDesignVersion = FireworkSDK.getInstance().livestreamPlayerDesignVersion; const shortVideoPlayerDesignVersion = FireworkSDK.getInstance().shortVideoPlayerDesignVersion; const { source, channel, playlist, playlistGroup, hashtagFilterExpression, productIds, contentId, circleStoryConfiguration, adConfiguration, } = this.props; const dynamicContentParametersString = this._generateDynamicContentParametersString(); const videoPlayerConfiguration = this._getVideoPlayerConfiguration(); const circleStoryConfigurationJsonKey = gennerateJsonKey( circleStoryConfiguration ); const videoPlayerConfigurationJsonKey = gennerateJsonKey( videoPlayerConfiguration ); const adConfigurationJsonKey = gennerateJsonKey(adConfiguration); let key = `gShareBaseURL:${gShareBaseURL}`; key += `_appLanguage:${appLanguage}`; key += `_videoLaunchBehavior:${videoLaunchBehavior}`; key += `_adBadgeTextType:${adBadgeTextType}`; key += `_backgroundColorOfAdBadge:${backgroundColorOfAdBadge}`; key += `_textColorOfAdBadge:${textColorOfAdBadge}`; if (Platform.OS === 'android') { key += `_androidFontIsCustomOfAdBadge${androidFontIsCustomOfAdBadge}`; key += `_androidFontTypefaceNameOfAdBadge${androidFontTypefaceNameOfAdBadge}`; } key += `_dataTrackingLevel:${dataTrackingLevel}`; key += `_livestreamPlayerDesignVersion:${livestreamPlayerDesignVersion}`; key += `_shortVideoPlayerDesignVersion:${shortVideoPlayerDesignVersion}`; key += `_source:${source}`; key += `_channel:${channel}`; key += `_playlist:${playlist}`; key += `_playlistGroup:${playlistGroup}`; key += `_dynamicContentParameters:${dynamicContentParametersString}`; key += `_hashtagFilterExpression:${hashtagFilterExpression}`; key += `_productIds:${productIds?.join(',')}`; key += `_contentId:${contentId}`; key += `_circleStoryConfiguration:${circleStoryConfigurationJsonKey}`; key += `_videoPlayerConfiguration:${videoPlayerConfigurationJsonKey}`; key += `_adConfiguration:${adConfigurationJsonKey}`; return ( ); } } export default CircleStory;