diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js | 1920 |
1 files changed, 1920 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js new file mode 100644 index 0000000..1de013a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js @@ -0,0 +1,1920 @@ +// +// lockups.ts +// AppStoreKit +// +// Created by Jonathan Ellenbogen on 11/15/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as validation from "@jet/environment/json/validation"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { makeBundlePageIntent } from "../../api/intents/bundle-page-intent"; +import { makeEditorialPageIntentByID } from "../../api/intents/editorial/editorial-page-intent"; +import { makeGroupingPageIntentByID } from "../../api/intents/grouping-page-intent"; +import { makeProductPageIntent } from "../../api/intents/product-page-intent"; +import { makeRoutableArticlePageIntent, } from "../../api/intents/routable-article-page-intent"; +import * as models from "../../api/models"; +import { AdvertActionMetrics, } from "../../api/models/metrics/advert-action-metrics"; +import * as productPageUtil from "../../common/product-page/product-page-util"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { editorialCardFromData } from "../../foundation/media/associations"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import { platformAttributeAsDictionary } from "../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as mediaUrlBuilder from "../../foundation/media/url-builder"; +import { BuyParameters } from "../../foundation/metrics/buy-parameters"; +import * as http from "../../foundation/network/http"; +import { InAppPurchaseInstallPageParameters, Parameters, Path, ProductPageParameters, Protocol, } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as color from "../../foundation/util/color-util"; +import * as objects from "../../foundation/util/objects"; +import * as gameModels from "../../gameservicesui/src/api/data-models/game"; +import { makeClickMetrics } from "../../gameservicesui/src/utility/metrics"; +import * as adCommon from "../ads/ad-common"; +import * as appEvent from "../app-promotions/app-event"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import * as arcadeCommon from "../arcade/arcade-common"; +import * as arcadeUpsell from "../arcade/arcade-upsell"; +import * as mediaUrlMapping from "../builders/url-mapping"; +import * as ageRatings from "../content/age-ratings"; +import { createArtworkForResource } from "../content/artwork/artwork"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as flowPreview from "../content/flow-preview"; +import { isMediaDark } from "../editorial-pages/editorial-media-util"; +import * as editorialComponentMediaUtil from "../editorial-pages/editorial-page-component-media-util"; +import { makeEditorialPageURL } from "../editorial-pages/editorial-page-intent-controller-utils"; +import * as filtering from "../filtering"; +import { makeGroupingPageCanonicalURL } from "../grouping/grouping-page-url"; +import * as externalDeepLink from "../linking/external-deep-link"; +import { getLocale } from "../locale"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersModels from "../metrics/helpers/models"; +import * as metricsUtil from "../metrics/helpers/util"; +import * as offerFormatting from "../offers/offer-formatting"; +import * as offers from "../offers/offers"; +import { getPlatform, inferPreviewPlatformFromDeviceFamilies } from "../preview-platform"; +import * as variants from "../product-page/product-page-variants"; +import * as reviews from "../product-page/reviews"; +import { customCreativeArtworkFromData, customCreativeVideoFromData } from "../search/custom-creative"; +import * as metadataRibbon from "../search/metadata-ribbon/metadata-ribbon"; +import * as searchTagsRibbon from "../search/metadata-ribbon/search-tags-ribbon"; +import * as sharing from "../sharing"; +import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils"; +import * as adLockup from "./ad-lockups"; +import { reportingDestinationFromMetricsOptions } from "./ad-lockups"; +export const forTesting = { + copyDataIntoLockup, +}; +export function offerTypeForMediaType(objectGraph, type, supportsArcade) { + switch (type) { + case "in-apps": + return supportsArcade ? "arcade" : "inAppPurchase"; + default: + return supportsArcade ? "arcadeApp" : "app"; + } +} +function isArcadeLockupWordmarkSupported(objectGraph) { + if (objectGraph.client.isTV && objectGraph.host.clientIdentifier === "com.apple.Arcade") { + return false; + } + else { + return true; + } +} +/** + * Takes the metadata ribbon items type candidates from the `SearchExperimentData` and determines which should be shown in each slot, + * prioritizing the natural order from the candidates arrays + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The data representing the mixed media lockup data + * @param lockup The lockup object the metadata object will be added to + * @param searchExperimentData The experiment data for the search experiments, to use its displayStyle object + */ +function copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentDataForPage, options) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + if (serverData.isNullOrEmpty(data)) { + return; + } + lockup.showMetadataInformationInLockup = objectGraph.bag.isLLMSearchTagsEnabled || objectGraph.client.isPad; + /// This needs to be guarded against iPhone as natively this will affect all iOS UI components, including iPad. + if (!objectGraph.client.isPhone) { + return; + } + /// If the individual lockup has a metadata ribbon field, that takes precedence over the page level metadata ribbon + const searchExperimentDataForLockup = serverData.asDictionary(data, "meta"); + const metadataRibbonItemSlots = (_b = (_a = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _a === void 0 ? void 0 : _a.metadataRibbon) !== null && _b !== void 0 ? _b : (_c = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _c === void 0 ? void 0 : _c.metadataRibbon; + let llmMetadataRibbonItems = []; + if (objectGraph.bag.isLLMSearchTagsEnabled) { + llmMetadataRibbonItems = + (_g = (_e = (_d = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _d === void 0 ? void 0 : _d.llmRibbon) !== null && _e !== void 0 ? _e : (_f = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _f === void 0 ? void 0 : _f.metadataRibbon) !== null && _g !== void 0 ? _g : (_h = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _h === void 0 ? void 0 : _h.metadataRibbon; + } + else { + llmMetadataRibbonItems = + (_k = (_j = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _j === void 0 ? void 0 : _j.metadataRibbon) !== null && _k !== void 0 ? _k : (_l = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _l === void 0 ? void 0 : _l.metadataRibbon; + } + const metadataRibbonItems = metadataRibbon.createMetadataRibbonItemsForLockup(objectGraph, data, lockup, metadataRibbonItemSlots, options); + const searchTagsRibbonItems = searchTagsRibbon.createSearchTagsRibbonItemsForLockup(objectGraph, data, lockup, llmMetadataRibbonItems, options); + lockup.metadataRibbonItems = metadataRibbonItems; + lockup.searchTagRibbonItems = searchTagsRibbonItems; + lockup.shouldEvenlyDistributeRibbonItems = !objectGraph.bag.isLLMSearchTagsEnabled; +} +/** + * Configures all the data into the lockup object + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The branch IAP used by the contingent offer + * @param lockup The lockup object the metadata object will be added to + * @param options Lockup configuration options + * @param isParentAppFree A flag used when creating an IAP lockup with missing parent app data in its relationships property. + */ +function copyDataIntoLockup(objectGraph, data, lockup, options, parentData, copyAdditionalDataIntoLockup) { + if (!data) { + return; + } + validation.context("copyDataIntoLockup", () => { + var _a, _b, _c, _d, _e, _f; + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + // Drop this item if it's not a preorder, but contained in a pre-order exclusive shelf. + if (options.isContainedInPreorderExclusiveShelf && !isPreorder) { + return null; + } + const attributePlatform = (_a = options === null || options === void 0 ? void 0 : options.attributePlatformOverride) !== null && _a !== void 0 ? _a : contentAttributes.defaultAttributePlatform(objectGraph); + const productVariantData = variants.productVariantDataForData(objectGraph, data); + lockup.productVariantID = variants.productVariantIDForVariantData(productVariantData); + options.metricsOptions.productVariantData = productVariantData; + lockup.adamId = data.id; + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.bundleId = bundleId; + lockup.decorations = []; + let clientIdentifierOverride = null; + if (options && options.clientIdentifierOverride) { + clientIdentifierOverride = options.clientIdentifierOverride; + } + lockup.icon = content.iconFromData(objectGraph, data, { + useCase: options.artworkUseCase, + withJoeColorPlaceholder: options.useJoeColorIconPlaceholder, + joeColorPlaceholderSelectionLogic: options.joeColorPlaceholderSelectionLogic, + overrideTextColorKey: options.overrideArtworkTextColorKey, + }, clientIdentifierOverride, productVariantData, options.attributePlatformOverride); + if (options && options.titleObjectPath) { + lockup.title = contentAttributes.contentAttributeAsString(objectGraph, data, options.titleObjectPath, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + } + else { + lockup.title = mediaAttributes.attributeAsString(data, "name"); + } + if (objectGraph.client.isWeb) { + lockup.isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible"); + } + // Only use an ad override language if this is an ad. + lockup.useAdsLocale = options.metricsOptions.isAdvert && isSome(objectGraph.bag.adsOverrideLanguage); + // Don't display "Apple Arcade" on a lockup if it's not an arcade app or we're running in the + // apple arcade app (tvOS) + const isArcadeLockup = content.isArcadeSupported(objectGraph, data, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + if (isArcadeLockup && isArcadeLockupWordmarkSupported(objectGraph) && !options.shouldHideArcadeHeader) { + // tvOS uses a 'pill' decoration over artwork to display apple arcade + // while everywhere else just uses the heading text slot to display a string + if (objectGraph.client.isTV) { + lockup.decorations.push("arcade"); + } + else { + lockup.heading = options.metricsOptions.isAdvert + ? objectGraph.adsLoc.string("Lockup.Heading.Arcade") + : objectGraph.loc.string("Lockup.Heading.Arcade"); + } + } + if (options.shouldShowFriendsPlayingShowcase) { + lockup.decorations.push("showcaseFriendsPlaying"); + } + // Subtitle + const allowMultilineTertiaryLabel = !isArcadeLockup && !isPreorder && ((_b = options.isMultilineTertiaryTitleAllowed) !== null && _b !== void 0 ? _b : true); + if (!options.isSubtitleHidden && !isBadgeMultilineFromData(objectGraph, data, allowMultilineTertiaryLabel)) { + lockup.subtitle = subtitleFromData(objectGraph, data, options); + } + // Tertiary badge + lockup.tertiaryTitle = badgeFromData(objectGraph, data, allowMultilineTertiaryLabel, options.hideCompatibilityBadge); + lockup.tertiaryTitleAction = badgeActionFromData(objectGraph, data); + lockup.tertiaryTitleArtwork = badgeArtworkFromData(objectGraph, data); + lockup.developerTagline = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.editorialTagline = content.notesFromData(objectGraph, data, "tagline", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.editorialDescription = content.notesFromData(objectGraph, data, "standard", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.shortEditorialDescription = content.notesFromData(objectGraph, data, "short", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.ageRating = ageRatings.name(objectGraph, data, true); + lockup.productDescription = contentAttributes.contentAttributeAsString(objectGraph, data, "description.standard", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + if ((_c = options === null || options === void 0 ? void 0 : options.shouldShowSupportedPlatformLabel) !== null && _c !== void 0 ? _c : false) { + copySupportedPlatformLabelIntoLockup(objectGraph, data, lockup, options); + } + if (!reviews.shouldSuppressReviews(objectGraph, data)) { + const ratingCount = mediaAttributes.attributeAsNumber(data, "userRating.ratingCount"); + if (ratingCount > 0 || !(options && options.hideZeroRatings)) { + lockup.rating = mediaAttributes.attributeAsNumber(data, "userRating.value"); + const count = mediaAttributes.attributeAsNumber(data, "userRating.ratingCount"); + const adsOverrideLanguage = options.metricsOptions.isAdvert + ? objectGraph.bag.adsOverrideLanguage + : null; + lockup.ratingCount = objectGraph.loc.formattedCountForPreferredLocale(objectGraph, count, adsOverrideLanguage); + } + } + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, options.metricsOptions, options.metricsClickOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsClickOptions, lockup.title); + const offerData = offers.offerDataFromData(objectGraph, data, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + const includeBetaApps = (_d = options === null || options === void 0 ? void 0 : options.includeBetaApps) !== null && _d !== void 0 ? _d : false; + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, clientIdentifierOverride); + const offerButtonMetricsClickOptions = objects.shallowCopyOf(metricsClickOptions); + const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, includeBetaApps, metricsPlatformDisplayStyle, offerButtonMetricsClickOptions, "default", options === null || options === void 0 ? void 0 : options.referrerData); + const allOfferDiscountData = serverData.asArrayOrEmpty(offerData, "discounts"); + let buttonAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, offerButtonMetricsClickOptions, "default", clientIdentifierOverride, options.shouldNavigateToProductPage); + const cppDeeplinkUrl = contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "customDeepLink", attributePlatform); + const isAdvert = adLockup.isAdvert(objectGraph, data); + // CPP deeplinks used for ads have a requirement to not be used when targeting criteria is too narrow. + const isCppDeeplinkingEnabled = !isAdvert || adLockup.isCppDeeplinkEnabledForAdvert(data); + const hasCppDeepLink = isCppDeeplinkingEnabled && (cppDeeplinkUrl === null || cppDeeplinkUrl === void 0 ? void 0 : cppDeeplinkUrl.length) > 0; + const hasExternalDeepLink = ((_e = options === null || options === void 0 ? void 0 : options.externalDeepLinkUrl) === null || _e === void 0 ? void 0 : _e.length) > 0; + let deepLinkUrl; + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const customCreativeDeepLinkUrl = adLockup.getCustomCreativeDeepLinkUrl(data); + const isCustomCreativeDeeplinkingEnabled = isAdvert && adLockup.isCustomCreativeDeeplinkEnabledForAdvert(data); + const hasCustomCreativeDeepLink = isCustomCreativeDeeplinkingEnabled && (customCreativeDeepLinkUrl === null || customCreativeDeepLinkUrl === void 0 ? void 0 : customCreativeDeepLinkUrl.length) > 0; + if (hasCustomCreativeDeepLink) { + deepLinkUrl = customCreativeDeepLinkUrl; + } + else if (hasCppDeepLink) { + deepLinkUrl = cppDeeplinkUrl; + } + else if (hasExternalDeepLink) { + deepLinkUrl = options.externalDeepLinkUrl; + } + } + else if (hasCppDeepLink || hasExternalDeepLink) { + deepLinkUrl = hasCppDeepLink ? cppDeeplinkUrl : options.externalDeepLinkUrl; + } + } + else if (hasCppDeepLink || hasExternalDeepLink) { + deepLinkUrl = hasCppDeepLink ? cppDeeplinkUrl : options.externalDeepLinkUrl; + } + if (isSome(deepLinkUrl)) { + // Configure cross link as well as deep link action. + buttonAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, buttonAction, offerAction.adamId, bundleId, deepLinkUrl, includeBetaApps, offerButtonMetricsClickOptions); + // Configure cross link title and subtitle. + if (((_f = options.crossLinkSubtitle) === null || _f === void 0 ? void 0 : _f.length) > 0) { + lockup.crossLinkTitle = objectGraph.loc.uppercased(mediaAttributes.attributeAsString(data, "name")); + lockup.crossLinkSubtitle = options.crossLinkSubtitle; + } + } + lockup.buttonAction = buttonAction; + // Beta apps + lockup.includeBetaApps = includeBetaApps; + lockup.developerName = mediaAttributes.attributeAsString(data, "artistName"); + if (isNothing(lockup.developerName)) { + // Some newer APIs use `developerName` instead of `artistName`. + lockup.developerName = mediaAttributes.attributeAsString(data, "developerName"); + } + // Ensure we grab its children. + lockup.children = childrenFromLockupData(objectGraph, data, options); + if (isSome(copyAdditionalDataIntoLockup)) { + copyAdditionalDataIntoLockup(); + } + metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker); + const shareSheetData = sharing.shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride); + if (shareSheetData) { + const shareMenuAction = new models.BlankAction(); + const shareMetricsOptions = objects.shallowCopyOf(metricsClickOptions); + shareMetricsOptions.actionType = "share"; + shareMetricsOptions.targetType = "lockup"; + metricsHelpersClicks.addClickEventToAction(objectGraph, shareMenuAction, shareMetricsOptions); + const shareMenuData = new models.LockupContextMenuData(); + shareMenuData.shareAction = shareMenuAction; + shareMenuData.shareSheetData = shareSheetData; + lockup.contextMenuData = shareMenuData; + } + const resolvedParentData = parentData !== null && parentData !== void 0 ? parentData : parentDataFromInAppData(objectGraph, data); + let isParentAppFree = false; + if (resolvedParentData) { + const parentOfferData = offers.offerDataFromData(objectGraph, resolvedParentData); + const parentPrice = offers.priceFromOfferData(objectGraph, parentOfferData); + isParentAppFree = !(parentPrice > 0); + } + const offerType = offerTypeForMediaType(objectGraph, data.type, isArcadeLockup); + if (options) { + lockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, data, isPreorder, options.isContainedInPreorderExclusiveShelf, options.offerStyle, options.offerEnvironment, allOfferDiscountData[0], isParentAppFree, "default", options.shouldNavigateToProductPage, options.metricsOptions.isAdvert, null, options.parentAppData, options.isBuyDisallowed); + } + else { + lockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, data, isPreorder, options.isContainedInPreorderExclusiveShelf, null, null, allOfferDiscountData[0], isParentAppFree, "default"); + } + if (!options || !options.skipDefaultClickAction) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + lockup.clickAction = productActionFromData(objectGraph, data, metricsClickOptions, { + clientIdentifierOverride: clientIdentifierOverride, + productVariantData: productVariantData, + alignedRegionDeepLinkUrl: adLockup.getCustomCreativeDeepLinkUrl(data), + isCppDeepLinkEligible: isCppDeeplinkingEnabled, + }); + } + else { + lockup.clickAction = productActionFromData(objectGraph, data, metricsClickOptions, { + clientIdentifierOverride: clientIdentifierOverride, + productVariantData: productVariantData, + isCppDeepLinkEligible: isCppDeeplinkingEnabled, + }); + } + } + if (options && options.ordinal) { + lockup.ordinal = options.ordinal; + } + const editorialBadgeInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialBadgeInfo", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (editorialBadgeInfo && !supportsVisionOSCompatibleIOSBinary) { + const badgeType = serverData.asString(editorialBadgeInfo, "editorialBadgeType"); + const hasEditorsChoiceBadge = badgeType && badgeType === "editorialPriority"; + lockup.isEditorsChoice = hasEditorsChoiceBadge; + } + // Flow preview actions + if (!isAdvert) { + lockup.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, false, clientIdentifierOverride, lockup.clickAction, options.metricsOptions, metricsClickOptions, options.externalDeepLinkUrl, lockup.subtitle, lockup.title); + } + const metricsImpressionOptions = metricsHelpersImpressions.impressionOptionsForLockup(objectGraph, data, lockup, metricsPlatformDisplayStyle, options.metricsOptions, options.canDisplayArcadeOfferButton); + metricsHelpersImpressions.addImpressionFields(objectGraph, lockup, metricsImpressionOptions); + }); +} +/** + * Configures the tertiary icons and label for showing unsupported apps in a bundle. + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The branch IAP used by the contingent offer + * @param lockup The lockup object the metadata object will be added to + * @param options Lockup configuration options + */ +function copySupportedPlatformLabelIntoLockup(objectGraph, data, lockup, options) { + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac); + const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + const isBuyable = content.buyableOnDevice(objectGraph, data, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleBinary); + /// If its buyable no need to display compatibility label + if (isBuyable) { + return; + } + const additionalPlatforms = appPlatforms.filter((platform) => platform !== objectGraph.client.deviceType); + if (additionalPlatforms.length === 0) { + return; + } + lockup.tertiaryTitleIcons = additionalPlatforms.map((platform) => content.systemImageNameForAppPlatform(platform)); + if (additionalPlatforms.length === 1) { + const platformTitle = content.appPlatformTitle(objectGraph, additionalPlatforms[0]); + lockup.tertiaryTitle = objectGraph.loc + .string("AppStore.Bundles.ProductPage.OnlyAvailable.Message") + .replace("@@platform@@", platformTitle); + } + else { + // We only support showing up to 2 platforms. Grab the first 2 + const platformTitle1 = content.appPlatformTitle(objectGraph, additionalPlatforms[0]); + const platformTitle2 = content.appPlatformTitle(objectGraph, additionalPlatforms[1]); + lockup.tertiaryTitle = objectGraph.loc + .string("AppStore.Bundles.ProductPage.AvailableOnTwo.Message") + .replace("@@platform1@@", platformTitle1) + .replace("@@platform2@@", platformTitle2); + } +} +export function childrenFromLockupData(objectGraph, data, options) { + const childrenRelationship = mediaRelationship.relationship(data, "apps"); + if (childrenRelationship) { + const listOptions = { + lockupOptions: { + ...options, + shouldCreateScreenshotsLockup: options === null || options === void 0 ? void 0 : options.shouldIncludeScreenshotsForChildren, + }, + filter: 0 /* filtering.Filter.None */, + }; + return lockupsFromDataContainer(objectGraph, childrenRelationship, listOptions); + } + return null; +} +function copyDataIntoInAppPurchaseLockup(objectGraph, data, lockup, options) { + if (!data) { + return; + } + validation.context("copyDataIntoInAppPurchaseLockup", () => { + var _a; + const parentData = (_a = parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : options.parentAppData; + const isStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(parentData, "supportsStreamlinedBuy"); + const iapData = iapDataFromData(objectGraph, data); + data = iapData; + copyDataIntoLockup(objectGraph, data, lockup, options, parentData); + lockup.productIdentifier = mediaAttributes.attributeAsString(data, "offerName"); + lockup.parent = lockupFromData(objectGraph, parentData, options); + lockup.description = mediaAttributes.attributeAsString(data, "description.standard"); + lockup.isVisibleByDefault = mediaAttributes.attributeAsBooleanOrFalse(data, "isMerchandisedVisibleByDefault"); + lockup.isSubscription = mediaAttributes.attributeAsBooleanOrFalse(data, "isSubscription"); + const offerData = offers.offerDataFromData(objectGraph, data); + const discountOfferData = serverData.asArrayOrEmpty(offerData, "discounts"); + lockup.offerDisplayProperties.hasDiscount = discountOfferData.length > 0; + lockup.offerDisplayProperties.subscriptionFamilyId = mediaAttributes.attributeAsString(data, "subscriptionFamilyId"); + // Action for App Install (needed in case the parent app is not installed) + const installRequiredAction = new models.FlowAction("inAppPurchaseInstall"); + installRequiredAction.presentationContext = "presentModalFormSheet"; + const installRequiredActionUrl = configureIAPInstallPageUrl(objectGraph, lockup.adamId, parentData.id); + installRequiredAction.pageUrl = installRequiredActionUrl; + // Install Page + const sidepackInstallPage = new models.InAppPurchaseInstallPage(); + sidepackInstallPage.parentLockup = objects.shallowCopyOf(lockup.parent); + sidepackInstallPage.lockup = objects.shallowCopyOf(lockup); + sidepackInstallPage.preInstallOfferDescription = offerFormatting.installPagePreInstallTrialDescription(objectGraph, offerData); + installRequiredAction.pageData = sidepackInstallPage; + const productIdentifier = mediaAttributes.attributeAsString(data, "offerName"); + const parentBundleId = contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId"); + const firstVersionSupportingMerchIAP = mediaAttributes.attributeAsString(parentData, "firstVersionSupportingInAppPurchaseApi"); + const hasDiscountedOffer = serverData.isDefinedNonNullNonEmpty(discountedOfferFromData(data)); + const metricsOptions = metricsHelpersImpressions.impressionOptionsForLockup(objectGraph, data, lockup, "iap", options.metricsOptions, options.canDisplayArcadeOfferButton); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, lockup.title); + if (isStreamlinedBuy && hasDiscountedOffer) { + const inAppPurchaseAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, lockup.parent.buttonAction); + if (lockup.parent) { + inAppPurchaseAction.appTitle = lockup.parent.title; + } + inAppPurchaseAction.productTitle = lockup.title; + inAppPurchaseAction.streamlineBuyAction = streamlineBuyActionForIAP(objectGraph, data, parentData, lockup, options); + lockup.buttonAction = inAppPurchaseAction; + lockup.subtitle = mediaAttributes.attributeAsString(parentData, "name"); + } + else if (firstVersionSupportingMerchIAP) { + const inAppPurchaseAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, installRequiredAction, firstVersionSupportingMerchIAP); + if (lockup.parent) { + inAppPurchaseAction.appTitle = lockup.parent.title; + } + inAppPurchaseAction.productTitle = lockup.title; + const clickOptions = { + ...options.metricsOptions, + id: lockup.adamId, + idType: "its_id", + actionDetails: { parentAdamId: parentData.id }, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, inAppPurchaseAction, clickOptions); + lockup.buttonAction = inAppPurchaseAction; + } + else { + const alert = new models.AlertAction("default"); + alert.title = objectGraph.loc.string("SEED_IN_APP_UNSUPPORTED_MESSAGE_OPTION_1"); + alert.message = ""; + alert.isCancelable = true; + lockup.buttonAction = alert; + } + metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker); + // Click action to the product page + if (!options || !options.skipDefaultClickAction) { + const clickAction = iAPActionFromData(objectGraph, data, metricsOptions); + lockup.clickAction = clickAction; + lockup.productAction = clickAction; + } + metricsHelpersImpressions.addImpressionFieldsToInAppPurchaseLockup(objectGraph, lockup, metricsOptions); + }, "item.offer.buyParams"); +} +/** + * Creates the discounted buy action for the streamline buy Action + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The branch IAP used by the contingent offer + * @param parentData The branch app used by the contingent offer + * @param lockup The lockup object the metadata object will be added to + * @param options Lockup configuration options + */ +function streamlineBuyActionForIAP(objectGraph, data, parentData, lockup, options) { + var _a; + const clickOptions = { + ...options.metricsOptions, + id: parentData.id, + targetId: parentData.id, + idType: "its_id", + actionDetails: { parentAdamId: parentData.id }, + }; + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, options.clientIdentifierOverride); + const offerButtonMetricsClickOptions = objects.shallowCopyOf(clickOptions); + const discountedOfferData = discountedOfferFromData(data); + const offerData = offers.offerDataFromData(objectGraph, data); + const appOfferData = offers.offerDataFromData(objectGraph, parentData); + const appBuyParams = new BuyParameters(serverData.asString(appOfferData, "buyParams")); + let currentBuyParams = (_a = serverData.asString(discountedOfferData, "buyParams")) !== null && _a !== void 0 ? _a : serverData.asString(offerData, "buyParams"); + currentBuyParams += `&appAdamId=${serverData.asString(parentData, "id")}`; + currentBuyParams += `&appExtVrsId=${appBuyParams.get("appExtVrsId", "")}`; + currentBuyParams += `&bid=${contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId")}`; + currentBuyParams += `&bvrs=1.0`; + currentBuyParams += `&offerName=${contentAttributes.contentAttributeAsString(objectGraph, data, "offerName")}`; + const offerId = serverData.asString(discountedOfferData, "offerId"); + if (serverData.isDefinedNonNullNonEmpty(offerId)) { + currentBuyParams += `&adHocOfferId=${offerId}`; + } + offerData["buyParams"] = currentBuyParams; + const subscribeAction = offers.offerActionFromOfferData(objectGraph, offerData, data, false, false, metricsPlatformDisplayStyle, offerButtonMetricsClickOptions, "default", options === null || options === void 0 ? void 0 : options.referrerData, false, parentData.id); + return subscribeAction; +} +function copyDataIntoTrailersLockup(objectGraph, data, lockup, videoConfiguration, options) { + if (!data) { + return; + } + validation.context("copyDataIntoTrailersLockup", () => { + copyDataIntoLockup(objectGraph, data, lockup, options); + lockup.trailers = content.trailersFromData(objectGraph, data, videoConfiguration, options.metricsOptions, lockup.adamId); + }); +} +function copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options, cropCode) { + var _a; + if (!data) { + return; + } + if (options.isNetworkConstrained) { + return; + } + const isAd = (_a = options.metricsOptions.isAdvert) !== null && _a !== void 0 ? _a : false; + validation.context("copyMediaIntoMixedMediaLockup", () => { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const customCreativeData = platformAttributeAsDictionary(data, contentAttributes.bestAttributePlatformFromData(objectGraph, data), "creativeAttributes"); + if (isSome(customCreativeData)) { + lockup.alignedRegionArtwork = customCreativeArtworkFromData(objectGraph, data, customCreativeData, cropCode); + lockup.alignedRegionVideo = customCreativeVideoFromData(objectGraph, data, customCreativeData, videoConfiguration, cropCode); + } + } + } + lockup.screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */, null, options.clientIdentifierOverride, null, isAd, cropCode); + const firstScreenshots = lockup.screenshots[0]; + lockup.trailers = []; + const trailers = content.trailersFromData(objectGraph, data, videoConfiguration, options.metricsOptions, lockup.adamId, isAd, cropCode); + if (serverData.isDefinedNonNull(trailers)) { + if (serverData.isNullOrEmpty(firstScreenshots) || + trailers.mediaPlatform.isEqualTo(firstScreenshots.mediaPlatform)) { + lockup.trailers.push(trailers); + } + } + }); +} +/** + * Copy the data contained in a platform response into a `ScreenshotsLockup`. + * @param data The store platform response data to read from. + * @param lockup The ScreenshotsLockup data model to copy store platform data to + * @param options Options customizing what data is needed. + */ +function copyDataIntoScreenshotsLockup(objectGraph, data, lockup, options) { + if (!data) { + return; + } + validation.context("copyDataIntoScreenshotsLockup", () => { + copyDataIntoLockup(objectGraph, data, lockup, options); + lockup.screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */, null, options.clientIdentifierOverride); + }); +} +/** + * Copy the data contained in a platform response into a `PosterLockup`. + * @param data The store platform response data to read from. + * @param lockup The PosterLockup data model to copy store platform data to + * @param options Options customizing what data is needed. + */ +function copyDataIntoPosterLockup(objectGraph, data, lockup, options) { + if (!data) { + return; + } + validation.context("copyDataIntoPosterLockup", () => { + copyDataIntoLockup(objectGraph, data, lockup, options); + lockup.epicHeading = content.posterEpicHeadingArtworkFromData(objectGraph, data); + lockup.posterArtwork = content.posterArtworkFromData(objectGraph, data); + lockup.posterVideo = content.posterEditorialVideoFromData(objectGraph, data, 19 /* content.ArtworkUseCase.GroupingHero */); + if (lockup.offerDisplayProperties) { + lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "white", "lightOverArtwork"); + } + if (lockup.posterVideo) { + lockup.isDark = color.isDarkColor(lockup.posterVideo.preview.backgroundColor); + } + else if (lockup.posterArtwork) { + lockup.isDark = color.isDarkColor(lockup.posterArtwork.backgroundColor); + } + else { + lockup.isDark = false; + } + const supportsArcade = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade"); + const isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder"); + if (isPreorder) { + const fallbackString = supportsArcade ? objectGraph.loc.string("Offer.Label.ComingSoon") : null; + let preorderDate; + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + preorderDate = content.dynamicPreorderDateFromData(objectGraph, data, fallbackString); + } + else { + preorderDate = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, data, fallbackString)); + } + if (isSome(preorderDate)) { + lockup.footerText = preorderDate; + } + } + }); +} +export function subtitleFromData(objectGraph, data, lockupOptions = null) { + if (isNothing(data)) { + return null; + } + return validation.context("subtitleFromData", () => { + let subtitle; + if (lockupOptions && lockupOptions.subtitleObjectPath) { + subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, lockupOptions.subtitleObjectPath, lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride); + } + if (serverData.isNullOrEmpty(subtitle)) { + subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle", lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride); + } + if (subtitle) { + return subtitle; + } + else { + return categoryFromData(objectGraph, data, lockupOptions); + } + }); +} +/** + * Creates the compability badge text for showing on a lockup. + * + * @param objectGraph Current object graph + * @param data Product data + * @returns Built badge text, or null + */ +export function badgeFromData(objectGraph, data, allowMultiline = false, hideCompatibilityBadge) { + return validation.context("badgeFromData", () => { + if (hideCompatibilityBadge) { + return null; + } + const doesClientSupportMacOSCompatibleIOSBinary = objectGraph.client.isMac || objectGraph.client.isWeb; + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary); + const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsMacOSCompatibleIOSBinary || supportsVisionOSCompatibleBinary) { + // 1. Build loc string base. + let locKey = ""; + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (content.supportsPlatform(appPlatforms, "pad")) { + locKey = "Platform.DesignedForPad"; + } + else if (content.supportsPlatform(appPlatforms, "phone")) { + locKey = "Platform.DesignedForPhone"; + } + if ((locKey === null || locKey === void 0 ? void 0 : locKey.length) > 0) { + if (supportsMacOSCompatibleIOSBinary) { + // 2. Check if app is verified. + const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data); + if (!isVerifiedForAppleSiliconMac) { + locKey += ".NotVerified"; + // 3. Use expanded two-line not verified badge. + if (allowMultiline) { + locKey += ".Expanded"; + } + } + } + return objectGraph.loc.string(locKey); + } + } + return null; + }); +} +/** + * Creates the compability badge artwork for showing on a lockup. + * + * @param objectGraph Current object graph + * @param data Product data + * @returns An iPhone/iPad symbol artwork, or null + */ +export function badgeArtworkFromData(objectGraph, data) { + const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsVisionOSCompatibleBinary && objectGraph.client.isVision) { + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (content.supportsPlatform(appPlatforms, "pad")) { + return createArtworkForResource(objectGraph, "systemimage://ipad.landscape"); + } + else if (content.supportsPlatform(appPlatforms, "phone")) { + return createArtworkForResource(objectGraph, "systemimage://iphone"); + } + } + return null; +} +export function badgeActionFromData(objectGraph, data) { + return validation.context("badgeActionFromData", () => { + // Add action for unverified macOS apps. + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac); + if (!supportsMacOSCompatibleIOSBinary) { + return null; + } + const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data); + if (isVerifiedForAppleSiliconMac) { + return null; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (!content.supportsPlatform(appPlatforms, "pad") && !content.supportsPlatform(appPlatforms, "phone")) { + return null; + } + const linkAction = new models.FlowAction("article"); + linkAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.appleSiliconMacUnverifiedBadgeEditorialItemId}`; + return linkAction; + }); +} +/// Does the badge require multiple lines? +export function isBadgeMultilineFromData(objectGraph, data, allowMultiline) { + return validation.context("isBadgeMultilineFromData", () => { + if (!allowMultiline) { + return false; + } + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac); + if (!supportsMacOSCompatibleIOSBinary) { + return false; + } + const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data); + if (isVerifiedForAppleSiliconMac) { + return false; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + return content.supportsPlatform(appPlatforms, "pad") || content.supportsPlatform(appPlatforms, "phone"); + }); +} +function isVerifiedForAppleSiliconMacFromData(objectGraph, data) { + // Don't badge unverified apps when Apple Silicon support is not enabled. + if (!objectGraph.appleSilicon.isSupportEnabled) { + return true; + } + // Check response. + const isVerifiedForAppleSiliconMac = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "isVerifiedForAppleSiliconMac", "ios"); + if (serverData.isDefinedNonNull(isVerifiedForAppleSiliconMac)) { + return isVerifiedForAppleSiliconMac; + } + return false; +} +export function categoryFromData(objectGraph, data, lockupOptions = null) { + return validation.context("categoryFromData", () => { + // genreDisplayName is the preferred field to use for the determining the category name + // It should always be available, but we have fallbacks below just in case. + const genreName = contentAttributes.contentAttributeAsString(objectGraph, data, "genreDisplayName", lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride); + if ((genreName === null || genreName === void 0 ? void 0 : genreName.length) > 0) { + return genreName; + } + // Fallback to interating over the genres list + const genres = mediaRelationship.relationshipCollection(data, "genres"); + if (genres.length > 0) { + let candidateGenre = genres[0]; + // If the primary genre is Games, search for the first sub-genre of games + const gamesId = 6014 /* constants.GenreIds.Games */.toString(); + if (candidateGenre.id === gamesId) { + for (const genre of genres) { + const parentGenreId = mediaAttributes.attributeAsString(genre, "parentId"); + if (genre.id !== gamesId && parentGenreId === gamesId) { + candidateGenre = genre; + break; + } + } + } + return mediaAttributes.attributeAsString(candidateGenre, "name"); + } + else { + // Fallback to genreNames + const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames"); + return genreNames.length > 0 ? genreNames[0] : null; + } + }); +} +/** + * Configures an internal URL for use as the pageUrl of an 'install' FlowAction, + * using the provided parameters. + * @param inAppPurchaseAdamId The adamId for the lockup to appear in the header. + * @param parentAdamId The adamId for the lockup that is being offered. + * @returns A fully configured internal URL for fetching an app install page. + */ +export function configureIAPInstallPageUrl(objectGraph, inAppPurchaseAdamId, parentAdamId) { + const parameters = new http.FormBuilder() + .param(Parameters.id, parentAdamId) + .param(InAppPurchaseInstallPageParameters.inAppPurchaseId, inAppPurchaseAdamId) + .build(); + return `${Protocol.internal}:/${Path.product}/${Path.install}/?${parameters}`; +} +/** + * Configures an internal url to be used as the url for a clickAction from an IAP + * lockup to a product page. + * @param productUrl The URL for the product. + * @param inAppPurchaseAdamId The adamId for the lockup from which the click occurs. + * @param inAppPurchaseType The type for the in-app purchase. + * @returns A fully configured internal URL for a product click action via IAP. + */ +export function configureProductUrlFromInAppPurchase(objectGraph, productUrl, inAppProductIdentifier, isSubscription) { + const parameters = new http.FormBuilder() + .param(ProductPageParameters.url, productUrl) + .param(Parameters.offerName, inAppProductIdentifier) + .param(ProductPageParameters.isSubscription, isSubscription.toString()) + .build(); + return `${Protocol.internal}:/${Path.product}/${Path.lookup}/?${parameters}`; +} +export function lockupFromData(objectGraph, data, options) { + return validation.context("lockupFromData", () => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + if (!data) { + validation.unexpectedNull("ignoredValue", "data"); + return null; + } + // Must setup iAdInfo before any builder methods. + const isAd = adLockup.isAdvert(objectGraph, data); + options.metricsOptions.isAdvert = isAd; + const isAdEligible = adCommon.isAdEligible((_b = (_a = options.metricsOptions.pageInformation) === null || _a === void 0 ? void 0 : _a.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType, options.metricsOptions.locationTracker); + options.metricsOptions.isAdEligible = isAdEligible; + if (isAd || isAdEligible) { + (_d = (_c = options.metricsOptions.pageInformation) === null || _c === void 0 ? void 0 : _c.iAdInfo) === null || _d === void 0 ? void 0 : _d.apply(objectGraph, data); + } + if (isAd) { + (_f = (_e = options.metricsOptions.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP"); + } + if (!mediaAttributes.hasAttributes(data)) { + return null; + } + switch (data.type) { + case "in-apps": + options.offerEnvironment = "widthConstrainedLockup"; + return inAppPurchaseLockupFromData(objectGraph, data, options); + case "app-events": + const parentAppData = mediaRelationship.relationshipData(objectGraph, data, "app"); + if (serverData.isNullOrEmpty(parentAppData)) { + return null; + } + const appLockup = new models.Lockup(); + copyDataIntoLockup(objectGraph, parentAppData, appLockup, options); + return appLockup; + case "contingent-items": + case "offer-items": + return appPromotionOfferLockupFromData(objectGraph, data, options); + default: + const lockup = new models.Lockup(); + copyDataIntoLockup(objectGraph, data, lockup, options); + if (isSome((_g = options.metricsOptions.pageInformation) === null || _g === void 0 ? void 0 : _g.iAdInfo)) { + if (isAd || isAdEligible) { + adLockup.performAdOverridesforLockup(objectGraph, data, lockup, options.metricsOptions); + } + if (isAd) { + if (objectGraph.props.enabled("advertSlotReporting")) { + (_h = lockup.searchAdOpportunity) === null || _h === void 0 ? void 0 : _h.setTemplateType("APPLOCKUP"); + } + else { + (_j = lockup.searchAd) === null || _j === void 0 ? void 0 : _j.setTemplateType("APPLOCKUP"); + } + } + } + return lockup; + } + }); +} +/** + * Configures a app promotion lockup with a contingent-items or offer-items object + * @param objectGraph The App Store Object Graph. + * @param data The data for the app to go into the lockup. + * @param options A set of options customising the lockup. + * @returns A Lockup with the desired configuration. + */ +export function appPromotionOfferLockupFromData(objectGraph, data, options) { + return validation.context("appPromotionOfferLockupFromData", () => { + var _a, _b, _c; + const parentData = (_a = parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : options.parentAppData; + const iapData = iapDataFromData(objectGraph, data); + const supportsStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(parentData, "supportsStreamlinedBuy"); + if (supportsStreamlinedBuy) { + const lockup = inAppPurchaseLockupFromData(objectGraph, data, options); + lockup.offerDisplayProperties.titles["standard"] = objectGraph.loc.string("OfferButton.Title.Subscribe"); + lockup.offerDisplayProperties.isStreamlinedBuy = true; + // Override any discounts to force showing "Subscribe" title on the offer button. + lockup.offerDisplayProperties.hasDiscount = false; + // Setup the artwork + const rawArtwork = mediaAttributes.attributeAsDictionary(iapData, "artwork"); + const backupArtwork = appPromotionsCommon.artworkFromPlatformData(objectGraph, parentData, "artwork"); + const iapArtwork = content.artworkFromApiArtwork(objectGraph, rawArtwork, { + useCase: options.artworkUseCase, + withJoeColorPlaceholder: options.useJoeColorIconPlaceholder, + style: "iap", + overrideTextColorKey: options.overrideArtworkTextColorKey, + }); + lockup.icon = iapArtwork !== null && iapArtwork !== void 0 ? iapArtwork : backupArtwork; + return lockup; + } + else { + const lockup = new models.Lockup(); + copyDataIntoLockup(objectGraph, parentData, lockup, options); + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, options.metricsOptions, options.metricsClickOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsClickOptions, lockup.title); + const parentBundleId = contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId"); + const productIdentifier = mediaAttributes.attributeAsString(iapData, "offerName"); + const firstVersionSupportingMerchIAP = mediaAttributes.attributeAsString(parentData, "firstVersionSupportingInAppPurchaseApi"); + const offerAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, lockup.buttonAction, firstVersionSupportingMerchIAP); + offerAction.appTitle = (_b = mediaAttributes.attributeAsString(parentData, "name")) !== null && _b !== void 0 ? _b : ""; + offerAction.productTitle = (_c = mediaAttributes.attributeAsString(iapData, "name")) !== null && _c !== void 0 ? _c : ""; + if (data.type === "offer-items") { + const discountedOffer = discountedOfferFromData(iapData); + const offerId = serverData.asString(discountedOffer, "offerId"); + if (isSome(offerId) && offerId.length > 0) { + offerAction.additionalBuyParams = "adHocOfferId=" + offerId; + } + } + else { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + offerAction.additionalBuyParams = "contingentItemId=" + data.id; + } + lockup.buttonAction = offerAction; + metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker); + return lockup; + } + }); +} +export function inAppPurchaseLockupFromData(objectGraph, data, options) { + return validation.context("inAppPurchaseLockupFromData", () => { + const lockup = new models.InAppPurchaseLockup(); + copyDataIntoInAppPurchaseLockup(objectGraph, data, lockup, options); + return lockup; + }); +} +/** + * Create a new `ScreenshotsLockup` from a platform response data blob. + * @param data The platform response data to read from. + * @param options Options customizing what data the returned store item will contain. + * @returns A new `Lockup` object. + */ +export function screenshotsLockupFromData(objectGraph, data, options) { + return validation.context("screenshotsLockupFromData", () => { + const lockup = new models.ScreenshotsLockup(); + copyDataIntoScreenshotsLockup(objectGraph, data, lockup, options); + return lockup; + }); +} +/** + * Create a new `PosterLockup` from a platform response data blob. + * @param data The platform response data to read from. + * @param options Options customizing what data the returned store item will contain. + * @returns A new `Lockup` object. + */ +export function posterLockupFromData(objectGraph, data, options) { + return validation.context("posterLockupFromData", () => { + const lockup = new models.PosterLockup(); + copyDataIntoPosterLockup(objectGraph, data, lockup, options); + return lockup; + }); +} +/** + * + * @param data + * @param options + * @param videoConfiguration + * @returns {TrailersLockup} + */ +export function trailersLockupFromData(objectGraph, data, options, videoConfiguration) { + return validation.context("trailersLockupFromData", () => { + const lockup = new models.TrailersLockup(); + copyDataIntoTrailersLockup(objectGraph, data, lockup, videoConfiguration, options); + return lockup; + }); +} +/** + *Build a mixed media lockup from given data + * @param objectGraph + * @param data + * @param options + * @param videoConfiguration + * @param searchExperimentsData + * @param cropCode The crop code to use for the media + * @returns {MixedMediaLockup} + */ +export function mixedMediaLockupFromData(objectGraph, data, options, videoConfiguration, searchExperimentsData = null, cropCode) { + return validation.context("mixedMediaLockupFromData", () => { + const lockup = new models.MixedMediaLockup(); + copyDataIntoLockup(objectGraph, data, lockup, options, null, () => { + copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options); + copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options, cropCode); + copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData); + }); + return lockup; + }); +} +/** + * Create am image lockup for shelfContents to display within a grouping shelf + * @param objectGraph + * @param mediaApiData shelfContents to create lockup for. + * @param lockupOptions The options needed to customize this lockup + * @param collectionShelfDisplayStyle + */ +export function imageLockupFromData(objectGraph, mediaApiData, lockupOptions, collectionDisplayStyle) { + const isEditorialApiData = mediaApiData.type === "editorial-items"; + const lockupData = isEditorialApiData + ? mediaRelationship.relationshipData(objectGraph, mediaApiData, "primary-content") + : mediaApiData; + const lockup = lockupFromData(objectGraph, lockupData, lockupOptions); + // Determine which artwork to use, giving priority to an attached EI first, and then falling back + // to the lockup artwork. Currently we only use the attached EI on visionOS. + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, lockupData); + const lockupEditorialMediaData = editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, lockupData, collectionDisplayStyle); + const articleEditorialMediaData = editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, mediaApiData, collectionDisplayStyle); + const lockupArtwork = lockupEditorialMediaData === null || lockupEditorialMediaData === void 0 ? void 0 : lockupEditorialMediaData.artwork; + const articleArtwork = articleEditorialMediaData === null || articleEditorialMediaData === void 0 ? void 0 : articleEditorialMediaData.artwork; + let artwork; + let isDark; + if (isSome(articleArtwork) && objectGraph.client.isVision) { + isDark = isMediaDark(objectGraph, articleEditorialMediaData); + artwork = articleArtwork; + } + else if (isSome(lockupArtwork)) { + isDark = isMediaDark(objectGraph, lockupEditorialMediaData); + artwork = lockupArtwork; + } + if (isSome(artwork) && isSome(lockup)) { + const imageLockup = new models.ImageLockup(artwork, lockup, null, null, isDark); + imageLockup.caption = mediaPlatformAttributes.platformAttributeAsString(lockupData, attributePlatform, "editorialNotes.badge"); + if (isSome(imageLockup.caption) && objectGraph.client.isVision) { + imageLockup.caption = objectGraph.loc.uppercased(imageLockup.caption); + } + imageLockup.title = + mediaPlatformAttributes.platformAttributeAsString(lockupData, attributePlatform, "editorialNotes.tagline") || mediaAttributes.attributeAsString(lockupData, "genreDisplayName"); + imageLockup.impressionMetrics = lockup.impressionMetrics; + return imageLockup; + } + else { + return null; + } +} +function copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData) { + var _a; + if (!objectGraph.client.isPhone) { + return; + } + const displayStyle = serverData.asString(data.meta, "imageLockupFromData"); + if (serverData.isDefinedNonNull(displayStyle)) { + lockup.screenshotsDisplayStyle = displayStyle; + } + else if (serverData.isDefinedNonNull((_a = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _a === void 0 ? void 0 : _a.screenshots)) { + lockup.screenshotsDisplayStyle = searchExperimentsData.displayStyle.screenshots; + } +} +/** + * Create a mixed media lockup, with some specific ad-related modifications, from the supplied data. + * @param objectGraph The App Store Object Graph. + * @param data The data for the app to go into the lockup. + * @param options A set of options customising the lockup. + * @param videoConfiguration A configuration object for any videos in the lockup. + * @param searchExperimentsData Data for any search results experiments being run currently. + * @param applyAdOfferDisplayProperties Whether to apply the default ad OfferDisplayProperties. Some callers of this function want to enforce their own offer styling. + * @returns A MixedMediaLockup with the desired configuration. + */ +export function mixedMediaAdLockupFromData(objectGraph, data, options, videoConfiguration, searchExperimentsData, applyAdOfferDisplayProperties = true) { + return validation.context("mixedMediaAdLockupFromData", () => { + const lockup = new models.MixedMediaLockup(); + if (!mediaAttributes.attributeAsBooleanOrFalse(data, "iad.format.images")) { + copyDataIntoLockup(objectGraph, data, lockup, options, null, () => { + copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options); + }); + lockup.screenshots = []; + } + else { + copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options); + adLockup.performAssetOverridesForMixedMediaAdLockupIfNeeded(objectGraph, data, lockup, options.metricsOptions); + copyDataIntoLockup(objectGraph, data, lockup, options, null, () => { + copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData); + copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options); + }); + } + adLockup.performAdOverridesforLockup(objectGraph, data, lockup, options.metricsOptions, applyAdOfferDisplayProperties); + return lockup; + }); +} +/** + * Create an lockup that describes the arcade service lockup from upsell data. + * This lockup: + * - Has a title, e.g. "Apple Arcade". + * - May have a subtitle text, e.g. "Play 100+ games". + * - Does *NOT* have a `clickAction` on itself (There is no Arcade product page). + * - Has two actions for each subscription state. + */ +export function arcadeLockupFromData(objectGraph, upsellData, metricsOptions, context, offerStyle, offerEnvironment) { + return validation.context("arcadeLockupFromData", () => { + const data = upsellData.marketingItemData; + const lockup = new models.ArcadeLockup(); + lockup.title = objectGraph.loc.string("ARCADE_LOCKUP_TITLE", "Apple Arcade"); + const marketingItemData = upsellData.marketingItemData; + metricsOptions = { + ...metricsOptions, + mercuryMetricsData: metricsUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData), + }; + // This `name` attribute contains strings like "Some Games. \nAll you can play.". Weird. + // We trim newlines for arcade service footer lockup since the multiline text looks ugly. + let subtitle = arcadeUpsell.descriptionFromData(objectGraph, data); + if ((subtitle === null || subtitle === void 0 ? void 0 : subtitle.length) > 0) { + // This subtitle string on the upsell data blob is shared by many different views. However, editorial has a tendency to program explicit newlines, since some views look better with them (notably sheets). + // For the arcade lockup on some platforms, e.g. iOS, the string appears in a vertically constrained space and we ideally want to fit in 1 line if possible, We'll trim for now... Long term we need an editorial notes key + // for strings w/o newlines for this use case. + const platformIgnoresSubtitleNewlines = objectGraph.host.isiOS; + subtitle = platformIgnoresSubtitleNewlines ? subtitle.replace(/\n/g, " ") : subtitle; + lockup.nonsubscribedSubtitle = subtitle; + lockup.subscribedSubtitle = subtitle; + } + // Unsubscribed state action + let unsubscribedAction; + const unsubscribedActionTitle = arcadeUpsell.callToActionLabelFromData(objectGraph, data); + const arcadeLockupInNavBarEnabled = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isVision; + if (arcadeLockupInNavBarEnabled) { + // This is configured as an `ArcadeAction` directly if the pricing token is present. + unsubscribedAction = arcadeUpsell.arcadeOfferButtonActionFromData(objectGraph, upsellData.marketingItemData, context, metricsOptions); + unsubscribedAction.title = unsubscribedActionTitle; + } + else if ((unsubscribedActionTitle === null || unsubscribedActionTitle === void 0 ? void 0 : unsubscribedActionTitle.length) > 0) { + unsubscribedAction = arcadeCommon.arcadeSubscribePageFlowAction(objectGraph, models.marketingItemContextFromString("editorialItemCanvas"), null, null, { + ...metricsOptions, + id: data.id, + }); + unsubscribedAction.title = unsubscribedActionTitle; + } + else { + // If Upsell data is misconfigured and missing description, default to opening Arcade app for unsubscribed state. + unsubscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, metricsOptions.pageInformation, metricsOptions.locationTracker); + if (preprocessor.GAMES_TARGET) { + unsubscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + unsubscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + } + lockup.unsubscribedButtonAction = unsubscribedAction; + // Subscribed state action + const subscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, metricsOptions.pageInformation, metricsOptions.locationTracker); + if (preprocessor.GAMES_TARGET) { + subscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + subscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + lockup.subscribedButtonAction = subscribedAction; + // Impressions: + const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, upsellData.marketingItemData, lockup.title, metricsOptions); + metricsImpressionOptions.displaysArcadeUpsell = true; + // If no targetType is provided, set the correct value for the platform. + if (serverData.isNullOrEmpty(metricsImpressionOptions.targetType)) { + metricsImpressionOptions.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, lockup, metricsImpressionOptions); + const displayProperties = new models.OfferDisplayProperties("arcade", objectGraph.bag.arcadeAppAdamId, null, offerStyle, null, offerEnvironment, null, null, null, null, null, null, null, null, null, null, null, null, objectGraph.bag.arcadeProductFamilyId); + if (preprocessor.GAMES_TARGET) { + displayProperties.titles["subscribed"] = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + displayProperties.titles["subscribed"] = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + lockup.offerDisplayProperties = displayProperties; + return lockup; + }); +} +export function lockupsFromDataContainer(objectGraph, dataContainer, options) { + if (serverData.isNull(dataContainer)) { + return []; + } + return lockupsFromData(objectGraph, dataContainer.data, options); +} +export function lockupsFromData(objectGraph, dataArray, options) { + return validation.context("lockupsFromData", () => { + var _a; + if (!dataArray) { + return []; + } + const items = []; + let isDeferring = false; + for (let index = 0; index < dataArray.length; index++) { + if (isDeferring) { + break; + } + const lockupData = dataArray[index]; + if (!mediaAttributes.hasAttributes(lockupData)) { + if (options.contentUnavailable) { + isDeferring = options.contentUnavailable(index, lockupData); + } + continue; + } + const lockupOptions = options.lockupOptions; + let filter = 80894 /* filtering.Filter.All */; + if (options.includeOrdinals) { + const ordinal = options.ordinalDirection === "descending" ? dataArray.length - index : index + 1; + lockupOptions.ordinal = objectGraph.loc.decimal(ordinal).toString(); + } + if (serverData.isDefinedNonNull(options.filter)) { + filter = options.filter; + } + if (filtering.shouldFilter(objectGraph, lockupData, filter) && !options.shouldShowOnUnsupportedPlatform) { + continue; + } + const lockup = ((_a = options.lockupOptions.shouldCreateScreenshotsLockup) !== null && _a !== void 0 ? _a : false) + ? screenshotsLockupFromData(objectGraph, lockupData, lockupOptions) + : lockupFromData(objectGraph, lockupData, lockupOptions); + if (serverData.isNull(lockup) || !lockup.isValid()) { + continue; + } + items.push(lockup); + metricsHelpersLocation.nextPosition(options.lockupOptions.metricsOptions.locationTracker); + } + return items; + }); +} +export function screenshotsLockupsFromData(objectGraph, dataArray, options) { + return validation.context("screenshotsLockupsFromData", () => { + if (!dataArray) { + return []; + } + const items = []; + for (let index = 0; index < dataArray.length; index++) { + const lockupData = dataArray[index]; + if (serverData.isNull(lockupData.attributes)) { + if (options.contentUnavailable) { + options.contentUnavailable(index, lockupData); + } + continue; + } + const lockupOptions = options.lockupOptions; + let filter = 80894 /* filtering.Filter.All */; + if (serverData.isDefinedNonNull(options.filter)) { + filter = options.filter; + } + if (filtering.shouldFilter(objectGraph, lockupData, filter)) { + continue; + } + const lockup = screenshotsLockupFromData(objectGraph, lockupData, lockupOptions); + if (!lockup.isValid()) { + continue; + } + items.push(lockup); + metricsHelpersLocation.nextPosition(options.lockupOptions.metricsOptions.locationTracker); + } + return items; + }); +} +/** + * Create an action for the provided data + * @param objectGraph The App Store Object Graph. + * @param data The data to create the action for. + * @param metricsOptions The metrics options to use for the action. + * @param clientIdentifierOverride A client identifier override, if any. + * @param externalDeepLinkUrl A custom deeplink URL, if any. + * @param isCppDeepLinkEligible Whether the action should be eligible for CPP Deep Links, if any. Used for restricting deep links on ads. + * @returns A configured action for the provided data. + */ +export function actionFromData(objectGraph, data, metricsOptions, clientIdentifierOverride, externalDeepLinkUrl = null, isCppDeepLinkEligible) { + return validation.context(`actionFromData: ${data.type}`, () => { + switch (data.type) { + case "apps": + case "app-bundles": { + return productActionFromData(objectGraph, data, metricsOptions, { + clientIdentifierOverride: clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + isCppDeepLinkEligible: isCppDeepLinkEligible, + }); + } + case "in-apps": { + return iAPActionFromData(objectGraph, data, metricsOptions); + } + case "editorial-items": { + return editorialItemActionFromData(objectGraph, data, metricsOptions, clientIdentifierOverride); + } + case "tags": + case "editorial-pages": { + return editorialPageActionFromData(objectGraph, data, metricsOptions); + } + case "multiple-system-operators": + return msoActionFromData(objectGraph, data, metricsOptions); + case "groupings": + return groupingActionFromData(objectGraph, data, metricsOptions); + case "developers": + default: { + return genericActionFromData(objectGraph, data, metricsOptions); + } + } + }); +} +function iAPActionFromData(objectGraph, data, options) { + return validation.context("iAPActionFromData", () => { + var _a; + const parentData = parentDataFromInAppData(objectGraph, data); + if (!parentData) { + return null; + } + const clickAction = new models.FlowAction("product"); + const parentUrl = urls.URL.from(mediaAttributes.attributeAsString(parentData, "url")); + // Attach the productVariantID/cppId/ppid to the parent URL, if it exists. + const productVariantData = (_a = options.productVariantData) !== null && _a !== void 0 ? _a : variants.productVariantDataForData(objectGraph, parentData); + const productVariantID = variants.productVariantIDForVariantData(productVariantData); + if (serverData.isDefinedNonNull(productVariantID)) { + parentUrl.param(Parameters.productVariantID, productVariantID); + } + const parentUrlString = parentUrl.toString(); + const inAppProductIdentifier = mediaAttributes.attributeAsString(data, "offerName"); + const isSubscription = mediaAttributes.attributeAsBooleanOrFalse(data, "isSubscription"); + clickAction.pageUrl = configureProductUrlFromInAppPurchase(objectGraph, parentUrlString, inAppProductIdentifier, isSubscription); + clickAction.title = mediaAttributes.attributeAsString(data, "name"); + metricsHelpersClicks.addClickEventToAction(objectGraph, clickAction, options); + return clickAction; + }); +} +export function editorialItemActionTypeFromData(objectGraph, data) { + if (serverData.isNullOrEmpty(data)) { + return 0 /* EditorialItemActionType.Unknown */; + } + return validation.context("editorialItemActionFromData", () => { + const link = mediaAttributes.attributeAsDictionary(data, "link"); + if (linkIsExternal(link)) { + return 1 /* EditorialItemActionType.ExternalLink */; + } + const substyle = mediaAttributes.attributeAsString(data, "displaySubStyle"); + const isListArticle = substyle === "List" || substyle === "NumberedList"; + if (mediaAttributes.attributeAsBooleanOrFalse(data, "isCanvasAvailable") || isListArticle) { + return 2 /* EditorialItemActionType.Article */; + } + // This is a bit of a workaround to get the product data from `primary-content`, if it's available, for app events + // as this is where it's currently made available for an EI. + const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content"); + const cardContents = mediaRelationship.relationshipCollection(data, "card-contents"); + const isPrimaryContentProduct = serverData.isDefinedNonNullNonEmpty(primaryContent) && primaryContent.length === 1; + const isProduct = serverData.isDefinedNonNullNonEmpty(cardContents) && cardContents.length === 1; + if ((isProduct && cardContents[0].type === "app-events") || + (isPrimaryContentProduct && primaryContent[0].type === "app-events")) { + return 4 /* EditorialItemActionType.AppEvent */; + } + if (isProduct) { + return 3 /* EditorialItemActionType.Product */; + } + if ((objectGraph.client.isVision || preprocessor.GAMES_TARGET) && isSome(data.href) && data.href.length > 0) { + return 5 /* EditorialItemActionType.Href */; + } + return 0 /* EditorialItemActionType.Unknown */; + }); +} +/** + * Create an action for an editorial story + * @param objectGraph The App Store Object Graph. + * @param data The data for the editorial story + * @param options A set of options customising the lockup. + * @param clientIdentifierOverride A client identifier override. + * @param articleRecoMetricsData The recommendation metrics data for the editorial item + * @param todayCardConfig The config options for the editorial Item + * @param canvasFilter The canvas filter for this editorial item, used to fetch a specific story + * @returns A click action for an editorial item + */ +export function editorialItemActionFromData(objectGraph, data, options, clientIdentifierOverride, articleRecoMetricsData, todayCardConfig) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + return validation.context("editorialItemActionFromData", () => { + var _a; + let flowDestination; + let pageUrlString; + let destinationIntent; + let presentation; + switch (editorialItemActionTypeFromData(objectGraph, data)) { + case 1 /* EditorialItemActionType.ExternalLink */: + return editorialItemExternalLinkActionFromData(objectGraph, data, options); + case 2 /* EditorialItemActionType.Article */: + flowDestination = "article"; + const pageUrl = urls.URL.from(mediaAttributes.attributeAsString(data, "url")); + if (serverData.isDefinedNonNull(articleRecoMetricsData)) { + pageUrl.param(Parameters.recoMetrics, JSON.stringify(articleRecoMetricsData)); + } + if (objectGraph.client.isiOS && isSome(todayCardConfig) && !todayCardConfig.isHorizontalShelfContext) { + pageUrl.param(Parameters.todayCardConfig, JSON.stringify(todayCardConfig)); + } + const editorialCardId = (_a = editorialCardFromData(data)) === null || _a === void 0 ? void 0 : _a.id; + if (isSome(editorialCardId)) { + pageUrl.param(Parameters.editorialCardId, editorialCardId); + } + pageUrlString = pageUrl.build(); + if (objectGraph.client.isWeb) { + destinationIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destinationIntent); + } + if (preprocessor.GAMES_TARGET) { + // For Luck Seed1 we always show stories as modal across all platform, + // in the future we should follow App Store and push them on Mac, + // according to the specs https://quip-apple.com/CEplADSB1MJR + // Need to support proper dual column layout first: + // rdar://141251194 ([Seed 3] [Stories] Dynamic page layout (single and dual column)) + if (objectGraph.host.isMac) { + presentation = "stackPush"; + } + else { + presentation = "sheetPresent"; + } + } + break; + case 3 /* EditorialItemActionType.Product */: + const productData = mediaRelationship.relationshipCollection(data, "card-contents")[0]; + return actionFromData(objectGraph, productData, options, clientIdentifierOverride); + case 4 /* EditorialItemActionType.AppEvent */: + // This is a bit of a workaround to get the product data from `primary-content`, if it's available, for app events + // as this is where it's currently made available for an EI. + const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content"); + const isPrimaryContentProduct = serverData.isDefinedNonNullNonEmpty(primaryContent) && primaryContent.length === 1; + const appEventData = isPrimaryContentProduct + ? primaryContent[0] + : mediaRelationship.relationshipCollection(data, "card-contents")[0]; + const eventProductData = mediaRelationship.relationshipData(objectGraph, appEventData, "app"); + if (serverData.isNullOrEmpty(eventProductData)) { + return null; + } + const appEventOrDate = appEvent.appEventOrPromotionStartDateFromData(objectGraph, appEventData, eventProductData, false, false, "dark", "infer", false, options, false, true, null, false, false); + // Return early if we received a Date, as this means the App Event shouldn't be accessible yet. + if (serverData.isNull(appEventOrDate) || appEventOrDate instanceof Date) { + return null; + } + return appPromotionsCommon.detailPageClickActionFromData(objectGraph, appEventData, eventProductData, appEventOrDate, options, true); + case 5 /* EditorialItemActionType.Href */: + flowDestination = "page"; + pageUrlString = mediaUrlMapping.hrefToRoutableUrl(objectGraph, data.href); + break; + default: + flowDestination = "unknown"; + const link = mediaAttributes.attributeAsDictionary(data, "link"); + if (objectGraph.client.isWeb) { + destinationIntent = makeEditorialPageIntentByID({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + pageUrlString = makeEditorialPageURL(objectGraph, destinationIntent); + } + else { + pageUrlString = serverData.asString(link, "url"); + } + } + if (isNothing(pageUrlString)) { + return null; + } + const action = new models.FlowAction(flowDestination); + action.pageUrl = pageUrlString; + if (isSome(presentation)) { + action.presentation = presentation; + } + let title = content.notesFromData(objectGraph, data, "name"); + if (serverData.isNull(title)) { + title = serverData.asString(data, "label"); + } + action.title = title; + if (destinationIntent) { + action.destination = destinationIntent; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display a product page. + * @param objectGraph Dependency soup + * @param data The media api data to create the action from. + * @param metricsOptions Metrics dependencies + * @param options.clientIdentifierOverride The preferred client identifier to use for the product page. + * @param options.externalDeepLinkUrl An external deep link for the propuct to be used when opening the app, if any. + * @param options.isCppDeepLinkEligible Whether a CPP deep link can be used for this lockup, if available. + * @param options.productVariantData A variant to use. This can be populated as an optimization to avoid re-resolving the same variant data, e.g. in a product page. + * @returns A `FlowAction` object pointing to a product page. + */ +function productActionFromData(objectGraph, data, metricsOptions, options) { + var _a, _b, _c, _d; + if (!data) { + return null; + } + const clientIdentifierOverride = (_a = options.clientIdentifierOverride) !== null && _a !== void 0 ? _a : null; + const externalDeepLinkUrl = (_b = options.externalDeepLinkUrl) !== null && _b !== void 0 ? _b : null; + const isCppDeepLinkEligible = (_c = options.isCppDeepLinkEligible) !== null && _c !== void 0 ? _c : false; + const isCppDeepLinkDisabled = !isCppDeepLinkEligible; + const productVariantData = (_d = options.productVariantData) !== null && _d !== void 0 ? _d : variants.productVariantDataForData(objectGraph, data); + return validation.context("productActionFromData", () => { + var _a, _b, _c; + let productUrl = mediaAttributes.attributeAsString(data, "url"); + if (!productUrl) { + validation.unexpectedNull("ignoredValue", "string", "url"); + return null; + } + let productPageOptions = {}; + const url = new urls.URL(productUrl); + if (metricsOptions.isAdvert) { + const lineItem = serverData.asString(data, "iad.lineItem"); + if (lineItem !== null && lineItem.length > 0) { + url.param(metricsHelpersModels.iAdURLLineItemParameterStringToken, lineItem); + } + const iAdClickFields = (_a = metricsOptions.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.clickFields; + url.param(metricsHelpersModels.iAdURLParameterStringToken, JSON.stringify(iAdClickFields)); + productPageOptions = { + iAdClickFields: serverData.asJSONData(iAdClickFields), + iAdLineItem: lineItem, + }; + const instanceId = adCommon.advertInstanceIdForData(objectGraph, data); + if (isSome(instanceId)) { + const advertType = content.isArcadeSupported(objectGraph, data) + ? "arcadeApp" + : "standardApp"; + const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation); + const bundleId = mediaAttributes.attributeAsString(data, "platformAttributes.ios.bundleId"); + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + const purchaseType = isPreorder ? "preorder" : "standard"; + const dismissAdActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "productPageDismissed", purchaseType, reportingDestination); + url.param(metricsHelpersModels.iAdDismissAdActionMetricsParameterStringToken, JSON.stringify(dismissAdActionMetrics)); + productPageOptions.iAdDismissAdActionMetrics = serverData.asJSONData(dismissAdActionMetrics); + } + } + const productVariantID = variants.productVariantIDForVariantData(productVariantData); + if (serverData.isDefinedNonNull(productVariantID)) { + url.param(Parameters.productVariantID, productVariantID); + } + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const alignedRegionDeeplinkUrl = adLockup.getCustomCreativeDeepLinkUrl(data); + if (isSome(alignedRegionDeeplinkUrl)) { + url.param(externalDeepLink.alignedRegionDeepLinkQueryParameter, alignedRegionDeeplinkUrl); + } + else { + const tapDestinationCppId = adLockup.getTapDestinationIdForAdvert(data); + if (isSome(tapDestinationCppId)) { + url.param(Parameters.productVariantID, tapDestinationCppId); + } + } + } + } + productPageOptions.externalDeepLinkUrl = externalDeepLinkUrl; + if (serverData.isDefinedNonNull(externalDeepLinkUrl)) { + url.param(externalDeepLink.externalDeepLinkQueryParameter, externalDeepLinkUrl); + } + const platformFromIntent = getPlatform(objectGraph).platform; + const platformInferredFromData = safelyInferPlatformFromData(objectGraph, data); + if (objectGraph.client.isWeb) { + // For the web client, we add the platform query parameter if the active intent's platform + // differs from the "default" platform inferred from the app's data. + if (platformFromIntent && platformFromIntent !== platformInferredFromData) { + url.param("platform", platformFromIntent); + } + } + else { + // For non-web clients, propagate CPP, search term, and client identifier parms, which are not needed for web. + productPageOptions.isCppDeepLinkDisabled = isCppDeepLinkDisabled; + url.param(externalDeepLink.cppDeepLinkDisabledQueryParameter, isCppDeepLinkDisabled.toString()); + // Add a searchTerm to the product URL to propagate to purchases on the subsequent product page. + const searchTerm = (_c = (_b = metricsOptions.pageInformation) === null || _b === void 0 ? void 0 : _b.searchTermContext) === null || _c === void 0 ? void 0 : _c.term; + if (isSome(searchTerm)) { + url.param("searchTerm", searchTerm); + } + productPageOptions.clientIdentifierOverride = clientIdentifierOverride; + if ((clientIdentifierOverride === null || clientIdentifierOverride === void 0 ? void 0 : clientIdentifierOverride.length) > 0) { + url.param("clientIdentifierOverride", clientIdentifierOverride); + } + } + productUrl = url.toString(); + if (preprocessor.GAMES_TARGET) { + return gameModels.viewGameActionWithMediaAPIData(data, objectGraph, makeClickMetrics(objectGraph, data.id, "lockup", "navigate", [data.id])); + } + const action = new models.FlowAction("product"); + action.pageUrl = productUrl; + action.pageData = productPageUtil.createProductPageSidePackFromResponse(objectGraph, data, productPageOptions); + action.title = mediaAttributes.attributeAsString(data, "name"); + if (metricsOptions && metricsOptions.pageInformation) { + action.referrerUrl = metricsOptions.pageInformation.pageUrl; + } + // Extract and add additional options for pre-orders + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + if (isPreorder) { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + ...metricsOptions, + offerType: "preorder", + offerReleaseDate: offers.expectedReleaseDateFromData(objectGraph, data), + }, true); + } + else { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, metricsOptions, true); + } + const productId = serverData.asString(data, "id"); + if (isSome(productId)) { + action.destination = + data.type === "app-bundles" + ? makeBundlePageIntent({ + ...getLocale(objectGraph), + id: productId, + }) + : makeProductPageIntent({ + ...getLocale(objectGraph), + platform: inferPreviewPlatform(objectGraph, data), + id: productId, + }); + } + return action; + }); +} +/** + * Safely infers a preview platform from media data, handling any potential errors. + */ +function safelyInferPlatformFromData(objectGraph, data) { + try { + return inferPreviewPlatformFromDeviceFamilies(objectGraph, { data: [data] }); + } + catch (error) { + objectGraph.console.error(`Error inferring preview platform from data: ${error}`); + return undefined; + } +} +function inferPreviewPlatform(objectGraph, data) { + const platformFromIntent = getPlatform(objectGraph).platform; + if (platformFromIntent) { + return platformFromIntent; + } + return safelyInferPlatformFromData(objectGraph, data); +} +/** + * Creates a flow action to display a grouping page + * @param data The media api data to create the action from. + * @returns A `FlowAction` object pointing to a grouping page. + */ +function groupingActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("groupingActionFromData", () => { + if (!data.href) { + validation.unexpectedNull("ignoredValue", "string", "href"); + return null; + } + const action = new models.FlowAction("page"); + if (objectGraph.client.isWeb) { + const destination = makeGroupingPageIntentByID({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + action.destination = destination; + action.pageUrl = makeGroupingPageCanonicalURL(objectGraph, destination); + } + else { + action.pageUrl = mediaUrlBuilder + .buildURLFromRequest(objectGraph, mediaUrlMapping.mediaApiGroupingURLFromHref(objectGraph, data.href)) + .toString(); + } + action.title = nameFromGroupingData(objectGraph, data); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display a mso-page. + * @param data The media api data to create the action from. + * @returns A `FlowAction` object pointing to a mso page. + */ +function msoActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("msoActionFromPlatformData", () => { + const roomUrl = mediaAttributes.attributeAsString(data, "url"); + if (!roomUrl) { + validation.unexpectedNull("ignoredValue", "string", "url"); + return null; + } + const action = new models.FlowAction("mso"); + action.pageUrl = roomUrl; + action.title = mediaAttributes.attributeAsString(data, "name"); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display an editorial page. + * + * editorial-pages asset type does not have a url attribute, so we have to synthesize one + * - The "web" client uses the URL defined by the `EditorialPageIntentController` + * - Other clients use the `href` attribute + * + * @param data The media api data to create the action from. + * @param options The required options passed to the lockup creation method. + * @returns A `FlowAction` object pointing to a product page. + */ +function editorialPageActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("editorialPageActionFromData", () => { + const action = new models.FlowAction("page"); + if (objectGraph.client.isWeb) { + const editorialPageIntent = makeEditorialPageIntentByID({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + action.destination = editorialPageIntent; + action.pageUrl = makeEditorialPageURL(objectGraph, editorialPageIntent); + } + else { + const href = data.href; + if (serverData.isNullOrEmpty(href)) { + validation.unexpectedNull("ignoredValue", "string", "href"); + return null; + } + action.pageUrl = mediaUrlMapping.hrefToRoutableUrl(objectGraph, href); + } + action.title = content.editorialNotesFromData(objectGraph, data, "name"); + if (serverData.isNullOrEmpty(action.title)) { + action.title = mediaAttributes.attributeAsString(data, "name"); + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display a generic page + * @param data The media api data to create the action from. + * @returns A `FlowAction` object pointing to a product page. + */ +function genericActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("genericActionFromData", () => { + const type = serverData.asString(data, "type"); + const url = mediaAttributes.attributeAsString(data, "url"); + if (!url) { + validation.unexpectedNull("ignoredValue", "string", "url"); + return null; + } + const action = new models.FlowAction("page"); + action.pageUrl = url; + if (type === "groupings") { + action.title = nameFromGroupingData(objectGraph, data); + } + else { + action.title = mediaAttributes.attributeAsString(data, "name"); + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Whether or not link blob is to external url. + * @param link JSON blob for 'link' + * @returns boolean whether url is external link. + */ +function linkIsExternal(link) { + const target = serverData.asString(link, "target"); + return target && target === "external"; +} +/** + * Create an `ExternalUrlAction` for an External Link card. + * The title of the action is either the url domain or provided short description. + * @param data Media Api data + * @returns A configured `ExternalUrlAction` for card. + */ +export function editorialItemExternalLinkActionFromData(objectGraph, data, options) { + return validation.context("editorialItemExternalLinkActionFromData", () => { + const link = mediaAttributes.attributeAsDictionary(data, "link"); + const urlString = serverData.asString(link, "url"); + const action = new models.ExternalUrlAction(urlString); + // Title is short description or url domain + const linkDescription = content.notesFromData(objectGraph, data, "short"); + if (linkDescription) { + action.title = linkDescription; + } + else { + const url = new urls.URL(urlString); + action.title = url.host; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a shallow copy of the provided lockups array, overriding any of the properties + * supplied. This function makes sure to override the properties non-destructively; that + * is, the overrides are only applied to the copied array's lockups. + * @param lockups The lockups to copy. + * @param overrideStyle The style to apply for the copied lockups' offerDisplayProperties. + * @param filterAds Whether to filter ads. In some places where we add ads to a set of lockups, showing that ad in a copied location isn't supported. + * @returns {Lockup[]} + */ +export function shallowCopyLockupsOverridingProperties(objectGraph, lockups, overrideStyle, filterAds = false) { + var _a, _b; + if (!lockups) { + return null; + } + // Store a count of the removed ads so we know how many items we've filtered from the beginning of the list of lockups. + let removedAdCount = 0; + const copies = []; + for (const lockup of lockups) { + if (filterAds && serverData.isDefinedNonNull((_a = lockup.searchAd) !== null && _a !== void 0 ? _a : (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.searchAd)) { + removedAdCount += 1; + continue; + } + const copy = objects.shallowCopyOf(lockup); + if (overrideStyle && copy.offerDisplayProperties) { + copy.offerDisplayProperties = copy.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, overrideStyle); + } + /// Adjust the impression index of the copy by the number of ads that have been removed prior to this item. + if (removedAdCount > 0) { + // Take a copy of the impressionMetrics.fields, as the copy is only shallow and we don't want to affect the original. + const impressionMetricsFields = objects.shallowCopyOf(copy.impressionMetrics.fields); + const impressionIndex = serverData.asNumber(impressionMetricsFields.impressionIndex); + if (isSome(impressionIndex)) { + impressionMetricsFields.impressionIndex = impressionIndex - removedAdCount; + copy.impressionMetrics = new models.ImpressionMetrics(impressionMetricsFields, copy.impressionMetrics.id, copy.impressionMetrics.custom); + } + } + copies.push(copy); + } + return copies; +} +/** + * Generates a name from a grouping media api resource + * @param data The grouping data from a genre response + * @return {string} A string of the grouping page's name + */ +export function nameFromGroupingData(objectGraph, data) { + const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames"); + if (serverData.isDefinedNonNullNonEmpty(genreNames)) { + return genreNames[0]; + } + else { + return mediaAttributes.attributeAsString(data, "name"); + } +} +export function deviceHasCapabilitiesFromData(objectGraph, data) { + if (!data) { + return false; + } + if (objectGraph.client.isWatch || objectGraph.client.isWeb || objectGraph.client.isCompanionVisionApp) { + // For the "watch" client, opt out of capability checks until we land <rdar://47322408>. + // For the "web" client, we treat it like it can run anything, since we can't infer actual capabilites. + // For the companion app, we can't call down to the remote device to check its capabilities, + // so we defer this check to later on. + return true; + } + const requiredCapabilitiesString = content.requiredCapabilitiesFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + if (serverData.isNullOrEmpty(requiredCapabilitiesString)) { + // If we don't have any capabilities, this is vacuously true. + return true; + } + const splitCapabilities = requiredCapabilitiesString.split(" "); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + return objectGraph.client.deviceHasCapabilitiesIncludingCompatibilityCheckIsVisionOSCompatibleIOSApp(splitCapabilities, supportsVisionOSCompatibleIOSBinary); +} +/** + * Finds the IAP data from an app promotion + * @param data The app promotion data + * @returns data of the IAP + */ +export function iapDataFromData(objectGraph, data) { + if (!data) { + return null; + } + switch (data.type) { + case "contingent-items": + return mediaRelationship.relationshipData(objectGraph, data, "branch"); + case "offer-items": + return mediaRelationship.relationshipData(objectGraph, data, "salables"); + default: + break; + } + return data; +} +/** + * Finds the discounted offer data from an IAP. + * @param data The IAP data + * @returns the discounted offer + */ +export function discountedOfferFromData(data) { + if (!data) { + return null; + } + const contingentOffer = serverData.asDictionary(data, "meta.contingentItemOffer"); + if (serverData.isDefinedNonNullNonEmpty(contingentOffer)) { + return contingentOffer; + } + const winbackOffer = serverData.asDictionary(data, "meta.discountOffer"); + if (serverData.isDefinedNonNullNonEmpty(winbackOffer)) { + return winbackOffer; + } + return null; +} +/** + * Finds the parent app data from a app promotion. + * @param data The contingent-offer or offer-item data + * @returns the parent app data + */ +export function parentDataFromInAppData(objectGraph, data) { + if (!data) { + return null; + } + switch (data.type) { + case "contingent-items": + return mediaRelationship.relationshipData(objectGraph, data, "branch-app"); + case "offer-items": + const iapData = mediaRelationship.relationshipData(objectGraph, data, "salables"); + return mediaRelationship.relationshipData(objectGraph, iapData, "app"); + default: + break; + } + return mediaRelationship.relationshipData(objectGraph, data, "app"); +} +export function cleanupArcadeDownloadPackLockupMetricsIfNeeded(lockup, objectGraph) { + if (!objectGraph.bag.arcadeDownloadPacksMetricsEventsEnabled) { + lockup.clickAction.actionMetrics.clearAll(); + lockup.buttonAction.actionMetrics.clearAll(); + if (lockup.buttonAction instanceof models.OfferStateAction) { + lockup.buttonAction.defaultAction.actionMetrics.clearAll(); + } + } + if (!objectGraph.bag.arcadeDownloadPacksImpressionEventsEnabled) { + lockup.impressionMetrics = null; + } +} +//# sourceMappingURL=lockups.js.map
\ No newline at end of file |
