import React, { useEffect, useRef } from "react"; import { useGleanClick } from "./telemetry/glean-context"; import useSWR from "swr"; import { PLACEMENT_ENABLED } from "./env"; import { useUserData } from "./user-context"; import { useLocation } from "react-router"; import { Payload as PlacementData } from "../../libs/pong/types"; export enum Status { success = "success", geoUnsupported = "geo_unsupported", capReached = "cap_reached", loading = "loading", empty = "empty", } type PlacementType = | "side" | "top" | "hpTop" | "hpMain" | "hpFooter" | "bottom"; export interface PlacementContextData extends Partial> { plusAvailable?: boolean; status: Status; } const PLACEMENT_MAP: Record = { side: { typ: "side", pattern: /^\/[^/]+\/(play|docs\/|blog\/|observatory\/?|curriculum\/[^$]|search$)/i, }, top: { typ: "top-banner", pattern: /^\/[^/]+\/(?!$|_homepage$).*/i, }, hpTop: { typ: "top-banner", pattern: /^\/[^/]+\/($|_homepage$)/i, }, hpMain: { typ: "hp-main", pattern: /^\/[^/]+\/($|_homepage$)/i, }, hpFooter: { typ: "hp-footer", pattern: /^\/[^/]+\/($|_homepage$)/i, }, bottom: { typ: "bottom-banner", pattern: /^\/[^/]+\/docs\//i, }, }; function placementTypes(pathname: string): string[] { return Object.entries(PLACEMENT_MAP) .map(([k, { pattern: re }]) => re.test(pathname) && k) .filter(Boolean) as string[]; } export const PlacementContext = React.createContext< PlacementContextData | null | undefined >(undefined); export function PlacementProvider(props: { children: React.ReactNode }) { const user = useUserData(); const location = useLocation(); const gleanClick = useGleanClick(); const pathname = useRef(location.pathname); const { data: pong, isLoading, isValidating, mutate, } = useSWR( !PLACEMENT_ENABLED || user?.settings?.noAds || !placementTypes(location.pathname) ? null : "/pong/get", async (url) => { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ keywords: [], pongs: placementTypes(location.pathname), }), }); gleanClick(`pong: pong->fetched ${response.status}`); if (!response.ok) { throw Error(response.statusText); } try { const { plusAvailable = true, // Fall back to true for seamless migration. ...placementResponse }: PlacementContextData = await response.json(); const typs = Object.entries(PLACEMENT_MAP) .filter(([key]) => key in placementResponse) .map(([, { typ }]) => typ); if (typs.length) { gleanClick(`pong: pong->served ${typs.join()}`); } return { plusAvailable, ...placementResponse }; } catch (e) { throw Error(response.statusText); } }, { revalidateIfStale: true, revalidateOnFocus: false, revalidateOnReconnect: false, } ); useEffect(() => { if (location.pathname !== pathname.current) { mutate(); pathname.current = location.pathname; } }, [location.pathname, mutate]); return ( {props.children} ); } export function usePlacement() { return React.useContext(PlacementContext); }