/** * Builder code specific to Ad Lockups */ import { isSome } from "@jet/environment"; import { AdInteractionAction, AdTransparencyAction, CompoundAction, Screenshots, SearchAd, SearchAdOpportunity, } from "../../api/models"; import { AdvertActionMetrics, } from "../../api/models/metrics/advert-action-metrics"; import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, asBooleanOrFalse, asArray, } from "../../foundation/json-parsing/server-data"; import { attributeAsArrayOrEmpty, attributeAsBooleanOrFalse, attributeAsDictionary, attributeAsString, } from "../../foundation/media/attributes"; import { advertInstanceIdForData } from "../ads/ad-common"; import * as content from "../content/content"; import { addClickEventsToAdLockup } from "../metrics/helpers/clicks"; import { addImpressionsFieldsToAd, impressionOptionsForLockup } from "../metrics/helpers/impressions"; import { metricsPlatformDisplayStyleFromData } from "../metrics/helpers/util"; import { appDataHasVariant } from "../product-page/product-page-variants"; import { adLogger } from "../search/search-ads"; import { asString } from "@apple-media-services/media-api"; // region Shared /** * Whether or not data is for ad. * * Note that this attribute *may not be initially part of the original response*. * - Sponsored Search: Since Ad Rotation, `iad` attribute is populated from contents of `iads` dictionary. * - Search Landing: `iad` attribute is populated from contents of `OnDeviceAdvert`. * * @param data Data to check if it is an ad. * @see`iadAttributesForType` and `decorateiAdAttributeFromOnDeviceAd` */ export function isAdvert(objectGraph, data) { const adDictionary = attributeAsDictionary(data, "iad"); return isDefinedNonNullNonEmpty(adDictionary); } // endregion // region Data Overrides /** * Populates Ad-specific customizations for given lockup to a **otherwise fully configured ad lockup** * @param objectGraph The object graph. * @param data Data that will provide the ad data. * @param lockup The lockup to modify. * @param metricsOptions The lockup metrics options. * @param applyAdOfferDisplayProperties Whether to apply the default ad OfferDisplayProperties. Some callers of this function want to enforce their own offer styling. */ export function performAdOverridesforLockup(objectGraph, data, lockup, metricsOptions, applyAdOfferDisplayProperties = true) { // If the lockup is not an ad, it's ad eligible. Just apply the metrics modifications and return early. if (!metricsOptions.isAdvert) { performMetricsOverridesForLockup(objectGraph, data, lockup, metricsOptions); return; } let searchAd; if (objectGraph.props.enabled("advertSlotReporting")) { lockup.searchAdOpportunity = searchAdOpportunityFromData(objectGraph, data, metricsOptions.pageInformation); searchAd = lockup.searchAdOpportunity.searchAd; } else { lockup.searchAd = searchAdFromData(objectGraph, data, metricsOptions.pageInformation); searchAd = lockup.searchAd; } /** * Advert Action Metrics (AzulC+) */ const advertType = content.isArcadeSupported(objectGraph, data) ? "arcadeApp" : "standardApp"; const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation); const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId"); const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder"); const purchaseType = isPreorder ? "preorder" : "standard"; const clickActionAdActionMetrics = new AdvertActionMetrics(searchAd.instanceId, data.id, bundleId, advertType, "advertPressed", purchaseType, reportingDestination); lockup.clickAction = attachAdActionMetricsToAction(objectGraph, lockup.clickAction, clickActionAdActionMetrics); const buttonActionAdActionMetrics = new AdvertActionMetrics(searchAd.instanceId, data.id, bundleId, advertType, "offerButtonPress", purchaseType, reportingDestination); lockup.buttonAction = attachAdActionMetricsToAction(objectGraph, lockup.buttonAction, buttonActionAdActionMetrics); lockup.itemBackground = objectGraph.props.enabled("insetAdItemBackground") ? "insetAd" : "ad"; // Offer button style if (lockup.offerDisplayProperties && applyAdOfferDisplayProperties) { lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "colored", "ad"); } // Rating if (!attributeAsBooleanOrFalse(data, "iad.format.userRating")) { lockup.rating = null; lockup.ratingCount = null; } performMetricsOverridesForLockup(objectGraph, data, lockup, metricsOptions); } /** * Whether or not an advert is eligible to use CPP deeplinks, regardless of whether or not one exists * @param data Data that will provide the ad data. */ export function isCppDeeplinkEnabledForAdvert(data) { const iAd = attributeAsDictionary(data, "iad"); const searchAdCppDeepLinkEnabled = asBooleanOrFalse(iAd, "passthroughAdInfo.deepLinkEligible"); const targetedAdCppDeeplinkingEnabled = asBooleanOrFalse(data, "meta.passthroughAdInfo.deepLinkEligible"); return searchAdCppDeepLinkEnabled || targetedAdCppDeeplinkingEnabled; } /** * Whether or not a custom creative advert is eligible to use CPP deeplinks, regardless of whether or not one exists * @param data Data that will provide the ad data. */ export function isCustomCreativeDeeplinkEnabledForAdvert(data) { const extraAdInfo = getExtraAdInfo(data); if (isSome(extraAdInfo)) { return asBooleanOrFalse(extraAdInfo, "passthroughAdInfo.deepLinkEligible"); } else { return asBooleanOrFalse(data, "meta.passthroughAdInfo.deepLinkEligible"); } } /** * The custom creative ad deeplink url, if there is one. * @param data Data that will provide the ad data. */ export function getCustomCreativeDeepLinkUrl(data) { const extraAdInfo = getExtraAdInfo(data); const creativeDetails = asArray(extraAdInfo, "creativeDetails"); if (isSome(creativeDetails)) { return asString(creativeDetails[0], "deepLink"); } else { return asString(data, "meta.alignedRegionDetails.deepLink"); } } /** * The tapDestination of the custom creative advert which is also the cppId for the PP. * @param data Data that will provide the ad data. */ export function getTapDestinationIdForAdvert(data) { const extraAdInfo = getExtraAdInfo(data); const creativeDetails = asArray(extraAdInfo, "creativeDetails"); if (isSome(creativeDetails)) { return asString(creativeDetails[0], "tapDestination"); } else { return asString(data, "meta.alignedRegionDetails.tapDestination"); } } /** * Local function to extract the extraAdInfo from the meta in the data. * @param data Data that will provide the ad data. */ function getExtraAdInfo(data) { const extraAdInfoString = asString(data, "meta.extraAdInfo"); if (isSome(extraAdInfoString)) { return JSON.parse(extraAdInfoString); } return null; } /** * Perform overrides for metrics for Ad Lockups * @param data Data for ad lockup * @param lockup Lockup to apply overrides to * @param baseMetricsOptions Base metrics options to extend. */ function performMetricsOverridesForLockup(objectGraph, data, lockup, baseMetricsOptions) { const platformDisplayStyle = metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, null); const impressionsMetricOptions = impressionOptionsForLockup(objectGraph, data, lockup, platformDisplayStyle, baseMetricsOptions, true); addClickEventsToAdLockup(objectGraph, lockup, impressionsMetricOptions); addImpressionsFieldsToAd(objectGraph, lockup, impressionsMetricOptions, impressionsMetricOptions.pageInformation.iAdInfo); } /** * Creates a template type string for the medium ad format * @param lockup The lockup whose artwork to use for the template type string * @returns The native ad template type for the medium format ad */ export function getTemplateTypeForMediumAdFromLockupWithScreenshots(screenshots) { var _a; const templateString = [ "MEDRIVER_", "U", "I", ((_a = screenshots === null || screenshots === void 0 ? void 0 : screenshots.artwork) !== null && _a !== void 0 ? _a : []).length, // Count of assets used. ].join(""); return templateString; } /** * Creates a template type string for the medium ad format * @param lockup The lockup whose artwork to use for the template type string * @returns The native ad template type for the medium format ad */ export function getTemplateTypeForMediumAdFromLockupWithCustomCreative() { const templateString = [ "MEDRIVER_", "U", "I", 1, "_2x1", // Aspect Ratio used. ].join(""); return templateString; } /** * Create an opportunity for an ad model * @param objectGraph The object graph. * @param data Data to build `SearchAd` for. * @param pageInformation Metrics page information. * @returns The search ad opportunity represented in the data */ export function searchAdOpportunityFromData(objectGraph, data, pageInformation) { const searchAd = searchAdFromData(objectGraph, data, pageInformation); const lifecycleEventPayloads = searchAdLifecycleEventPayloads(searchAd.instanceId, pageInformation); return new SearchAdOpportunity(searchAd.instanceId, lifecycleEventPayloads, searchAd); } /** * Create a missed opportunity for an ad model. This differentiates from `searchAdOpportunityFromData` because the * opportunity is represented by an organic content that is in an ad eligible position. * @param objectGraph The object graph. * @param pageInformation Metrics page information. * @returns The search ad opportunity for the missed opportunity */ export function searchAdMissedOpportunityFromId(objectGraph, pageInformation) { let adInstanceId; const placementType = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo.placementType; if (isDefinedNonNull(placementType) && objectGraph.props.enabled("advertSlotReporting")) { try { adInstanceId = objectGraph.ads.getIdentifierForMissedOpportunity(placementType); } catch { adInstanceId = objectGraph.random.nextUUID(); adLogger(objectGraph, `Error: getIdentifierForMissedOpportunity threw exception. Assigned ${adInstanceId}`); } } else { adInstanceId = objectGraph.random.nextUUID(); adLogger(objectGraph, `Error: placementType was null or empty. Assigned ${adInstanceId}`); } const lifecycleEventPayloads = searchAdLifecycleEventPayloads(adInstanceId, pageInformation); return new SearchAdOpportunity(adInstanceId, lifecycleEventPayloads); } /** * Create the backing search ad model for any representation of ad. * @param objectGraph The object graph. * @param data Data to build `SearchAd` for. * @param pageInformation Information of the page hosting the lockup * @returns The search ad model */ function searchAdFromData(objectGraph, data, pageInformation) { let instanceId = advertInstanceIdForData(objectGraph, data); if (isNull(instanceId) || instanceId.length === 0) { instanceId = objectGraph.random.nextUUID(); adLogger(objectGraph, `Error: instanceId was null or empty. Assigned ${instanceId}`); } const iAd = attributeAsDictionary(data, "iad"); const impressionId = attributeAsString(data, "iad.impressionId"); const transparencyData = attributeAsString(data, "iad.privacy"); const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId"); const transparencyAction = new AdTransparencyAction(transparencyData); transparencyAction.title = objectGraph.adsLoc.string("IAD_PRIVACY_MARKER_BUTTON_TITLE"); const advertType = content.isArcadeSupported(objectGraph, data) ? "arcadeApp" : "standardApp"; const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder"); const purchaseType = isPreorder ? "preorder" : "standard"; const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, pageInformation); const adActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "markerPress", purchaseType, reportingDestination); const lifecycleEventPayloads = searchAdLifecycleEventPayloads(instanceId, pageInformation); const action = attachAdActionMetricsToAction(objectGraph, transparencyAction, adActionMetrics); return new SearchAd(instanceId, iAd, lifecycleEventPayloads, impressionId, action); } /** * Create the backing metadata for any representation of ad opportunity. * @param instanceId The identifier for the ad opportunity we're tracking * @param pageInformation Information of the page hosting the lockup * @returns Payloads to be sent to PromotedContent */ export function searchAdLifecycleEventPayloads(instanceId, pageInformation) { const pageIdentifierRaw = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.baseFields.pageId; const pageIdentifier = typeof pageIdentifierRaw === "string" ? pageIdentifierRaw : "unknown"; return { placed: { adInstanceId: instanceId, pageIdentifier: pageIdentifier, }, pageEnter: { pageIdentifier: pageIdentifier, }, pageExit: { pageIdentifier: pageIdentifier, }, onScreen: { adInstanceId: instanceId, }, offScreen: { adInstanceId: instanceId, }, visible: { adInstanceId: instanceId, }, completed: { adInstanceId: instanceId, }, }; } /** * Populates Ad-specific customizations for given card to an otherwise fully configured Today card * @param objectGraph The object graph. * @param data Data that will provide the ad data. * @param card The Today card to modify. * @param metricsOptions The card metrics options. */ export function performAdOverridesForCard(objectGraph, data, card, metricsOptions) { let instanceId = advertInstanceIdForData(objectGraph, data); if (isNull(instanceId) || instanceId.length === 0) { instanceId = objectGraph.random.nextUUID(); adLogger(objectGraph, `Error: instanceId was null or empty. Assigned ${instanceId}`); } const advertType = content.isArcadeSupported(objectGraph, data) ? "arcadeApp" : "standardApp"; const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation); const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId"); const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder"); const purchaseType = isPreorder ? "preorder" : "standard"; const adActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "advertPressed", purchaseType, reportingDestination); card.clickAction = attachAdActionMetricsToAction(objectGraph, card.clickAction, adActionMetrics); } /** * Attaches `AdActionMetrics` to an existing action enabling native ad interaction instrumentation. * This works by wrapping the existing action, along with a new `AdInteractionAction`, in a `CompoundAction`, which are executed at the same time. * * @param objectGraph The Object Graph. * @param existingAction The existing Action to wrap. * @param adActionMetrics The adActionMetrics object for the `AdInteractionAction`. * @returns The new Action containing the existing and new Actions. */ export function attachAdActionMetricsToAction(objectGraph, existingAction, adActionMetrics) { const adInteractionAction = new AdInteractionAction(adActionMetrics); const action = new CompoundAction([adInteractionAction, existingAction]); // Re-use the title from the existing action. action.title = existingAction.title; return action; } // endregion // region Asset Overrides /** * Performs iAd Asset overrides, where a field dictates which assets to use * @param data The data to source overrides from * @param lockup The lockup to modify * @param metricsOptions The metrics options that this operation has side effects on. */ export function performAssetOverridesForMixedMediaAdLockupIfNeeded(objectGraph, data, lockup, metricsOptions) { if (!shouldPerformAssetOverridesForData(objectGraph, data)) { return; } const iAdAssetOverrides = attributeAsArrayOrEmpty(data, "iad.assetOverride"); if (iAdAssetOverrides.length) { const didApplyAssetOverrides = applyAssetOverridesToMixedMediaAdLockup(objectGraph, lockup, iAdAssetOverrides); if (metricsOptions.pageInformation.iAdInfo && metricsOptions.pageInformation.iAdInfo.iAdIsPresent) { if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { metricsOptions.pageInformation.iAdInfo.setSpecifiedAlignedRegionUsed(didApplyAssetOverrides); } } } } /** * Whether or not an asset override should occur. * This is used to control specific scenarios when asset overrides should not occur, e.g. when it clashes with other features. * * @param data The apps resource to determine if asset overrides should be done for. */ function shouldPerformAssetOverridesForData(objectGraph, data) { /** * If an AB Test is being run on given app, don't accept the override. */ const appHasABTest = appDataHasVariant(objectGraph, data, "abExperiment"); return !appHasABTest; } /** * Apply asset overrides to the ad lock up, based on a given list of overrides * @param lockup Lockup to modify * @param iAdAssetOverrides Asset overrides specified * @return {boolean} - Return whether or not the overrides were applied */ function applyAssetOverridesToMixedMediaAdLockup(objectGraph, lockup, iAdAssetOverrides) { const unusedAssetOverrides = new Set(iAdAssetOverrides); const videoOverrides = []; const screenshotOverrides = []; const isOverrideMedia = function (url, checksum) { if (unusedAssetOverrides.size === 0) { return false; } for (const assetOverride of iAdAssetOverrides) { if (assetOverride === checksum || url.indexOf(assetOverride) !== -1) { unusedAssetOverrides.delete(assetOverride); return true; } } return false; }; if (iAdAssetOverrides.length && (lockup.screenshots.length || lockup.trailers.length)) { if (lockup.trailers.length) { for (const video of lockup.trailers[0].videos) { if (isOverrideMedia(video.videoUrl, video.preview.checksum)) { videoOverrides.push(video); } } } if (lockup.screenshots.length) { for (const screenshot of lockup.screenshots[0].artwork) { if (isOverrideMedia(screenshot.template, screenshot.checksum)) { screenshotOverrides.push(screenshot); } } } } if (unusedAssetOverrides.size === 0 && (videoOverrides.length || screenshotOverrides.length)) { if (lockup.trailers.length) { lockup.trailers[0].videos = videoOverrides; } if (lockup.screenshots.length) { lockup.screenshots[0] = new Screenshots(screenshotOverrides, lockup.screenshots[0].mediaPlatform); } return true; } else { return false; } } // endregion // region Reporting Destionation /** * Determine the reporting destination based on metrics options */ export function reportingDestinationFromMetricsOptions(objectGraph, pageInformation) { const iadInfo = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo; if (isNull(iadInfo)) { return "undefined"; } const placementType = iadInfo.placementType; switch (placementType) { case "searchLanding": return "promotedContent"; // SLP uses PC-backed Journey reporting case "searchResults": return "searchAds"; // SRP uses SA-backed Journey reporting case "today": case "productPageYMAL": case "productPageYMALDuringDownload": return "promotedContent"; // Chainlink uses PC-backed Journey reporting default: throw new Error(`This method should never be called with value: ${placementType}`); } } // endregion //# sourceMappingURL=ad-lockups.js.map