import { bridgeState } from './bridge-state'; import { addConnectListener, addListener, BridgeEventListener, ConnectListener, registerEventHandler, } from './event-listener'; import { sendEvent } from './event-sender'; import { CommentsInfoEventPayload, OutgoingEvent, PageMetaData, PlayAudioPayload, PlaybackState, PlayLivestreamPayload, PlayVideoPayload, TrackingEventPayload, WebToUDPTrackingEventPayload, } from './event-types'; import { SRFLegacyBridge } from './legacy-compatibility'; import { log } from './logger'; export const SRFBridge = { /** * Initialize the native bridge with the given page metadata. * @param pageMetaData the metadata of the current page * @param debugMode true if you want to enable debug logging, defaults to false */ init: (pageMetaData: PageMetaData, debugMode: boolean = false) => { bridgeState.debugModeEnabled = debugMode; log('Initializing'); registerEventHandler(); initalizeApp(); sendEvent({ eventType: 'PageMetaData', payload: pageMetaData }); }, /** * Register a listener for a bridge event. * @param eventType the event name you want to be notified about. * @param listener the callback when such an event is received. */ addListener: (eventType: string, listener: BridgeEventListener) => addListener(eventType, listener), /** * @returns true if the current page is running in an app. */ isRunningInApp: () => !!(window.SRFApp || SRFLegacyBridge.isRunningInLegacyApp()), /** * @returns true if autoplay should be enabled */ isAutoplayEnabled: () => bridgeState.autoplayEnabled || SRFLegacyBridge.isAutoplayEnabled(), /** * @returns true if view mode should be compact */ isViewModeCompact: () => bridgeState.compactViewModeEnabled, /** * Requests the app to play the video with the given URN * @param payload a PlayVideoPayload */ playVideo: (payload: PlayVideoPayload) => sendEvent({ eventType: 'PlayVideo', payload }), /** * Requests the app to play the audio with the given URN * @param payload a PlayAudioPayload */ playAudio: (payload: PlayAudioPayload) => sendEvent({ eventType: 'PlayAudio', payload }), /** * Requests the app to play the livestream with the given URN * @param payload a PlayLivestreamPayload */ playLivestream: (payload: PlayLivestreamPayload) => sendEvent({ eventType: 'PlayLivestream', payload }), /** * Information about media playback state between app and web * @param playbackState * @param urn */ mediaPlaybackStatus: (playbackState: PlaybackState, urn: string) => sendEvent({ eventType: 'MediaPlaybackStatus', payload: { playbackState, urn }, }), /** * Informs the app about the current view mode * @param isCompact compact state */ informViewMode: (isCompact: boolean) => sendEvent({ eventType: 'InformViewMode', payload: { isCompact } }), /** * Informs the app whether a swipeable collection is visible in the viewport. * This is necessary to prevent column swiping in the Android Webviews inside a HorizontalPager. * @param swipeableInViewport true if a swipeable collection is visible false otherwise */ updateSwipeableStatus: (swipeableInViewport: boolean) => sendEvent({ eventType: 'SwipeableStatus', payload: { swipeableInViewport }, }), /** * Informs the app about the current comment situation * @param payload the comment info payload */ sendCommentsInfo: (payload: CommentsInfoEventPayload) => sendEvent({ eventType: 'CommentsInfo', payload }), /** * Requests the app to open an article * @param urn the article URN * @param url the URL * @param title the article title */ openArticle: (urn: string, url: string, title: string) => { try { const typedURL = new URL(url); const params = typedURL?.searchParams; const anchor = params?.get('anchor'); if (anchor) { sendEvent({ eventType: 'OpenArticle', payload: { urn, url, title, anchor }, }); return; } } catch (error) { // no valid URL } sendEvent({ eventType: 'OpenArticle', payload: { urn, url, title } }); }, /** * Requests the app to open a landing page * @param urn the landing page URN * @param url the URL * @param title the landing page title */ openLandingPage: (urn: string, url: string, title: string) => sendEvent({ eventType: 'OpenLandingPage', payload: { urn, url, title } }), /** * Requests the app to open an external link * @param url the URL * @param title the title of the link */ openLink: (url: string, title?: string) => sendEvent({ eventType: 'OpenLink', payload: { url, title } }), /** * Requests the app to track the given metadata * @param payload the metadata payload to track */ trackEvent: (payload: TrackingEventPayload) => sendEvent({ eventType: 'TrackingEvent', payload }), /** * Requests the app to track the given metadata to UDP * @param payload the metadata payload to track */ trackUdpEvent: (payload: WebToUDPTrackingEventPayload) => sendEvent({ eventType: 'WebToUDPTrackingEvent', payload }), /** * Requests the app to authenticate the user * @param payload client identifier, a scope for authentication */ requestAuthentication: (clientId: string) => sendEvent({ eventType: 'RequestAuthentication', payload: { clientId } }), /** * Requests the app to deauthenticate the user * @param clientIds client identifiers for the logout */ requestLogout: (clientIds: string[]) => sendEvent({ eventType: 'RequestLogout', payload: { clientIds } }), /** * Requests the app to open user profile */ openProfile: () => sendEvent({ eventType: 'OpenProfile', payload: {} }), /** * Sends the given event to the app * @param event the event to send */ sendEvent: (event: OutgoingEvent) => sendEvent(event), /** * Notifies the given listener when an app has connected. * @param listener the listener to notify when the app is ready */ onAppConnected: (listener: ConnectListener) => addConnectListener(listener), }; function initalizeApp() { if (window.SRFApp?.init) { // Android provides an init function window.SRFApp.init(); } else if (window.SRFApp) { // iOS expects a message through the native port for initialization window.webkit?.messageHandlers?.nativePort?.postMessage( JSON.stringify({ eventType: 'Ready', payload: {} }) ); } SRFLegacyBridge.init(); } declare global { interface Window { // This is the global object injected by apps supporting this version of the bridge. // If this object is defined, we assume that we are running in an SRF app. SRFApp?: { // Android defines an init() function that we use to initialize the app, // iOS expects that we send and event using the message channel below. init?: () => void; }; messageListenerRegistered: boolean; // iOS defines the message channel to talk to the app on the webkit object webkit?: { messageHandlers: { nativePort?: MessagePort; }; }; // ****** LEGACY STUFF ****** // These funtions have to be defined globally by pages supporting old apps. fetchMetaData: () => void; setDeviceInfo: (data: any) => void; setAutoplayEnabled: (autoplay: boolean) => void; // The 'nativebridge' object is injected by old apps (similar to the new SRFApp above). // If this object is defined, we assume that we are running in a legacy SRF app. nativebridge?: { onMetadataRetrieved: (metadata: any) => void; trackEvent: (event: any) => void; trackUdpEvent: (event: any) => void; isAutoplayEnabled?: () => Promise; }; // These globals are set by the legacy bridge compatibility mode code. legacyAppReady?: boolean; legacyAutoplayEnabled?: boolean; legacyDeviceInfo?: { ceid: string; navigation_app_site_name: string; }; } }