import { useEffect, useRef, useState } from "react"; import type { AdDecision } from "./types.ts"; export type { AdDecision }; /** Render only after the partner backend has selected an ad. No publisher keys here. */ type CardProps = { ad: AdDecision; onDismiss?: () => void; openExternal?: (url: string) => void; }; export function AdCard(props: CardProps) { return ; } function CardContent({ ad, onDismiss, openExternal }: CardProps) { const ref = useRef(null); const cleanup = useRef<() => void>(() => {}); const [hidden, setHidden] = useState(false); useEffect(() => { setHidden(false); const el = ref.current; if (!el || Date.parse(ad.expiresAt) <= Date.now()) { setHidden(true); return; } let timer: ReturnType | undefined; let sent = false; let stopped = false; const remaining = Math.max(0, Date.parse(ad.expiresAt) - Date.now()); const expiry = setTimeout(() => setHidden(true), remaining); const send = async () => { if (sent || stopped || document.visibilityState !== "visible") return; sent = true; try { const r = await fetch(ad.tracking.impressionUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token: ad.tracking.token, type: "impression", }), keepalive: true, credentials: "omit", referrerPolicy: "no-referrer", }); if (!r.ok && !stopped) sent = false; } catch { if (!stopped) sent = false; } }; let visible = false; const schedule = () => { clearTimeout(timer); if (visible && document.visibilityState === "visible" && !sent) timer = setTimeout(() => { void send(); }, 1000); }; const observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver( (entries) => { visible = (entries[0]?.intersectionRatio ?? 0) >= 0.5; schedule(); }, { threshold: [0, 0.5] }, ); observer?.observe(el); document.addEventListener("visibilitychange", schedule); const stop = () => { stopped = true; clearTimeout(timer); clearTimeout(expiry); observer?.disconnect(); document.removeEventListener("visibilitychange", schedule); }; cleanup.current = stop; return stop; }, [ad]); let destination: string | undefined; try { const url = new URL(ad.tracking.destinationUrl ?? ""); if (url.protocol === "https:" && !url.username && !url.password) destination = url.href; } catch {} const href = destination ?? ad.tracking.clickUrl; const trackClick = () => { if (!destination) return; try { const token = new URL(ad.tracking.clickUrl).searchParams.get("token"); if (token?.startsWith("c1.")) void fetch(ad.tracking.impressionUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, type: "click" }), keepalive: true, credentials: "omit", referrerPolicy: "no-referrer", }).catch(() => {}); } catch { /* Tracking cannot interrupt native navigation. */ } }; if (hidden || Date.parse(ad.expiresAt) <= Date.now()) return null; return ( ); }