import { EventSubscription } from 'fbemitter'; import nullthrows from 'nullthrows'; import PropTypes from 'prop-types'; import React from 'react'; import { Platform, View, ViewPropTypes, findNodeHandle, requireNativeComponent, } from 'react-native'; import { NativeAdIconView } from './AdIconView'; import { NativeAdMediaView } from './AdMediaView'; import AdsManager from './NativeAdsManager'; type AdContainerProps

= { adsManager: AdsManager; // TODO: rename this to onAdLoad onAdLoaded?: ((ad: NativeAd) => void) | null; } & P; type AdContainerState = { ad: NativeAd | null; canRequestAds: boolean; }; type AdProps = { nativeAd: NativeAd }; type AdNodeHandles = { adMediaViewNodeHandle?: number | null; adIconViewNodeHandle?: number | null; interactiveTriggerNodeHandles?: Map; }; /** * A higher-order function that wraps the given `Component` type and returns a new container * component type that passes in an extra `nativeAd` prop to the wrapped component. * * The container component renders null if the native ads manager is not yet ready to display ads or * if no ad could be loaded. */ export default function withNativeAd

( Component: React.ComponentType

): React.ComponentType> { return class NativeAdContainer extends React.Component, AdContainerState> { _subscription: EventSubscription | null = null; _nativeAdViewRef = React.createRef(); _adMediaViewNodeHandle: number | null = null; _adIconViewNodeHandle: number | null = null; _interactiveTriggerNodeHandles: Map = new Map(); state: AdContainerState; constructor(props: AdContainerProps

) { super(props); this.state = { ad: null, canRequestAds: props.adsManager.isValid, }; } componentDidMount() { if (!this.state.canRequestAds) { // On mounting, listen to the ads manager to learn when it is ready to display ads this._subscription = this.props.adsManager.onAdsLoaded(() => { this.setState({ canRequestAds: true }); }); } } componentWillUnmount() { if (this._subscription) { this._subscription.remove(); this._subscription = null; } } render() { if (!this.state.canRequestAds) { return null; } let { adsManager } = this.props; let props = this._getForwardedProps(); return ( {this.state.ad ? : null} ); } _getForwardedProps(): P { let { adsManager, onAdLoaded, ...props } = this.props as any; return props as P; } _handleAdLoaded = ({ nativeEvent: ad }: { nativeEvent: NativeAd }) => { this.setState({ ad }, () => { if (this.props.onAdLoaded) { let ad = nullthrows(this.state.ad); this.props.onAdLoaded(ad); } }); }; _adMediaViewContextValue = { nativeRef: (component: NativeAdMediaView | null) => { if (component) { this._setAdNodeHandles({ adMediaViewNodeHandle: nullthrows(findNodeHandle(component)) }); } else { this._setAdNodeHandles({ adMediaViewNodeHandle: null }); } }, }; _adIconViewContextValue = { nativeRef: (component: NativeAdIconView | null) => { if (component) { this._setAdNodeHandles({ adIconViewNodeHandle: nullthrows(findNodeHandle(component)) }); } else { this._setAdNodeHandles({ adIconViewNodeHandle: null }); } }, }; _adTriggerViewContextValue = { registerComponent: (component: React.Component) => { let nodeHandle = nullthrows(findNodeHandle(component)); let interactiveTriggerNodeHandles = new Map(this._interactiveTriggerNodeHandles); interactiveTriggerNodeHandles.set(component, nodeHandle); this._setAdNodeHandles({ interactiveTriggerNodeHandles }); }, unregisterComponent: (component: React.Component) => { let interactiveTriggerNodeHandles = new Map(this._interactiveTriggerNodeHandles); interactiveTriggerNodeHandles.delete(component); this._setAdNodeHandles({ interactiveTriggerNodeHandles }); }, onTriggerAd: () => { if (this._adMediaViewNodeHandle !== null && Platform.OS === 'android') { let nodeHandle = findNodeHandle(this._nativeAdViewRef.current)!; AdsManager.triggerEvent(nodeHandle); } }, }; /** * Updates the registered ad views given their node handles. The node handles are not stored in * this component's state nor does this method call "setState" to avoid unnecessarily * re-rendering. */ _setAdNodeHandles({ adMediaViewNodeHandle = this._adMediaViewNodeHandle, adIconViewNodeHandle = this._adIconViewNodeHandle, interactiveTriggerNodeHandles = this._interactiveTriggerNodeHandles, }: AdNodeHandles): void { let adMediaViewChanged = adMediaViewNodeHandle !== this._adMediaViewNodeHandle; let adIconViewChanged = adIconViewNodeHandle !== this._adIconViewNodeHandle; let interactiveTriggersChanged = !_areEqualSets( new Set(interactiveTriggerNodeHandles.values()), new Set(this._interactiveTriggerNodeHandles.values()) ); if (adMediaViewChanged || adIconViewChanged || interactiveTriggersChanged) { this._adMediaViewNodeHandle = adMediaViewNodeHandle; this._adIconViewNodeHandle = adIconViewNodeHandle; this._interactiveTriggerNodeHandles = interactiveTriggerNodeHandles; // TODO: handle unregistering views when components are unmounted if (this._adMediaViewNodeHandle !== null && this._adIconViewNodeHandle !== null) { AdsManager.registerViewsForInteractionAsync( nullthrows(findNodeHandle(this._nativeAdViewRef.current)), this._adMediaViewNodeHandle, this._adIconViewNodeHandle, [...this._interactiveTriggerNodeHandles.values()] ); } } } }; } type NativeAdViewProps = { adsManager: string; onAdLoaded?: (event: { nativeEvent: NativeAd }) => void; } & React.ElementProps; type NativeAdView = React.Component; const NativeAdView = requireNativeComponent('CTKNativeAd', { propTypes: { ...ViewPropTypes, adsManager: PropTypes.string.isRequired, onAdLoaded: PropTypes.func, }, }); // React contexts for ad views that need to register with the ad container export type AdIconViewContextValue = { nativeRef: (component: NativeAdMediaView | null) => void; }; export type AdMediaViewContextValue = { nativeRef: (component: NativeAdIconView | null) => void; }; export type AdTriggerViewContextValue = { registerComponent: (component: React.Component) => void; unregisterComponent: (component: React.Component) => void; onTriggerAd: () => void; }; export const AdIconViewContext = React.createContext(null); export const AdMediaViewContext = React.createContext(null); export const AdTriggerViewContext = React.createContext(null); export type NativeAd = { /** * The headline the advertiser entered when they created their ad. This is usually the ad's main * title. */ headline?: string; /** * The link description which is additional information that the advertiser may have entered */ linkDescription?: string; /** * The name of the Facebook Page or mobile app that represents the business running the ad */ advertiserName?: string; /** * The ad's social context, such as, "Over half a million users" */ socialContext?: string; /** * The call-to-action phrase of the ad, such as, "Install Now" */ callToActionText?: string; /** * The body text, truncated to 90 characters, that contains the text the advertiser entered when * they created their ad to tell people what the ad promotes */ bodyText?: string; /** * The word "ad", translated into the viewer's language */ adTranslation?: string; /** * The word "promoted", translated into the viewer's language */ promotedTranslation?: string; /** * The word "sponsored", translated into the viewer's language */ sponsoredTranslation?: string; }; function _areEqualSets(set1: Set, set2: Set): boolean { if (set1.size !== set2.size) { return false; } for (let item of set1.values()) { if (!set2.has(item)) { return false; } } return true; }