/** * BigCrunchBannerView - React Native component for displaying banner ads */ import React, { useEffect, useRef, useCallback, useMemo } from 'react'; import { requireNativeComponent, ViewStyle, StyleSheet, } from 'react-native'; import { BigCrunchAdsEventEmitter, NativeEventNames } from './NativeBigCrunchAds'; import type { BigCrunchBannerViewProps, BannerSize, AdError, AdRevenue, EventSubscription, } from './types'; // Native component name const NATIVE_COMPONENT_NAME = 'BigCrunchBannerView'; // Banner size dimensions const BANNER_SIZES = { BANNER: { width: 320, height: 50 }, LARGE_BANNER: { width: 320, height: 100 }, MEDIUM_RECTANGLE: { width: 300, height: 250 }, FULL_BANNER: { width: 468, height: 60 }, LEADERBOARD: { width: 728, height: 90 }, ADAPTIVE: { width: -1, height: -1 }, // Calculated dynamically SMART: { width: -1, height: -1 }, // Deprecated, use ADAPTIVE }; // Native view component const NativeBannerView = requireNativeComponent(NATIVE_COMPONENT_NAME); /** * React component for displaying BigCrunch banner ads * * @example * ```tsx * console.log('Ad loaded')} * onAdFailedToLoad={(error) => console.error('Ad failed to load', error)} * /> * ``` */ export const BigCrunchBannerView: React.FC = ({ placementId, size, autoLoad = true, refreshInterval = 0, customTargeting, style, onAdLoaded, onAdFailedToLoad, onAdImpression, onAdClicked, onAdOpened, onAdClosed, onAdRevenue, }) => { const viewRef = useRef(null); const subscriptionsRef = useRef([]); const viewIdRef = useRef(`banner_${placementId}_${Date.now()}`); // Calculate banner size for layout // When size is undefined, config drives the ad size natively — use adaptive layout const bannerSize = useMemo(() => { if (size === undefined) { // No explicit size — let backend config drive the ad size // Use adaptive layout since config may specify smart/adaptive sizes return { width: 0, height: 0, adaptive: true, configDriven: true }; } if (typeof size === 'object' && 'width' in size && 'height' in size) { // Custom size return size; } else if (typeof size === 'string' && size in BANNER_SIZES) { // Predefined size const dimensions = BANNER_SIZES[size as BannerSize]; if (dimensions.width === -1 || dimensions.height === -1) { // Adaptive/Smart banner - will be calculated natively return { width: 0, height: 0, adaptive: true }; } return dimensions; } // Unknown size string — fallback to adaptive layout return { width: 0, height: 0, adaptive: true }; }, [size]); // Handle native events const handleNativeEvent = useCallback((_eventName: string, handler?: Function) => { return (event: any) => { // Match exclusively on viewId — two banner views sharing a placementId must // not receive each other's events. Native attaches viewId to every banner event. if (event.viewId === viewIdRef.current) { handler?.(event); } }; }, []); // Setup event listeners useEffect(() => { const subscriptions: EventSubscription[] = []; if (onAdLoaded) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_LOADED, handleNativeEvent(NativeEventNames.BANNER_AD_LOADED, onAdLoaded) ) ); } if (onAdFailedToLoad) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_FAILED_TO_LOAD, handleNativeEvent(NativeEventNames.BANNER_AD_FAILED_TO_LOAD, (event: any) => { const error: AdError = { code: event.errorCode || 'UNKNOWN', message: event.errorMessage || 'Ad failed to load', underlyingError: event.underlyingError, }; onAdFailedToLoad(error); }) ) ); } if (onAdImpression) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_IMPRESSION, handleNativeEvent(NativeEventNames.BANNER_AD_IMPRESSION, onAdImpression) ) ); } if (onAdClicked) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_CLICKED, handleNativeEvent(NativeEventNames.BANNER_AD_CLICKED, onAdClicked) ) ); } if (onAdOpened) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_OPENED, handleNativeEvent(NativeEventNames.BANNER_AD_OPENED, onAdOpened) ) ); } if (onAdClosed) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_CLOSED, handleNativeEvent(NativeEventNames.BANNER_AD_CLOSED, onAdClosed) ) ); } if (onAdRevenue) { subscriptions.push( BigCrunchAdsEventEmitter.addListener( NativeEventNames.BANNER_AD_REVENUE, handleNativeEvent(NativeEventNames.BANNER_AD_REVENUE, (event: any) => { const revenue: AdRevenue = { valueMicros: event.valueMicros, currencyCode: event.currencyCode, adUnitId: event.adUnitId, precision: event.precision, }; onAdRevenue(revenue); }) ) ); } subscriptionsRef.current = subscriptions; // Cleanup on unmount return () => { subscriptions.forEach(sub => sub.remove()); subscriptionsRef.current = []; }; }, [ placementId, handleNativeEvent, onAdLoaded, onAdFailedToLoad, onAdImpression, onAdClicked, onAdOpened, onAdClosed, onAdRevenue, ]); // Note: auto-loading is handled natively via the `autoLoad` prop (both view // managers trigger the load once placementId is set). Dispatching a loadAd // command here as well caused every banner to load twice. // Container styles const containerStyle: ViewStyle = useMemo(() => { const baseStyle: ViewStyle = { width: bannerSize.width > 0 ? bannerSize.width : 320, height: bannerSize.height > 0 ? bannerSize.height : 50, // Remove overflow: 'hidden' to avoid clipping the ad content }; if ((bannerSize as any).adaptive) { // Adaptive banner - let it size itself baseStyle.alignSelf = 'stretch'; baseStyle.width = '100%'; } return StyleSheet.flatten([baseStyle, style]); }, [bannerSize, style]); // Native view props // Only send size/customWidth/customHeight when explicitly specified by the developer. // When omitted, the native SDK uses placement config sizes (e.g., smart/adaptive from backend). const nativeProps: any = { ref: viewRef, style: containerStyle, placementId, autoLoad, refreshInterval, customTargeting, viewId: viewIdRef.current, }; // Only send size to native when explicitly set by the developer if (size !== undefined) { if (typeof size === 'string') { nativeProps.size = size; } else if (typeof size === 'object' && 'width' in size && 'height' in size) { nativeProps.size = 'CUSTOM'; nativeProps.customWidth = size.width; nativeProps.customHeight = size.height; } } // Return the native view directly with the container style return ; }; // Export as default export default BigCrunchBannerView;