// // 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 . // 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