import React, { useEffect, useRef, useState } from 'react'; import { AppState, type AppStateStatus, StyleSheet, Text, View } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import { DEFAULT_NATIVE_AD_ASPECT_RATIO, DEFAULT_NATIVE_AD_PLACEMENT, getNativeAdCatalog, } from '../ads/native-ad-catalog'; import { colors, m3Colors, m3Shape, spacing } from '../theme/proulr-theme'; import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks'; type NativeAdModule = typeof import('react-native-google-mobile-ads'); type LoadedNativeAd = Awaited>; /** * Ao voltar do segundo plano o `NativeMediaView` do Android costuma renderizar * em branco e o ad expira (~1h). Recarrega quando falhou antes ou está mais * velho que isto; abaixo do limiar são só app-switches rápidos. */ const NATIVE_AD_RESUME_RELOAD_MS = 30_000; function resolveBlockWidth(props: Record): number | undefined { if (typeof props.width === 'number' && props.width > 0) return props.width; return undefined; } function resolvePlacement(props: Record): string { return typeof props.placement === 'string' && props.placement.length > 0 ? props.placement : DEFAULT_NATIVE_AD_PLACEMENT; } function resolveAspectRatio(placement: string, props: Record): number { if (typeof props.aspect_ratio === 'number' && props.aspect_ratio > 0) { return props.aspect_ratio; } const catalog = getNativeAdCatalog(); const placementConfig = catalog?.getPlacementConfig?.(placement); return placementConfig?.aspectRatio ?? DEFAULT_NATIVE_AD_ASPECT_RATIO; } export function SduiNativeAdBlock({ block }: SduiBlockRenderProps) { const catalog = getNativeAdCatalog(); const placement = resolvePlacement(block.props); const width = resolveBlockWidth(block.props); const aspectRatio = resolveAspectRatio(placement, block.props); const [adsModule, setAdsModule] = useState(null); const [nativeAd, setNativeAd] = useState(null); const [failed, setFailed] = useState(false); const [reloadToken, setReloadToken] = useState(0); const loadedAtRef = useRef(0); const failedRef = useRef(false); useEffect(() => { if (!catalog?.isEnabled()) return; let cancelled = false; void (async () => { try { const mod = await import('react-native-google-mobile-ads'); if (cancelled) return; setAdsModule(mod); const adUnitId = catalog.resolveAdUnitId(placement, mod.TestIds.NATIVE); const ad = await mod.NativeAd.createForAdRequest(adUnitId, { aspectRatio: mod.NativeMediaAspectRatio.PORTRAIT, adChoicesPlacement: mod.NativeAdChoicesPlacement.TOP_RIGHT, startVideoMuted: true, }); if (cancelled) { ad.destroy(); return; } loadedAtRef.current = Date.now(); failedRef.current = false; setFailed(false); setNativeAd(ad); } catch { if (!cancelled) { failedRef.current = true; setFailed(true); } } })(); return () => { cancelled = true; }; }, [catalog, placement, reloadToken]); useEffect(() => { if (!nativeAd) return; return () => { nativeAd.destroy(); }; }, [nativeAd]); useEffect(() => { if (!catalog?.isEnabled()) return; let backgroundSince: number | null = null; const subscription = AppState.addEventListener('change', (state: AppStateStatus) => { if (state === 'active') { if (backgroundSince === null) return; const backgroundMs = Date.now() - backgroundSince; backgroundSince = null; if (backgroundMs < 3_000) return; const stale = loadedAtRef.current === 0 || Date.now() - loadedAtRef.current > NATIVE_AD_RESUME_RELOAD_MS; if (failedRef.current || stale) { setReloadToken((token) => token + 1); } return; } if (backgroundSince === null) { backgroundSince = Date.now(); } }); return () => { subscription.remove(); }; }, [catalog]); if (!catalog?.isEnabled()) { return null; } const cardStyle = [ styles.card, { aspectRatio }, width != null ? { width } : styles.flexCard, ]; if (failed) { return ; } if (!adsModule || !nativeAd) { return ; } const { NativeAdView, NativeAsset, NativeAssetType, NativeMediaView } = adsModule; return ( Anúncio {nativeAd.headline ? ( {nativeAd.headline} ) : null} {nativeAd.callToAction ? ( {nativeAd.callToAction} ) : null} ); } const styles = StyleSheet.create({ flexCard: { flex: 1, }, card: { borderRadius: m3Shape.cornerMedium, overflow: 'hidden', backgroundColor: m3Colors.surfaceContainerHigh, borderWidth: 1, borderColor: m3Colors.outline, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.22, shadowRadius: 2, elevation: 1, }, placeholder: { borderRadius: m3Shape.cornerMedium, backgroundColor: m3Colors.surfaceContainerHigh, borderWidth: 1, borderColor: m3Colors.outlineVariant, }, loading: { opacity: 0.55, }, media: { ...StyleSheet.absoluteFill, }, shade: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 88, }, adBadge: { position: 'absolute', top: spacing.sm, left: spacing.sm, backgroundColor: m3Colors.surfaceContainerHighest, paddingHorizontal: spacing.sm, paddingVertical: 3, borderRadius: m3Shape.cornerSmall, }, adBadgeText: { color: colors.onSurfaceVariant, fontSize: 10, fontWeight: '700', letterSpacing: 0.4, textTransform: 'uppercase', }, headline: { position: 'absolute', left: spacing.sm, right: spacing.sm, bottom: spacing.xl + spacing.sm, color: m3Colors.onSurface, fontSize: 12, fontWeight: '700', }, cta: { position: 'absolute', left: spacing.sm, right: spacing.sm, bottom: spacing.sm, color: m3Colors.secondary, fontSize: 11, fontWeight: '600', }, });