import { isNothing, isSome } from "@jet/environment"; import * as validation from "@jet/environment/json/validation"; import { ArtworkContentMode, ExternalUrlAction, FlowAction, TodayCardActionOverlay, TodayCardMediaArtwork, TodayCardMediaHero, TodayCardMediaMultiApp, TodayCardMediaVideo, TodayCardMediaWithArtwork, } from "../../api/models"; import { asBoolean, asDictionary, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, objectPathToString, } from "../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../foundation/media/attributes"; import { relationshipCollection } from "../../foundation/media/relationships"; import { Parameters } from "../../foundation/network/url-constants"; import { URL } from "../../foundation/network/urls"; import * as color from "../../foundation/util/color-util"; import { artworkFromApiArtwork, editorialNotesFromData, hasMessagesExtensionFromData, isHiddenFromSpringboardFromData, joeColorSetFromData, notesFromData, screenSizeIPhone134, } from "../content/content"; import { extractEditorialClientParams } from "../editorial-pages/editorial-data-util"; import * as lockupsEditorialContext from "../lockups/editorial-context"; import { lockupsFromData } from "../lockups/lockups"; import * as metricsHelpersLocation from "../metrics/helpers/location"; import * as metricsHelpersImpressions from "./../metrics/helpers/impressions"; import { createTodayAppEventCard } from "./cards/today-app-event-card-builder"; import { createTodayBrandedCard } from "./cards/today-branded-card-builder"; import { createTodayFullBleedImageCard } from "./cards/today-full-bleed-image-card-builder"; import { createTodayGridCard } from "./cards/today-grid-card-builder"; import { createTodayInAppPurchaseCard } from "./cards/today-in-app-purchase-card-builder"; import { createTodayListCard } from "./cards/today-list-card-builder"; import { createTodayRiverCard } from "./cards/today-river-card-builder"; import { createTodayShortImageCard } from "./cards/today-short-image-card-builder"; import { createTodaySingleAppCard } from "./cards/today-single-app-card-builder"; import { createTodayVideoCard } from "./cards/today-video-card-builder"; import { HeroMediaDisplayContext, OfTheDayIntention, TodayCardDisplayStyle, } from "./today-types"; import { createTodayBaseCard } from "./cards/today-base-card-builder"; // MARK: - Today Card Creation /** * Create a card with some appearance configuration within today page. * @param {AppStoreObjectGraph} objectGraph The object graph for the app store. * @param {Data} data to build card of. * @param {TodayCardConfiguration} cardConfig flags on how to configure a given card. * @param {TodayParseContext} context to store state updated throughout parsing. * @param {TodayCardAugmentingData} augmentingData that stores some additional responses that may be used to enhance the contents of `data` */ export function todayCardFromData(objectGraph, data, cardConfig, context, augmentingData) { return validation.catchingContext("todayCardFromData", () => { const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); const clientIdentifier = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, data); // If clientIdentifier is empty, don't override the cardConfig as this may have been set // at a higher level. if (isSome(clientIdentifier)) { if (shouldRespectClientIdentifierOverride(objectGraph, data, clientIdentifier, cardConfig)) { cardConfig.clientIdentifierOverride = clientIdentifier; } else { cardConfig.clientIdentifierOverride = null; } } // Configure subtitle for cross link. cardConfig.crossLinkSubtitle = crossLinkSubtitleFromData(objectGraph, data); let card = null; switch (cardDisplayStyle) { case TodayCardDisplayStyle.AppOfTheDay: case TodayCardDisplayStyle.GameOfTheDay: card = createTodayBrandedCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.Video: card = createTodayVideoCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.FullBleedImage: card = createTodayFullBleedImageCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.InAppPurchase: card = createTodayInAppPurchaseCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.AppEventCard: card = createTodayAppEventCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.List: case TodayCardDisplayStyle.NumberedList: card = createTodayListCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.River: case TodayCardDisplayStyle.Grid: if (objectGraph.client.isMac) { card = createTodayGridCard(objectGraph, data, cardConfig, context, augmentingData); } else { card = createTodayRiverCard(objectGraph, data, cardConfig, context, augmentingData); } break; case TodayCardDisplayStyle.SingleApp: card = createTodaySingleAppCard(objectGraph, data, cardConfig, context, augmentingData); break; case TodayCardDisplayStyle.ShortImage: card = createTodayShortImageCard(objectGraph, data, cardConfig, context, augmentingData); break; default: card = null; break; } if (isNull(card)) { objectGraph.console.log(`Unknown style: ${cardDisplayStyle}`); return card; } // For certain platforms we add hero media to the card. addHeroMediaToTodayCardIfNecessary(objectGraph, card, cardConfig, data); addExternalLinkOverlayToTodayCardIfNecessary(card); addOTDStyleToCardIfNecessary(card, cardConfig); enableFlipAndBlurIfNecessary(objectGraph, card); if (isNothing(card.media)) { objectGraph.console.log(`Missing required media: ${cardDisplayStyle}`); card = null; } return card; }, (error) => { objectGraph.console.log(error); return null; }); } /** * On the watch we need the todayCard on the article to get the title and subtitle, * so even if we dont successfully make a card for the article we create a basic one with an empty media so the title / subtitle can be used * @param {AppStoreObjectGraph} objectGraph The object graph for the app store. * @param {Data} data to build card of. * @param {TodayCardConfiguration} cardConfig flags on how to configure a given card. * @param {TodayParseContext} context to store state updated throughout parsing. */ export function fallbackWatchTodayCardFromData(objectGraph, data, cardConfig, context) { if (!objectGraph.client.isWatch) { return null; } const fallbackCard = createTodayBaseCard(objectGraph, data, cardConfig, context); fallbackCard.media = new TodayCardMediaArtwork([], [], []); return fallbackCard; } /** * Create the {EditorialDisplayOptions for a card given the MAPI data * @param objectGraph The dependency graph for the App Store * @param data The MAPI data for this today card * @param cardConfig The configuration for the card * @returns The editorialDisplayOptions for this card */ export function todayCardEditorialDisplayOptionsFromData(objectGraph, data, cardConfig) { var _a; // here is where we make the display options const editorialClientParams = extractEditorialClientParams(objectGraph, data); const isExtraWideCard = objectGraph.client.isPad && (cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.isHeroCard); const editorialDisplayOptions = { suppressTagline: mediaAttributes.attributeAsBoolean(data, "ignoreITunesShortNotes"), suppressLockup: asBoolean(editorialClientParams["suppressLockup"]), showBadgeInSmallCards: (_a = cardConfig.alwaysShowBadgeInSmallCards) !== null && _a !== void 0 ? _a : asBoolean(editorialClientParams["showBadgeInSmallCards"]), useMaterialBlur: cardConfig.alwaysUseMaterialBlur || isExtraWideCard || asBoolean(editorialClientParams["useMaterialBlur"]), }; return editorialDisplayOptions; } // MARK: - Today Configuration /** * Create the "default" card configuration that callers can generally base their configuration from. */ export function defaultTodayCardConfiguration(objectGraph) { return { useOTDTextStyle: false, enableFullScreenVideo: true, enableListCardToMultiAppFallback: true, canDisplayArcadeOfferButton: true, isHeroCard: false, }; } // MARK: - Metrics / Location Tracking /** * Allows for specialized card builders to share the same location tracking push / pop code. * * @param objectGraph The dependency graph for the App Store * @param data The media api data to get location information from * @param cardConfig The configuration for the card * @param context The parse context for the over all today page */ export function pushTodayCardLocation(objectGraph, data, cardConfig, context, titleOverride) { const title = isDefinedNonNullNonEmpty(titleOverride) ? titleOverride : todayCardTitleFromData(objectGraph, data, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle)); metricsHelpersLocation.pushContentLocation(objectGraph, todayCardMetricsOptions(objectGraph, data, cardConfig, context, title), title !== null && title !== void 0 ? title : ""); } export function todayCardMetricsOptions(objectGraph, data, cardConfig, context, titleOverride) { var _a; return metricsHelpersImpressions.impressionOptions(objectGraph, data, titleOverride !== null && titleOverride !== void 0 ? titleOverride : "", { ...cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.baseMetricsOptions, targetType: "todayCard", pageInformation: context.pageInformation, locationTracker: context.locationTracker, isAdEligible: (_a = cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.isAdEligible) !== null && _a !== void 0 ? _a : false, optimizationId: asString(data, "meta.personalizationData.optimizationId"), optimizationEntityId: asString(data, "meta.personalizationData.optimizationEntityId"), rowIndex: cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.currentRowIndex, displayStyle: cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.metricsDisplayStyle, }); } /** * Pop the today card location that was pushed by the pushTodayCardLocation function. * * @param context The parse context for the over all today page */ export function popTodayCardLocation(context) { metricsHelpersLocation.popLocation(context.locationTracker); } // MARK: - Card Title /** * @param objectGraph The dependency graph for the App Store * @param data The media api data used to determine the title of the card * @param cardDisplayStyle The display style of the card * @returns The title of the card */ export function todayCardTitleFromData(objectGraph, data, cardDisplayStyle) { let title = notesFromData(objectGraph, data, "name"); if (isNullOrEmpty(title)) { switch (cardDisplayStyle) { case TodayCardDisplayStyle.AppOfTheDay: case TodayCardDisplayStyle.GameOfTheDay: title = mediaAttributes.attributeAsString(data, "label"); break; default: break; } } return title; } // MARK: - Card Display Styles /** * These are today card types that can be coerced into an override type, there are times where we want to * have all List cards for instance, render as grids, this set denotes the types that can be coerced. */ const coercibleTodayCardStyles = new Set([ TodayCardDisplayStyle.Grid, TodayCardDisplayStyle.List, TodayCardDisplayStyle.NumberedList, TodayCardDisplayStyle.River, ]); /** * Find the today card style for the given data, allowing coercion if necessary. * @param data The media api data for an editorial item * @param styleOverride The override style to use if the data has a style that can be coerced * @returns The resolved todayCardDisplayStyle */ export function cardDisplayStyleFromData(data, styleOverride) { const cardDisplayStyle = mediaAttributes.attributeAsString(data, "cardDisplayStyle"); if (coercibleTodayCardStyles.has(cardDisplayStyle) && isSome(styleOverride)) { return styleOverride; } return cardDisplayStyle; } // MARK: - Collection Cards /** * @param objectGraph Dependency graph for the App Store * @param data MAPI data for the card these lockups are included in * @param cardConfig The configuration for the card these lockups are included in * @param context The parse context for the over all today page * @param includeLockupClickActions Whether we should include the click actions for the lockups, this is only needed * when a card allows clicks on a lockup * @returns The list of lockups for the collection displayed on a TodayCard */ export function lockupsForCollectionCardFromData(objectGraph, data, cardConfig, context, includeLockupClickActions) { const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); const filteredRelatedContent = relatedContent.filter((cardData) => { const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, cardData); const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, cardData); return !hasMessagesExtension || !isHiddenFromSpringboard; }); return lockupsForRelatedContent(objectGraph, filteredRelatedContent, cardConfig, context.pageInformation, context.locationTracker, undefined, undefined, undefined, includeLockupClickActions); } /** * @param objectGraph Dependency graph for the App Store * @param relatedContent The list of MAPI data objects to generate lockups for * @param cardConfig The configuration for the card these lockups are included in * @param pageInformation The pageInformation for the page these lockups are included in * @param locationTracker The locationTracker used for impressions on these lockups * @param offerEnvironment The offer environment to use for the lockups * @param offerStyle The offerStyle to use for the lockups * @param externalDeepLinkUrl The promotional deep link url to use on the lockup's offer. * @param includeLockupClickActions Whether we should include the click actions for the lockups, this is only needed * when a card allows clicks on a lockup * @returns The list of lockups for the collection displayed on a TodayCard */ export function lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, pageInformation, locationTracker, offerEnvironment, offerStyle, externalDeepLinkUrl, includeLockupClickActions = true) { if (isNullOrEmpty(relatedContent)) { return []; } // IAPs are only suitable for the TodayCardInAppPurchase card. That card follows a separate path, so we should // filter them out here. const filteredRelatedContent = relatedContent.filter((cardData) => { if (isDefinedNonNullNonEmpty(cardData.attributes)) { return cardData.type !== "in-apps"; } return true; }); const options = { lockupOptions: { metricsOptions: { pageInformation: pageInformation, locationTracker: locationTracker, }, offerEnvironment: offerEnvironment, offerStyle: offerStyle, clientIdentifierOverride: cardConfig.clientIdentifierOverride, externalDeepLinkUrl: externalDeepLinkUrl, crossLinkSubtitle: cardConfig.crossLinkSubtitle, artworkUseCase: 1 /* ArtworkUseCase.LockupIconSmall */, canDisplayArcadeOfferButton: cardConfig.canDisplayArcadeOfferButton, useJoeColorIconPlaceholder: cardConfig.useJoeColorIconPlaceholder, skipDefaultClickAction: !includeLockupClickActions, }, filter: 76670 /* Filter.TodayCard */, }; return lockupsFromData(objectGraph, filteredRelatedContent, options); } // MARK: - Related Content /** * @param objectGraph Dependency graph for the App Store * @param data MAPI data to get the relationship from * @returns The card-contents relationship from the MAPI data */ export function relatedCardContentsContentsFromData(objectGraph, data) { return relationshipCollection(data, "card-contents"); } // MARK: - Grid / List Collection Card Fallback /** * @param objectGraph The object graph for the app store * @returns The min icon count that a Grid card should use before falling back to fallback media */ export function gridFallbackLimit(objectGraph) { switch (objectGraph.client.deviceType) { case "tv": return 2; default: return 4; } } /** * @param objectGraph The object graph for the app store * @returns The min icon count that a List card should use before falling back to fallback media */ export function listFallbackLimit(objectGraph) { // Limits before using fallback media card for list-types and grid-types (including river). switch (objectGraph.client.deviceType) { case "tv": return 3; default: return 4; } } /** * When we do not have enough content to render a list or grid card, we will use a multi * app fallback media in its place. This also updates the click action to include a param * indicating we're using fallback media. * * @param objectGraph The object graph for the app store * @param data The MAPI data for the card * @param fallbackItems The list of lockups to use as fallback media * @param card The base card to apply the fallback media to */ export function applyMultiAppFallbackToCollectionCard(objectGraph, data, fallbackItems, card) { const extraText = notesFromData(objectGraph, data, "short"); card.media = new TodayCardMediaMultiApp(fallbackItems, extraText); card.style = "dark"; if (card.clickAction instanceof FlowAction) { const updatedPageUrl = URL.from(card.clickAction.pageUrl); updatedPageUrl.param(Parameters.showingFallbackMedia, "true"); card.clickAction.pageUrl = updatedPageUrl.build(); } } // MARK: - Editorial Artwork /** * @param objectGraph The object graph for the app store * @param cardDisplayStyle The display style for the card we're getting the key path for * @returns The keypath used to find the editorial artwork for the card */ export function editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle) { if (objectGraph.client.isWatch) { return "editorialArtwork.subscriptionHero"; } else if (objectGraph.client.isVision) { return "editorialArtwork.storyCenteredStatic16x9"; } else { switch (cardDisplayStyle) { case TodayCardDisplayStyle.AppOfTheDay: case TodayCardDisplayStyle.GameOfTheDay: return "editorialArtwork.dayCard"; case TodayCardDisplayStyle.AppEventCard: return "editorialArtwork.eventCard"; case TodayCardDisplayStyle.Video: case TodayCardDisplayStyle.FullBleedImage: return "editorialArtwork.mediaCard"; default: return "editorialArtwork.generalCard"; } } } /** * @param objectGraph The object graph for the app store * @param artworkData The artwork data to create the artwork from, from the cards media api data * @param cropCode The crop code to use for the artwork, otherwise we use the correct crop for the platform * @returns The artwork model for the card */ export function todayCardArtworkFromArtworkData(objectGraph, artworkData, cropCode) { if (isNullOrEmpty(artworkData)) { return null; } const artwork = artworkFromApiArtwork(objectGraph, artworkData, { withJoeColorPlaceholder: true, useCase: 15 /* ArtworkUseCase.TodayCardMedia */, }); if (cropCode) { artwork.crop = cropCode; } else if (objectGraph.client.isMac || objectGraph.client.isTV) { // Articles: Use new crossover crop code for macOS story card art artwork.crop = "fn"; } else { artwork.crop = "sr"; } return artwork; } /** * Returns a branded title `Artwork` model object from the API `editorialArtwork.contentLogoTrimmed` response. * This artwork can be used in place of a text title for a Today Card. * @param objectGraph The dependency graph for the App Store * @param data The media API data to fetch the Artwork from * @returns The branded title `Artwork` object, or `undefined` if a branded title could not be found */ export function brandedTitleArtworkForCard(objectGraph, data) { const brandedTitleData = mediaAttributes.attributeAsDictionary(data, "editorialArtwork.contentLogoTrimmed"); return artworkFromApiArtwork(objectGraph, brandedTitleData, { contentMode: ArtworkContentMode.scaleAspectFit, allowingTransparency: true, useCase: 17 /* ArtworkUseCase.TodayCardBrandedTitle */, }); } /** * @param objectGraph The object graph for the app store * @param data The media api data for the card * @param cardConfig The configuration for the card being built * @returns The `bgGradientKind` field from the correct artwork dictionary */ export function todayCardArtworkTitleBackingGradientForKey(objectGraph, data, cardConfig) { const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); const artworkKey = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); const artworkData = mediaAttributes.attributeAsDictionary(data, artworkKey); if (!isDefinedNonNull(artworkData)) { return null; } return asString(artworkData, "bgGradientKind"); } /** * @param objectGraph The object graph for the app store * @param artworkData The artwork data from the media api data for a card, that allows us to determine the style * @returns The light or dark style for this card, if we can determine it. */ export function todayCardStyleFromArtwork(objectGraph, artworkData) { var _a; if (isNullOrEmpty(artworkData)) { return undefined; } const joeColors = joeColorSetFromData(artworkData); const backgroundColor = joeColors.bgColor; const hasGradient = ((_a = joeColors.textGradient) === null || _a === void 0 ? void 0 : _a.length) === 2; if (!backgroundColor && !hasGradient) { return undefined; } if (objectGraph.client.isiOS || objectGraph.client.isWeb) { return cardStyleFromJoeColors(joeColors, "bgColor"); } else if (hasGradient) { // Gradient colors are the text color, so if the gradient is a dark color, then // the environment is a light environment return color.isDarkColor(joeColors.textGradient[0]) ? "light" : "dark"; } else { return color.isDarkColor(backgroundColor) ? "dark" : "light"; } } export function cardStyleFromJoeColorsWithoutFallback(joeColors, preferredColorKeypath = "bgColor") { if (isNothing(joeColors)) { return undefined; } if (isSome(joeColors === null || joeColors === void 0 ? void 0 : joeColors.textGradient) && joeColors.textGradient.length === 2) { // Rare, but if gradient colors are present, defer to them return color.isDarkColor(joeColors.textGradient[0]) ? "white" : "dark"; } const preferredColor = joeColors[preferredColorKeypath]; if (isNothing(preferredColor)) { return undefined; } const luminance = color.luminanceFrom(preferredColor); if (luminance <= 0.1) { return "dark"; } else if (luminance >= 0.84) { return "white"; } else { return "light"; } } export function cardStyleFromJoeColors(joeColors, preferredColorKeypath = "bgColor") { var _a; return (_a = cardStyleFromJoeColorsWithoutFallback(joeColors, preferredColorKeypath)) !== null && _a !== void 0 ? _a : "light"; } // MARK: - Editorial Text /** * Determines the heading for the card, were it to appear inline. * If the source card heading is null, we default to the existing * inline heading, if any. * @param objectGraph The object graph for the app store * @param card The today card we're collapsing the heading for * @returns string The title to use for the card. */ export function collapsedHeadingForTodayCard(objectGraph, card) { if (isDefinedNonNull(card.heading)) { return card.heading.replace(/\n/g, " "); } return card.collapsedHeading; } // MARK: - Offers /** * Determines an appropriate OfferStyle for a TodayCard to ensure correct * contrast with the card's contents. * * @param cardStyle The today style for the card * @returns an appropriate offer style to use for the button */ export function offerStyleForTodayCard(objectGraph, cardStyle) { return "transparent"; } /** * Determines an appropriate OfferEnvironment for a TodayCard to ensure correct * contrast with the card's contents. * * @param cardStyle The today style for the card * @returns an appropriate offer environment to use for the button */ export function offerEnvironmentForTodayCard(cardStyle) { if (cardStyle === "white") { return "light"; } else { return "todayCard"; } } // MARK: - Hero Media /** * For mac and tv platforms, we add hero media to the today cards. * * @param objectGraph The dependency graph for the app store * @param card The today card we're adding hero media to * @param cardConfig The config used to create this card * @param data The media api data used to create this card */ export function addHeroMediaToTodayCardIfNecessary(objectGraph, card, cardConfig, data) { const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); // MacOS and tvOS: Hero const heroArt = todayCardHeroArtForData(objectGraph, data, cardConfig.heroDisplayContext, cardDisplayStyle, cardConfig.prevailingCropCodes); let artworks = []; let videos = []; if (isDefinedNonNull(heroArt)) { artworks = [heroArt]; } if (isDefinedNonNull(card.media) && (card.media instanceof TodayCardMediaVideo || card.media instanceof TodayCardMediaArtwork)) { videos = card.media.videos; } const hero = new TodayCardMediaHero(artworks, videos); const supportsHeroMedia = objectGraph.client.isMac || objectGraph.client.isTV || objectGraph.client.isWeb || (objectGraph.client.isVision && !cardConfig.isSearchContext) || preprocessor.GAMES_TARGET; if (supportsHeroMedia && hero.isValid()) { card.heroMedia = hero; // Ensure we update the card styling. We prefer hero art, and fallback to hero video. const heroArtworkData = mediaAttributes.attributeAsDictionary(data, heroMediaArtworkKeyForContext(objectGraph, cardConfig.heroDisplayContext, cardDisplayStyle, Object.keys(mediaAttributes.attributeAsDictionary(data, "editorialArtwork")))); let style = todayCardStyleFromArtwork(objectGraph, heroArtworkData); if (!isDefinedNonNull(style)) { const heroVideoPreviewData = asDictionary(todayCardHeroVideoFromData(objectGraph, data), "previewFrame"); style = todayCardStyleFromArtwork(objectGraph, heroVideoPreviewData); } card.style = style; } } /** * For external links we will display the standard link overlay in place of whatever overlay we had previously * * @param card The today card we're adding hero media to */ export function addExternalLinkOverlayToTodayCardIfNecessary(card) { if (card.clickAction instanceof ExternalUrlAction) { card.overlay = new TodayCardActionOverlay(card.clickAction); card.style = "white"; } } /** * @param objectGraph The dependency graph for the app store * @param data The media api data to search for hero art in * @param context The context in which this hero media will be displayed * @param displayStyle The display style of the card * @param prevailingCropCodes The prevailing crop code to use for inline hero media * @returns The artwork to be used in the hero position for a today card */ export function todayCardHeroArtForData(objectGraph, data, context, displayStyle, prevailingCropCodes) { const heroArtworkKey = heroMediaArtworkKeyForContext(objectGraph, context, displayStyle, Object.keys(mediaAttributes.attributeAsDictionary(data, "editorialArtwork"))); const heroArtworkData = mediaAttributes.attributeAsDictionary(data, heroArtworkKey); return todayCardArtworkFromArtworkData(objectGraph, heroArtworkData, heroMediaArtworkCropForContext(objectGraph, context, objectPathToString(heroArtworkKey), prevailingCropCodes)); } /** * @param objectGraph The dependency graph for the app store * @param context The context in which this hero media will be displayed * @param displayStyle The display style of the card * @returns The key to use to find the hero media artwork */ function heroMediaArtworkKeyForContext(objectGraph, context, displayStyle, availableArtworkKeys) { if (context === "article" && (objectGraph.client.isMac || objectGraph.client.isiOS)) { // Use "iOS" art return editorialArtKeyPathForCardDisplayStyle(objectGraph, displayStyle); } else if (objectGraph.client.isVision) { return "editorialArtwork.heroStatic16x9"; } else { if (objectGraph.client.isTV && availableArtworkKeys.includes("categoryDetailStatic16x9")) { return "editorialArtwork.categoryDetailStatic16x9"; } return "editorialArtwork.crossoverCard"; } } /** * @param objectGraph The dependency graph for the app store * @param context The context in which this hero media will be displayed * @param prevailingCropCodes The crop code to use if we're not overriding it * @returns The crop code to use for hero artwork in a given context */ function heroMediaArtworkCropForContext(objectGraph, context, heroArtworkKey, prevailingCropCodes) { if (context === HeroMediaDisplayContext.Article && objectGraph.client.isMac) { // Use "iOS" art crop code return "fn"; } else if (context === HeroMediaDisplayContext.Article && objectGraph.client.isTV && heroArtworkKey === "editorialArtwork.categoryDetailStatic16x9") { return "sr"; } else { return prevailingCropCodes === null || prevailingCropCodes === void 0 ? void 0 : prevailingCropCodes.defaultCrop; } } /** * @param objectGraph The dependency graph for the app store * @param data The media api data to search for hero video in * @returns The video to be used in the hero position for a today card */ function todayCardHeroVideoFromData(objectGraph, data) { let videoData; const videoData4x3 = mediaAttributes.attributeAsDictionary(data, "editorialVideo.storeFrontVideo4x3"); const videoData16x9 = mediaAttributes.attributeAsDictionary(data, "editorialVideo.storeFrontVideo"); // Today: Video Card: Incorrect key for 4x3 videos // On pad prefer 4x3 fallback to 16x9 and on phone vice versa if (objectGraph.client.isPad || objectGraph.client.screenSize.isEqualTo(screenSizeIPhone134)) { videoData = videoData4x3 || videoData16x9; } else { videoData = videoData16x9 || videoData4x3; } return videoData; } // MARK: - Crosslinks /** * Determines the cross link subtitle from card data. */ export function crossLinkSubtitleFromData(objectGraph, data) { // Start with short notes let subtitle = editorialNotesFromData(objectGraph, data, "short"); // Fallback to name if (!isDefinedNonNullNonEmpty(subtitle)) { subtitle = notesFromData(objectGraph, data, "name"); } // Fallback to label if (!isDefinedNonNullNonEmpty(subtitle)) { const cardDisplayStyle = mediaAttributes.attributeAsString(data, "displayStyle"); if (cardDisplayStyle === TodayCardDisplayStyle.AppOfTheDay || cardDisplayStyle === TodayCardDisplayStyle.GameOfTheDay) { subtitle = mediaAttributes.attributeAsString(data, "label"); } } return subtitle; } // MARK: - Client Identifier Overrids function shouldRespectClientIdentifierOverride(objectGraph, data, clientIdentifier, cardConfig) { var _a; if (clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */ || clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */) { // If using the companion app, it's expected that we always respect the given identifier. return true; } const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); // Do not apply the override if this is not a Content type card. const cardTypesRequiringClientIdentifierOverride = new Set([ TodayCardDisplayStyle.AppEventCard, TodayCardDisplayStyle.Grid, TodayCardDisplayStyle.InAppPurchase, TodayCardDisplayStyle.List, TodayCardDisplayStyle.NumberedList, TodayCardDisplayStyle.River, TodayCardDisplayStyle.ShortImage, TodayCardDisplayStyle.SingleApp, ]); if (!cardTypesRequiringClientIdentifierOverride.has(cardDisplayStyle)) { return false; } // Grids with >= 6 items are unsupported if (relatedContent.length >= 6 && cardDisplayStyle === TodayCardDisplayStyle.Grid) { return false; } // Content cards with Artwork are unsupported const artworkKey = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); if (!mediaAttributes.attributeAsBooleanOrFalse(data, "ignoreEditorialArt") && todayCardArtworkFromArtworkData(objectGraph, mediaAttributes.attributeAsDictionary(data, artworkKey), (_a = cardConfig.prevailingCropCodes) === null || _a === void 0 ? void 0 : _a.defaultCrop)) { return false; } // Content cards with iAP are unsupported if (inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent)) { return false; } return true; } function inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent) { if (relatedContent.length === 1) { const contentData = relatedContent[0]; if (contentData.type === "in-apps") { return contentData; } } return null; } /** * If a card should be using the OTD style we need to make sure we set that on the card * * @param card The today card we're adding the style to * @param cardConfig The config used to create this card */ function addOTDStyleToCardIfNecessary(card, cardConfig) { if (isNothing(card.media)) { return; } card.media.otdTextStyle = cardConfig.useOTDTextStyle; } /** * Check if this card should have the flip and blur enabled, this is the case when a card has videos. * @param objectGraph The dependency graph for the app store * @param card The today card we're modifying */ function enableFlipAndBlurIfNecessary(objectGraph, card) { const cardMedia = card.media; const cardMediaHasArtwork = cardMedia instanceof TodayCardMediaWithArtwork; if (!cardMediaHasArtwork) { return; } const cardMdiaWithArtwork = cardMedia; const hasVideos = isDefinedNonNullNonEmpty(cardMdiaWithArtwork.videos); card.supportsMediaMirroring = hasVideos; } /** * @param data The media API data used to determine the intention of the card * @param cardConfig The configuration for the card * @returns Whether this app is an App of the Day or Game of the Day */ export function isCardOTDIntention(data, cardConfig) { let otdIntention = mediaAttributes.attributeAsString(data, "ofTheDayIntent"); if (isNothing(otdIntention)) { const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.coercedCollectionTodayCardDisplayStyle); switch (cardDisplayStyle) { case TodayCardDisplayStyle.AppOfTheDay: otdIntention = OfTheDayIntention.AppOfTheDay; break; case TodayCardDisplayStyle.GameOfTheDay: otdIntention = OfTheDayIntention.GameOfTheDay; break; default: break; } } return otdIntention === OfTheDayIntention.AppOfTheDay || otdIntention === OfTheDayIntention.GameOfTheDay; } //# sourceMappingURL=today-card-util.js.map