/** * Common utilities for dealing with Ads. */ import { isNothing, isSome } from "@jet/environment"; import { Lockup, TodayCard, TodayCardMediaMediumLockupWithAlignedRegion, TodayCardMediaMediumLockupWithScreenshots, TodayCardMediaSingleLockup, } from "../../api/models"; import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; import { attributeAsString } from "../../foundation/media/attributes"; import { IAdSearchInformation } from "../metrics/helpers/models"; import { platformSupportsAdverts } from "../search/search-ads"; import { shouldTodayAdBeCondensed } from "../../foundation/experimentation/today-ad-experiments"; import { getTemplateTypeForMediumAdFromLockupWithCustomCreative, getTemplateTypeForMediumAdFromLockupWithScreenshots, searchAdMissedOpportunityFromId, } from "../lockups/ad-lockups"; import { contentAttributeAsString } from "../content/attributes"; import { currentLocation, currentPosition } from "../metrics/helpers/location"; // region placement logic /** * A helper function to check if the given AdPlacement is enabled. * @param objectGraph The object graph. * @param adPlacementKey The ad placement to check. Matches values sent in the bag. * @returns A boolean indicating if the provided AdPlacement is enabled. */ export function isAdPlacementEnabled(objectGraph, placementType) { if (!platformSupportsAdverts(objectGraph)) { return false; } switch (placementType) { case "searchLanding": // In transitioning from the legacy bag key `isSearchLandingAdsEnabled` to the new key `enabledAdPlacements`, // we will enable SLP ads if either of the keys indicates ads are enabled for SLP. const slpPlacementBagValue = adPlacementBagValueForAdPlacementType(placementType); if (isSome(slpPlacementBagValue)) { return (objectGraph.bag.isSearchLandingAdsEnabled || objectGraph.bag.enabledAdPlacements.includes(slpPlacementBagValue)); } else { return objectGraph.bag.isSearchLandingAdsEnabled; } case "searchResults": return true; // Legacy case "today": const todayBagValue = adPlacementBagValueForAdPlacementType(placementType); if (isNull(todayBagValue)) { return false; } return objectGraph.bag.enabledAdPlacements.includes(todayBagValue) && isSome(todayAdStyle(objectGraph)); case "productPageYMAL": case "productPageYMALDuringDownload": const placementBagValue = adPlacementBagValueForAdPlacementType(placementType); if (isNull(placementBagValue)) { return false; } return objectGraph.bag.enabledAdPlacements.includes(placementBagValue); default: return false; } } /** * Get the style to use for the Today ad. * A return type of `undefined` indicates the ad style is unsupported on the client. * @param objectGraph The Object Graph. * @returns A style, or undefined if it's unsupported. */ export function todayAdStyle(objectGraph) { if (objectGraph.bag.todayAdMediumLockupScreenshotEnabled) { return "mediumLockup"; } if (objectGraph.bag.todayAdCondensedEnabled) { // Only supported on iPhone. if (!objectGraph.client.isPhone) { return undefined; } return "singleLockup"; } if (shouldTodayAdBeCondensed(objectGraph)) { return "singleLockup"; } else { return undefined; } } /** * Get the timeout of the ad fetch for a given placement from the bag values. * @param objectGraph The object graph. * @param placementType The ad placement. * @param isDeltaTimeout Whether the timeout we're looking for is a delta timeout between parallel organic and ad requests completing. * See `Ads.setTimeoutForCurrentOnDeviceAdFetch` for more details on how this is used. * @returns The timeout. */ export function adFetchTimeoutForPlacement(objectGraph, placementType, isDeltaTimeout) { var _a, _b, _c, _d, _e; const timeouts = objectGraph.bag.adPlacementTimeouts; const defaultTimeout = 0.3; switch (placementType) { case "searchResults": return isDeltaTimeout ? null : (_a = timeouts === null || timeouts === void 0 ? void 0 : timeouts["search-results-in-seconds"]) !== null && _a !== void 0 ? _a : defaultTimeout; case "searchLanding": return isDeltaTimeout ? null : (_b = objectGraph.bag.searchLandingAdFetchTimeout) !== null && _b !== void 0 ? _b : defaultTimeout; // Legacy value. case "today": // Today uses its timeout value as a delta after the organic request finishes. return isDeltaTimeout ? (_c = timeouts === null || timeouts === void 0 ? void 0 : timeouts["today-in-seconds"]) !== null && _c !== void 0 ? _c : defaultTimeout : null; case "productPageYMAL": return isDeltaTimeout ? null : (_d = timeouts === null || timeouts === void 0 ? void 0 : timeouts["product-page-ymal-in-seconds"]) !== null && _d !== void 0 ? _d : defaultTimeout; case "productPageYMALDuringDownload": return isDeltaTimeout ? null : (_e = timeouts === null || timeouts === void 0 ? void 0 : timeouts["product-page-ymal-during-download-in-seconds"]) !== null && _e !== void 0 ? _e : defaultTimeout; default: return defaultTimeout; } } /** * Convert the value describing an ad placement from the App Store representation to the bag representation. * @param placementType The bag value for different ad placements * @returns The equivalent bag placement value as a string. */ function adPlacementBagValueForAdPlacementType(placementType) { switch (placementType) { case "searchResults": return "search-results"; case "searchLanding": return "search-landing"; case "today": return "today"; case "productPageYMAL": return "product-page-ymal"; case "productPageYMALDuringDownload": return "product-page-ymal-during-download"; default: return undefined; } } /** * The minimum number of landscape media items for a Today ad placement. * The app must satisfy this or `todayAdPlacementMinimumPortraitMedia` to be shown. */ export const todayAdPlacementMinimumLandscapeMedia = 5; /** * The minimum portrait media for a Today ad placement. * The app must satisfy this or `todayAdPlacementMinimumLandscapeMedia` to be shown. */ export const todayAdPlacementMinimumPortraitMedia = 4; /** * Get a count of the media in each orientation specifically for a Today ad placement. * This is specific to Today because the placement only supports one video. * @param trailers A set of trailers for an app. * @param screenshots A set of screenshots for an app. * @returns A count of media in each orientation, respecting the placement's rules. */ export function getMediaOrientationCountsForTodayPlacement(trailers, screenshots) { var _a, _b, _c, _d; // Grab the first array of platform screenshots and trailers - the ones the ad will display. const platformScreenshotsArtwork = (_b = (_a = screenshots[0]) === null || _a === void 0 ? void 0 : _a.artwork) !== null && _b !== void 0 ? _b : []; const platformTrailersVideos = (_d = (_c = trailers[0]) === null || _c === void 0 ? void 0 : _c.videos) !== null && _d !== void 0 ? _d : []; // Split media into orientations. const portraitScreenshots = platformScreenshotsArtwork.filter((artwork) => artwork.isPortrait()); const landscapeScreenshots = platformScreenshotsArtwork.filter((artwork) => artwork.isLandscape()); // Take a maximum of one video of each orientation. const portraitVideos = platformTrailersVideos.filter((video) => video.preview.isPortrait()).slice(0, 1); const landscapeVideos = platformTrailersVideos.filter((video) => video.preview.isLandscape()).slice(0, 1); const portraitCount = portraitScreenshots.length + portraitVideos.length; const landscapeCount = landscapeScreenshots.length + landscapeVideos.length; return { landscape: landscapeCount, portrait: portraitCount, }; } /** * A convenience method to determine if a position is ad eligible. * @param placementType The type of placement for an ad. * @param locationTracker The location tracker being used to build the items. Used to determine the current shelf and position within the shelf. * @returns A boolean indicating if the current position is considered ad eligible. */ export function isAdEligible(placementType, locationTracker) { var _a; if (isNothing(locationTracker) || isNothing(placementType)) { return false; } const shelfId = (_a = currentLocation(locationTracker)) === null || _a === void 0 ? void 0 : _a.id; if (isNothing(shelfId)) { return false; } const eligibleIndex = adEligibleIndexForType(placementType, shelfId); if (isNothing(eligibleIndex)) { return false; } const index = currentPosition(locationTracker); return index === eligibleIndex; } /** * Inserts a missed opportunity on the correct slot in the page if an ad isn't already present. This looks for * types conforming to SearchAdOpportunityProviding and handles all of the construction for the metadata associated * with a SearchAdOpportunity. * @param objectGraph The object graph. * @param shelves Completed shelves that are constructed for the page. * @param placementType The location where the ad will be shown. * @param shelfIdentifier The specific shelf identifier we should be evaluating against. This is not inferred to support things like Today Page that can use a shelf-per-item. * @param pageInformation Metrics information for the page. */ export function applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, shelves, placementType, shelfIdentifier, pageInformation) { var _a, _b, _c; if (!platformSupportsAdverts(objectGraph) || isNull(pageInformation.iAdInfo)) { return; } const adEligibleIndex = adEligibleIndexForType(placementType, shelfIdentifier); if (isNothing(adEligibleIndex)) { return; } // Opportunities aren't present when we've recorded certain missed opportunities let missedOpportunityReason = null; if (typeof pageInformation.iAdInfo.pageFields.iAdMissedOpportunityReason === "string") { missedOpportunityReason = pageInformation.iAdInfo.pageFields.iAdMissedOpportunityReason; } if (isNothing(missedOpportunityReason) || missedOpportunityReason.length === 0 || missedOpportunityReason === "EDITORIALTAKEOVER" || missedOpportunityReason === "SLPLOAD") { return; } const allShelfItems = []; for (const shelf of shelves) { // searchResult is handled in a separate non-shelf workflow const isValidContentType = shelf.contentType === "smallLockup" || shelf.contentType === "todayCard"; if (!isValidContentType) { continue; } const nextShelfItems = shelf.items; if (isDefinedNonNull(nextShelfItems) && nextShelfItems.length > 0) { allShelfItems.push(...nextShelfItems); } } if (allShelfItems.length <= adEligibleIndex) { return; } const adEligibleShelfItem = allShelfItems[adEligibleIndex]; const isTodayCard = adEligibleShelfItem instanceof TodayCard; const isSmallLockup = adEligibleShelfItem instanceof Lockup; const shelfItemMedia = isTodayCard ? adEligibleShelfItem.media : null; const lockupTypeIsMediumScreenshotFormat = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaMediumLockupWithScreenshots; const lockupTypeIsMediumCreativeFormat = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaMediumLockupWithAlignedRegion; const lockupHasPlacedCondensedTodayAd = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaSingleLockup && isDefinedNonNull(shelfItemMedia.condensedAdLockupWithIconBackground.lockup.searchAdOpportunity); const lockupHasPlacedMediumAdScreenshots = lockupTypeIsMediumScreenshotFormat && isDefinedNonNull(shelfItemMedia.mediumAdLockupWithScreenshotsBackground.lockup.searchAdOpportunity); const lockupHasPlacedMediumAdCreative = lockupTypeIsMediumCreativeFormat && isDefinedNonNull(shelfItemMedia.mediumAdLockupWithAlignedRegionBackground.lockup.searchAdOpportunity); const lockupHasPlacedSmallLockup = isSmallLockup && isDefinedNonNull(adEligibleShelfItem.searchAdOpportunity); // If we've already processed the ad data and placed it, we don't want to indicate that there's a missed opportunity if (lockupHasPlacedCondensedTodayAd || lockupHasPlacedMediumAdScreenshots || lockupHasPlacedMediumAdCreative || lockupHasPlacedSmallLockup) { return; } adEligibleShelfItem.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, pageInformation); (_a = adEligibleShelfItem.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.setMissedOpportunityReason(missedOpportunityReason !== null && missedOpportunityReason !== void 0 ? missedOpportunityReason : "NOAD"); if (lockupTypeIsMediumScreenshotFormat) { (_b = adEligibleShelfItem.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType(getTemplateTypeForMediumAdFromLockupWithScreenshots(shelfItemMedia.mediumAdLockupWithScreenshotsBackground.screenshots[0])); } else if (lockupTypeIsMediumCreativeFormat) { adEligibleShelfItem.searchAdOpportunity.setTemplateType(getTemplateTypeForMediumAdFromLockupWithCustomCreative()); } else { (_c = adEligibleShelfItem.searchAdOpportunity) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); } } /** * This is a bit of a workaround for `isAdEligible` function in this file. Ideally this is bag-driven, but I need to figure out * how to make it work. `shelfIdentifier` doesn't seem to work correctly, except for the YMAL ad on PP * @param placementType The ad placement type to evaluate for ad eligibility * @param shelfIdentifier The identifier for the shelf to evaluate for ad eligibility * @returns The equivalent bag placement value as a string. */ function adEligibleIndexForType(placementType, shelfIdentifier) { var _a; const adEligibleBagRepresentation = { today: [ { shelfIdentifier: "today", adEligibleIndex: 1, }, ], productPageYMAL: [ { shelfIdentifier: "customers-also-bought-apps", adEligibleIndex: 0, }, ], searchLanding: [ { shelfIdentifier: "R8802", adEligibleIndex: 0, }, ], searchResults: [ { shelfIdentifier: "search-results", adEligibleIndex: 0, }, ], }; const matchingSlot = ((_a = adEligibleBagRepresentation[placementType]) !== null && _a !== void 0 ? _a : []).find((element) => { return element.shelfIdentifier === shelfIdentifier; }); if (isDefinedNonNullNonEmpty(matchingSlot) && isDefinedNonNull(matchingSlot.adEligibleIndex)) { return matchingSlot.adEligibleIndex; } else { return undefined; } } // endregion // region iad data export function iadInfoFromOnDeviceAdResponse(objectGraph, placementType, adResponse, flattenedTodayFeed = null) { var _a, _b; if (!platformSupportsAdverts(objectGraph) || isNull(adResponse)) { return null; } return new IAdSearchInformation(objectGraph, placementType, IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, (_a = adResponse === null || adResponse === void 0 ? void 0 : adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo, flattenedTodayFeed), adResponse.iAdId, adResponse.clientRequestId, undefined, (_b = adResponse.onDeviceAd) === null || _b === void 0 ? void 0 : _b.positionInfo); } /** * Key added to `Data`'s `attributes` field so downstream builders can use this field. * This is currently generated by JS when we create `SponsoredSearchAdverts` */ export const instanceIdAttributeKey = "jet_native_advert_instanceid"; /** * Returns the native advert id if one was annotated during `applyNativeAdvertData` */ export function advertInstanceIdForData(objectGraph, data) { return attributeAsString(data, instanceIdAttributeKey); } /** * Decorates `instanceId` that identifies an ad to given `Data` for consumption in builders. */ export function decorateAdInstanceIdOnData(objectGraph, data, instanceId) { if (isDefinedNonNullNonEmpty(data === null || data === void 0 ? void 0 : data.attributes)) { data.attributes[instanceIdAttributeKey] = instanceId; } } // endregion // region ad localization /** * Whether the available localization data in an ad is valid for the defined `adsOverrideLanguage`, if any. * If no `adsOverrideLanguage` is set, `true` is returned. * The logic for a valid set of data differs based on the passed in placement and if an ad display style * is defined (for SRP only). * @param objectGraph The Object Graph. * @param data The app data. * @param onDeviceAd Optionally, an `OnDeviceAdvert` - this is only used as a fallback for SLP ads, * where the meta resource object is attached to the original ad request, but not to the subsequent * hydration request. For this case only, we must use a combination of these two requests to identify * whether the localization is valid. * @param adDisplayStyle: Optionally, an ad display style - this is used for SRP ads, where the data used * in the ad UI depends on which ad display style is being used. * @returns A boolean indicating if the available localization data is valid. */ export function isAdLocalizationValid(objectGraph, data, onDeviceAd, adDisplayStyle) { var _a, _b, _c, _d, _e; const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; // No localization requirements if there is no adsOverrideLanguage. if (isNullOrEmpty(adsOverrideLanguage) || isNullOrEmpty(data)) { return true; } let metaResource = (_a = data.meta) === null || _a === void 0 ? void 0 : _a.resource; // If the `metaResource in `data` is missing, attempt to fall back to the `onDeviceAd` values // which are provided for SLP. if (isNullOrEmpty(metaResource) && isDefinedNonNullNonEmpty(onDeviceAd)) { metaResource = (_e = (_d = (_c = (_b = onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.appMetadata) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.meta) === null || _e === void 0 ? void 0 : _e.resource; } // If `metaResource` still don't exist, we can't validate localization. if (isNullOrEmpty(metaResource)) { // The meta resource attributes object is missing, assume we don't have the correct localizations. return false; } // Title // Title is always used. Check it's localized. const titleLocale = attributeAsString(metaResource, "name.locale"); if (titleLocale !== adsOverrideLanguage) { return false; } // Subtitle // Subtitle is constructed using either the subtitle field, or falling back to the category. // If the subtitle text is present, we need to validate the locale. If not, we don't. const subtitle = contentAttributeAsString(objectGraph, data, "subtitle"); const subtitleLocale = contentAttributeAsString(objectGraph, metaResource, "subtitle.locale"); if (isDefinedNonNullNonEmpty(subtitle) && subtitleLocale !== adsOverrideLanguage) { return false; } // SRP advertising text // SRP ads can optionally provide a 3rd set of text, `advertisingText`. // It's only used when the `adDisplayStyle` is `TEXT`. if (adDisplayStyle === "TEXT" /* SearchAdDisplayStyle.TEXT */) { // The text used can be defined by the ad payload inside the `data`. We need to use that path // to get both the text and the locale for that text. const iAdTextKey = attributeAsString(data, "iad.format.text"); if (isSome(iAdTextKey) && iAdTextKey !== "none") { let attributePath; const attributePathLocale = iAdTextKey; // In the case where the path is "description", we need to use different paths for the text and the locale. // This is due to a Media API limitation where the locale for "description" is just under that key, rather // than "description.standard", which is the actual path for the text content. if (iAdTextKey === "description") { attributePath = "description.standard"; } else { attributePath = iAdTextKey; } const advertisingText = contentAttributeAsString(objectGraph, data, attributePath); const advertisingTextLocale = contentAttributeAsString(objectGraph, metaResource, attributePathLocale.concat(".locale")); if (isDefinedNonNullNonEmpty(advertisingText) && advertisingTextLocale !== adsOverrideLanguage) { return false; } } } return true; } // endregion // region metrics /** * Retrieve the eligible slot positions for a given ad placement type. * @param objectGraph The Object Graph. * @param placementType The placement the ad is for. * @returns An array of eligible slot positions for the provided placement. */ export function eligibleSlotPositionsForAdPlacement(objectGraph, placementType) { if (isNothing(placementType)) { return undefined; } const placementTypeBagValue = adPlacementBagValueForAdPlacementType(placementType); if (isNothing(placementTypeBagValue)) { return undefined; } return objectGraph.bag.adPlacementEligibleSlotPositions[placementTypeBagValue]; } // endregion //# sourceMappingURL=ad-common.js.map