summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js785
1 files changed, 785 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js
new file mode 100644
index 0000000..688f0fb
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js
@@ -0,0 +1,785 @@
+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) {
+ // <rdar://problem/39155084> 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");
+ // <rdar://problem/34339194> 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 \ No newline at end of file