import * as React from 'react'; import { useEffect, useState, useRef, useCallback, useImperativeHandle, useReducer, forwardRef } from 'react'; import { StyleSheet, useWindowDimensions, View } from 'react-native'; import type { NativeSyntheticEvent, ViewProps, ViewStyle, StyleProp, DimensionValue } from 'react-native'; import AppLovinMAX from './specs/NativeAppLovinMAXModule'; import AdViewComponent, { Commands } from './specs/AppLovinMAXAdViewNativeComponent'; import type { AdInfoEvent, AdLoadFailedEvent, AdDisplayFailedEvent } from './specs/AppLovinMAXAdViewNativeComponent'; import { addEventListener, removeEventListener } from './EventEmitter'; import type { AdInfo, AdLoadFailedInfo } from './types/AdInfo'; import type { AdViewProps, AdViewHandler, NativeUIComponentAdViewOptions, AdViewId } from './types/AdViewProps'; import { makeExtraParametersArray, makeLocalExtraParametersArray } from './Utils'; const { ON_NATIVE_UI_COMPONENT_ADVIEW_AD_LOADED_EVENT, ON_NATIVE_UI_COMPONENT_ADVIEW_AD_LOAD_FAILED_EVENT } = AppLovinMAX.getConstants(); /** * Defines the format of an ad. */ export enum AdFormat { /** * Banner ad. */ BANNER = 'BANNER', /** * MREC ad. */ MREC = 'MREC', } /** * Defines the position for rendering a banner or MREC ad within its container. */ export enum AdViewPosition { TOP_CENTER = 'top_center', TOP_LEFT = 'top_left', TOP_RIGHT = 'top_right', CENTERED = 'centered', CENTER_LEFT = 'center_left', CENTER_RIGHT = 'center_right', BOTTOM_LEFT = 'bottom_left', BOTTOM_CENTER = 'bottom_center', BOTTOM_RIGHT = 'bottom_right', } type SizeKey = 'width' | 'height'; type SizeRecord = Partial>; const ADVIEW_SIZE = { banner: { width: 320, height: 50 }, leader: { width: 728, height: 90 }, mrec: { width: 300, height: 250 }, }; // Extracts width and height from the style prop, defaulting to 'auto' when unspecified. const getOutlineViewSize = (style: StyleProp): [DimensionValue, DimensionValue] => { const viewStyle = StyleSheet.flatten(style) || {}; return [viewStyle.width ?? 'auto', viewStyle.height ?? 'auto']; }; const handleAdViewEvent = (event: NativeSyntheticEvent, callback?: (adInfo: T) => void) => { if (!callback) return; callback(event.nativeEvent); }; /** * Renders a banner or MREC ad using a native view, with adaptive or responsive sizing. * * - **Banners**: 320×50 on phones, 728×90 on tablets. * - **MRECs**: Fixed size of 300×250 on all devices. * * Internally uses {@link AppLovinMAX.isTablet()} to determine banner size on tablets. * You can also use this method externally to assist with layout decisions. * * For adaptive banners, use {@link BannerAd.getAdaptiveHeightForWidth()} to determine the appropriate height. * * **Preloading**: * If you preload an ad for AdView using {@link preloadNativeUIComponentAdView}, * pass the returned {@link AdViewId} to this component to display the preloaded instance. * * **Note:** The AppLovin SDK must be initialized before using this component. * * ### Example: * ```tsx * { ... }} * /> * ``` * * For complete implementation examples, see: * - https://github.com/AppLovin/AppLovin-MAX-React-Native/blob/master/example/src/NativeBannerExample.tsx * - https://github.com/AppLovin/AppLovin-MAX-React-Native/blob/master/example/src/NativeMRecExample.tsx */ export const AdView = forwardRef(function AdView( { adUnitId, adFormat, adViewId, placement, customData, adaptiveBannerEnabled = true, autoRefresh = true, loadOnMount = true, extraParameters, localExtraParameters, onAdLoaded, onAdLoadFailed, onAdDisplayFailed, onAdClicked, onAdExpanded, onAdCollapsed, onAdRevenuePaid, style, ...otherProps }, ref ) { const { width: screenWidth } = useWindowDimensions(); const adFormatSize = useRef({}); const [, forceUpdate] = useReducer((x) => x + 1, 0); const adViewRef = useRef | undefined>(); const [isInitialized, setIsInitialized] = useState(false); const dimensions = useRef({}); const loadAd = useCallback(() => { adViewRef.current && Commands.loadAd(adViewRef.current); }, []); const destroy = useCallback(() => { adViewRef.current && Commands.destroy(adViewRef.current); }, []); useImperativeHandle(ref, () => ({ loadAd, destroy }), [loadAd, destroy]); useEffect(() => { (async () => { const isTablet = adFormat === AdFormat.BANNER ? await AppLovinMAX.isTablet() : false; adFormatSize.current = adFormat === AdFormat.BANNER ? (isTablet ? ADVIEW_SIZE.leader : ADVIEW_SIZE.banner) : ADVIEW_SIZE.mrec; const initialized = await AppLovinMAX.isInitialized(); setIsInitialized(initialized); if (!initialized) { console.warn('AdView was mounted before the AppLovin MAX React Native module was initialized.'); } })(); }, [adFormat]); useEffect(() => { const [width, height] = getOutlineViewSize(style); if (dimensions.current.width === width && dimensions.current.height === height) return; (async () => { const isBanner = adFormat === AdFormat.BANNER; const isWidthAuto = width === 'auto'; const isHeightAuto = height === 'auto'; const resolvedWidth = isWidthAuto ? (isBanner ? screenWidth : adFormatSize.current.width) : width; let resolvedHeight: DimensionValue | undefined = height; if (isHeightAuto) { if (isBanner && adaptiveBannerEnabled) { try { resolvedHeight = await AppLovinMAX.getAdaptiveBannerHeightForWidth(screenWidth); } catch (error) { console.error('Error getting adaptive banner height:', error); resolvedHeight = adFormatSize.current.height; } } else { resolvedHeight = adFormatSize.current.height; } } const newSize = { width: resolvedWidth, height: resolvedHeight }; if (dimensions.current.width !== newSize.width || dimensions.current.height !== newSize.height) { dimensions.current = newSize; forceUpdate(); } })(); }, [adFormat, adaptiveBannerEnabled, isInitialized, screenWidth, style]); const onAdLoadedEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdLoaded), [onAdLoaded]); const onAdLoadFailedEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdLoadFailed), [onAdLoadFailed]); const onAdDisplayFailedEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdDisplayFailed), [onAdDisplayFailed]); const onAdClickedEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdClicked), [onAdClicked]); const onAdExpandedEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdExpanded), [onAdExpanded]); const onAdCollapsedEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdCollapsed), [onAdCollapsed]); const onAdRevenuePaidEvent = useCallback((event: NativeSyntheticEvent) => handleAdViewEvent(event, onAdRevenuePaid), [onAdRevenuePaid]); if (!isInitialized || Object.keys(dimensions.current).length === 0) { // Early return if not initialized or dimensions are not set return ; } return ( (adViewRef.current = element ?? undefined)} adUnitId={adUnitId} adFormat={adFormat} adViewId={adViewId || 0} placement={placement} customData={customData} adaptiveBannerEnabled={adaptiveBannerEnabled} autoRefresh={autoRefresh} loadOnMount={loadOnMount} extraParameters={makeExtraParametersArray(extraParameters)} strLocalExtraParameters={makeLocalExtraParametersArray(localExtraParameters, 'str')} boolLocalExtraParameters={makeLocalExtraParametersArray(localExtraParameters, 'bool')} onAdLoadedEvent={onAdLoadedEvent} onAdLoadFailedEvent={onAdLoadFailedEvent} onAdDisplayFailedEvent={onAdDisplayFailedEvent} onAdClickedEvent={onAdClickedEvent} onAdExpandedEvent={onAdExpandedEvent} onAdCollapsedEvent={onAdCollapsedEvent} onAdRevenuePaidEvent={onAdRevenuePaidEvent} style={[style, dimensions.current]} {...otherProps} /> ); }); /** * Preloads a native UI component for {@link AdView}, enabling faster ad display on first mount. * * - When you mount {@link AdView} with the preloaded Ad Unit ID, it uses the preloaded native UI component. * - Unmounting {@link AdView} does not destroy the preloaded component—it will be reused on the next mount. * - You must manually destroy the preloaded component when it is no longer needed using {@link destroyNativeUIComponentAdView}. * * @param adUnitId - The Ad Unit ID for which ads should be preloaded. * @param adFormat - The ad format to preload. Must be either {@link AdFormat.BANNER} or {@link AdFormat.MREC}. * @param options - Optional properties to configure the native UI component (e.g., placement, custom data). * @returns A promise that resolves to an {@link AdViewId}, uniquely identifying the preloaded component instance. * @throws An error if the preload request fails. */ export const preloadNativeUIComponentAdView = async (adUnitId: string, adFormat: AdFormat, options: NativeUIComponentAdViewOptions = {}): Promise => { const { isAdaptive = true, placement = null, customData = null, extraParameters = {}, localExtraParameters = {} } = options; return AppLovinMAX.preloadNativeUIComponentAdView(adUnitId, adFormat, isAdaptive, placement, customData, extraParameters, localExtraParameters); }; /** * Destroys a preloaded native UI component identified by its {@link AdViewId}. * * - This method should be called when the preloaded component is no longer needed to free resources. * * @param adViewId - The {@link AdViewId} of the native UI component to be destroyed. * @returns A promise that resolves when the native UI component is successfully destroyed. * @throws An error if the destruction process fails. */ export const destroyNativeUIComponentAdView = async (adViewId: AdViewId): Promise => { if (adViewId === undefined) { return Promise.reject(new Error('adViewId is not provided')); } return AppLovinMAX.destroyNativeUIComponentAdView(adViewId); }; /** * Adds the specified event listener to receive {@link AdInfo} when a native UI component loads a new ad. * * @param listener Listener to be notified. */ export const addNativeUIComponentAdViewAdLoadedEventListener = (listener: (adInfo: AdInfo) => void): void => { addEventListener(ON_NATIVE_UI_COMPONENT_ADVIEW_AD_LOADED_EVENT, listener); }; /** * Removes the event listener registered to receive {@link AdInfo} when a native UI component loads a new ad. */ export const removeNativeUIComponentAdViewAdLoadedEventListener = (): void => { removeEventListener(ON_NATIVE_UI_COMPONENT_ADVIEW_AD_LOADED_EVENT); }; /** * Adds the specified event listener to receive {@link AdLoadFailedInfo} when a native UI component could not load a new ad. * * @param listener Listener to be notified. */ export const addNativeUIComponentAdViewAdLoadFailedEventListener = (listener: (errorInfo: AdLoadFailedInfo) => void): void => { addEventListener(ON_NATIVE_UI_COMPONENT_ADVIEW_AD_LOAD_FAILED_EVENT, listener); }; /** * Removes the event listener registered to receive {@link AdLoadFailedInfo} when a native UI component could not load a new ad. */ export const removeNativeUIComponentAdViewAdLoadFailedEventListener = (): void => { removeEventListener(ON_NATIVE_UI_COMPONENT_ADVIEW_AD_LOAD_FAILED_EVENT); };