import { EventSubscription } from 'fbemitter'; import React, { ReactNode } from 'react'; import { findNodeHandle, requireNativeComponent } from 'react-native'; import MediaView from './MediaViewManager'; import AdIconView from './AdIconViewManager'; import { AdChoicesViewContext, AdIconViewContext, AdIconViewContextValueType, ComponentOrClass, MediaViewContext, MediaViewContextValueType, TriggerableContext, TriggerableContextValueType } from './contexts'; import { HasNativeAd, NativeAd } from './nativeAd'; import AdsManager from './NativeAdsManager'; import { areSetsEqual } from '../util/areSetsEqual'; interface NativeAdViewProps { adsManager: string; onAdLoaded: (args: { nativeEvent: NativeAd }) => void; } // tslint:disable-next-line:variable-name const NativeAdView = requireNativeComponent('CTKNativeAd'); interface AdWrapperState { ad?: NativeAd; canRequestAds: boolean; mediaViewNodeHandle: number; adIconViewNodeHandle: number; clickableChildren: Set; } interface AdWrapperProps { adsManager: AdsManager; onAdLoaded?: (ad: NativeAd) => void; } export default ( // tslint:disable-next-line:variable-name Component: React.ComponentType ) => class NativeAdWrapper extends React.Component< AdWrapperProps & T, AdWrapperState > { private subscription?: EventSubscription; private subscriptionError?: EventSubscription; private nativeAdViewRef?: React.Component; private registerFunctionsForTriggerables: TriggerableContextValueType; private registerFunctionsForMediaView: MediaViewContextValueType; private registerFunctionsForAdIconView: AdIconViewContextValueType; private clickableChildrenNodeHandles: Map; constructor(props: AdWrapperProps & T) { super(props); this.registerFunctionsForTriggerables = { register: this.registerClickableChild, unregister: this.unregisterClickableChild }; this.registerFunctionsForMediaView = { unregister: this.unregisterMediaView, register: this.registerMediaView }; this.registerFunctionsForAdIconView = { unregister: this.unregisterAdIconView, register: this.registerAdIconView }; this.clickableChildrenNodeHandles = new Map(); this.state = { // iOS requires a non-null value mediaViewNodeHandle: -1, adIconViewNodeHandle: -1, clickableChildren: new Set(), canRequestAds: false }; } /** * On init, register for updates on `adsManager` to know when it becomes available */ public componentDidMount() { this.subscription = this.props.adsManager.onAdsLoaded(() => this.setState({ canRequestAds: true }) ); this.subscriptionError = this.props.adsManager.onAdsError(() => this.setState({ canRequestAds: false }) ); } public componentDidUpdate(_: AdWrapperProps, prevState: AdWrapperState) { if ( this.state.mediaViewNodeHandle === -1 || this.state.adIconViewNodeHandle === -1 ) { // Facebook's SDK requires both MediaView and AdIconView references in order to register // interactable views. If one of them is missing, we can't proceed with the registration. return; } const mediaViewNodeHandleChanged = this.state.mediaViewNodeHandle !== prevState.mediaViewNodeHandle; const adIconViewNodeHandleChanged = this.state.adIconViewNodeHandle !== prevState.adIconViewNodeHandle; const clickableChildrenChanged = areSetsEqual( prevState.clickableChildren, this.state.clickableChildren ); if ( mediaViewNodeHandleChanged || adIconViewNodeHandleChanged || clickableChildrenChanged ) { const viewHandle = findNodeHandle(this.nativeAdViewRef!); if (!viewHandle) { // Skip registration if the view is no longer valid. return; } AdsManager.registerViewsForInteractionAsync( viewHandle, this.state.mediaViewNodeHandle, this.state.adIconViewNodeHandle, [...this.state.clickableChildren] ); } } /** * Clear subscription when component goes off screen */ public componentWillUnmount() { if (this.subscription) { this.subscription.remove(); } if (this.subscriptionError) { this.subscriptionError.remove(); } } private registerMediaView = (mediaView: ComponentOrClass) => this.setState({ mediaViewNodeHandle: findNodeHandle(mediaView) || -1 }); private unregisterMediaView = () => this.setState({ mediaViewNodeHandle: -1 }); private registerAdIconView = (adIconView: ComponentOrClass) => this.setState({ adIconViewNodeHandle: findNodeHandle(adIconView) || -1 }); private unregisterAdIconView = () => this.setState({ adIconViewNodeHandle: -1 }); private registerClickableChild = (child: ComponentOrClass) => { const handle = findNodeHandle(child); if (!handle) { return; } this.clickableChildrenNodeHandles.set(child, handle); this.setState({ clickableChildren: this.state.clickableChildren.add(handle) }); }; private unregisterClickableChild = (child: ComponentOrClass) => { this.setState(({ clickableChildren }) => { const newClickableChildren = new Set(clickableChildren); newClickableChildren.delete( this.clickableChildrenNodeHandles.get(child)! ); this.clickableChildrenNodeHandles.delete(child); return { clickableChildren: newClickableChildren }; }); }; private handleAdUpdated = () => this.state.ad && this.props.onAdLoaded && this.props.onAdLoaded(this.state.ad); private handleAdLoaded = ({ nativeEvent }: { nativeEvent: NativeAd }) => { this.setState({ ad: nativeEvent }, this.handleAdUpdated); }; private handleNativeAdViewMount = (ref: React.Component) => { this.nativeAdViewRef = ref; }; private renderAdComponent(componentProps: T): ReactNode { if (!this.state.ad) { return null; } return ( {/* Facebook's registerViewForInteraction requires both AdIconView and MediaView references to be set. We include both as a default */} ); } render() { // Cast to any until https://github.com/Microsoft/TypeScript/issues/10727 is resolved const { adsManager, onAdLoaded, ...rest } = this.props as any; if (!this.state.canRequestAds) { return null; } return ( {this.renderAdComponent(rest)} ); } };