summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/content/content.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/content/content.js')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/content.js2820
1 files changed, 2820 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/content.js b/node_modules/@jet-app/app-store/tmp/src/common/content/content.js
new file mode 100644
index 0000000..72b5ae1
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/content.js
@@ -0,0 +1,2820 @@
+import * as validation from "@jet/environment/json/validation";
+import { isNothing, isSome, unwrapOptional as unwrap } from "@jet/environment/types/optional";
+import * as models from "../../api/models";
+import * as modelsBase from "../../api/models/base";
+import * as modelsShelves from "../../api/models/shelves";
+import { ads } from "../../api/typings/constants";
+import * as derivedData from "../../foundation/json-parsing/derived-data";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as mediaDataFetching from "../../foundation/media/data-fetching";
+import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes";
+import * as mediaRelationship from "../../foundation/media/relationships";
+import * as mediaUrlBuilder from "../../foundation/media/url-builder";
+import { Parameters, Path, Protocol } from "../../foundation/network/url-constants";
+import * as color from "../../foundation/util/color-util";
+import * as dateUtil from "../../foundation/util/date-util";
+import { unreachable } from "../../foundation/util/errors";
+import { isDefinedNonNullNonEmpty } from "@apple-media-services/media-api";
+import { editorialCardFromData } from "../../foundation/media/associations";
+import * as client from "../../foundation/wrappers/client";
+import * as videoDefaults from "../constants/video-constants";
+import * as filtering from "../filtering";
+import * as lockups from "../lockups/lockups";
+import * as metricsHelpersImpressions from "../metrics/helpers/impressions";
+import * as metricsHelpersLocation from "../metrics/helpers/location";
+import * as metricsHelpersMedia from "../metrics/helpers/media";
+import * as offers from "../offers/offers";
+import * as productPageVariants from "../product-page/product-page-variants";
+import * as artwork from "./artwork/artwork";
+import * as contentAttributes from "./attributes";
+import * as contentDeviceFamily from "./device-family";
+import * as sad from "./sad";
+import { isFeatureEnabledForCurrentUser } from "../util/lottery";
+class RunnabilityInfo {
+ constructor() {
+ this.runsOnIntel = true;
+ this.runsOnAppleSilicon = true;
+ this.requiresRosetta = false;
+ }
+}
+/**
+ * Determines a reasonable artwork use case from a given shelf style
+ *
+ * @param shelfStyle The shelf style to consider
+ */
+export function artworkUseCaseFromShelfStyle(objectGraph, shelfStyle) {
+ switch (shelfStyle) {
+ case "inAppPurchaseLockup":
+ case "appShowcase":
+ case "smallLockup": {
+ return 1 /* ArtworkUseCase.LockupIconSmall */;
+ break;
+ }
+ case "mediumLockup": {
+ return 2 /* ArtworkUseCase.LockupIconMedium */;
+ break;
+ }
+ case "largeLockup": {
+ return 3 /* ArtworkUseCase.LockupIconLarge */;
+ break;
+ }
+ default: {
+ return 0 /* ArtworkUseCase.Default */;
+ }
+ }
+}
+/**
+ * Convert an API artwork object into an Artwork model object.
+ * @param artwork The artwork in API format.
+ * @returns An `Artwork` object.
+ */
+export function artworkFromApiArtwork(objectGraph, artworkData, options) {
+ return validation.context("artworkFromApiArtwork", () => {
+ var _a, _b, _c;
+ const allowingTransparency = serverData.isDefinedNonNull(options.allowingTransparency)
+ ? options.allowingTransparency
+ : false;
+ const useJoeColorDefault = objectGraph.client.isVision || objectGraph.client.isWeb;
+ const withJoeColorPlaceholder = serverData.isDefinedNonNull(options.withJoeColorPlaceholder)
+ ? options.withJoeColorPlaceholder
+ : useJoeColorDefault;
+ const artworkUrl = serverData.asString(artworkData, "url");
+ if (serverData.isNull(artworkUrl)) {
+ return null;
+ }
+ // Whether wide gamut is supported
+ const supportsWideGamut = serverData.asBooleanOrFalse(artworkData, "hasP3");
+ // Add base variant
+ const variants = [
+ artwork.createArtworkVariantForClient(objectGraph, allowingTransparency, supportsWideGamut, options.useCase),
+ ];
+ // Add layered image variant
+ const supportsLayeredImage = serverData.asBooleanOrFalse(artworkData, "supportsLayeredImage");
+ if (supportsLayeredImage && (objectGraph.client.isTV || objectGraph.client.isVision)) {
+ variants.push(artwork.createArtworkVariantForFormat(objectGraph, "lcr", supportsWideGamut, options.useCase));
+ }
+ // Artwork Placeholder Color
+ // If we indicate the image could be transparent then we don't want a placeholder background
+ let placeholderBackgroundColor = null;
+ if (allowingTransparency) {
+ placeholderBackgroundColor = color.named("clear");
+ }
+ else if (withJoeColorPlaceholder) {
+ const joeColorHexSet = joeColorHexSetFromData(artworkData);
+ const placeholderColorHex = (_b = (_a = options.joeColorPlaceholderSelectionLogic) === null || _a === void 0 ? void 0 : _a.call(options, joeColorHexSet)) !== null && _b !== void 0 ? _b : serverData.asString(artworkData, "bgColor");
+ const apiBackgroundColor = color.fromHex(placeholderColorHex);
+ if (!serverData.isNull(apiBackgroundColor)) {
+ placeholderBackgroundColor = apiBackgroundColor;
+ }
+ }
+ // If we don't want clear, joe color, or joe color fails to parse then fall back to default background
+ if (serverData.isNull(placeholderBackgroundColor) && !objectGraph.client.isVision) {
+ placeholderBackgroundColor = color.named("placeholderBackground");
+ }
+ const textColorKey = (_c = options.overrideTextColorKey) !== null && _c !== void 0 ? _c : "textColor1";
+ const apiTextColor = color.fromHex(serverData.asString(artworkData, textColorKey));
+ const artworkModel = new modelsBase.Artwork(artworkUrl, options.overrideWidth || serverData.asNumber(artworkData, "width"), options.overrideHeight || serverData.asNumber(artworkData, "height"), variants);
+ artworkModel.backgroundColor = placeholderBackgroundColor;
+ artworkModel.checksum = serverData.asString(artworkData, "checksum");
+ if (serverData.isDefinedNonNull(apiTextColor)) {
+ artworkModel.textColor = apiTextColor;
+ }
+ if (serverData.isDefinedNonNull(options.style)) {
+ artworkModel.style = options.style;
+ }
+ if (serverData.isDefinedNonNull(options.cropCode)) {
+ artworkModel.crop = options.cropCode;
+ }
+ if (serverData.isDefinedNonNull(options.contentMode)) {
+ artworkModel.contentMode = options.contentMode;
+ }
+ return artworkModel;
+ });
+}
+export function impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions) {
+ return validation.context("impressionableAppIconFromData", () => {
+ const rawArtwork = iconFromData(objectGraph, data, artworkOptions);
+ if (!serverData.isDefinedNonNull(rawArtwork)) {
+ return null;
+ }
+ const icon = new models.ImpressionableArtwork(rawArtwork);
+ const title = mediaAttributes.attributeAsString(data, "name");
+ const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, title, metricsOptions);
+ metricsHelpersImpressions.addImpressionFields(objectGraph, icon, metricsImpressionOptions);
+ return icon;
+ });
+}
+/**
+ * Batch method for `impressionableAppIconFromData`. Doesn't push location stack or increment location counter, matching other behavior with other icon grids.
+ * @param dataCollection Data container array with app data.
+ * @param metricsOptions Metrics blob containing information about page and location.
+ * @returns Array of `ImpressionableArtwork`
+ */
+export function impressionableAppIconsFromDataCollection(objectGraph, dataCollection, metricsOptions, artworkOptions) {
+ return validation.context("impressionableAppIconFromData", () => {
+ const icons = [];
+ if (serverData.isNullOrEmpty(metricsOptions.targetType)) {
+ metricsOptions.targetType = "artwork";
+ }
+ for (const data of dataCollection) {
+ const icon = impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions);
+ if (icon) {
+ icons.push(icon);
+ metricsHelpersLocation.nextPosition(metricsOptions.locationTracker);
+ }
+ }
+ return icons;
+ });
+}
+/**
+ * Defines possible use cases for SearchChartOrCategoryBrick.
+ */
+export var SearchChartOrCategoryBrickUseCase;
+(function (SearchChartOrCategoryBrickUseCase) {
+ SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["seeAllPage"] = 0] = "seeAllPage";
+ SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["categoryBreakout"] = 1] = "categoryBreakout";
+ SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["other"] = 2] = "other";
+})(SearchChartOrCategoryBrickUseCase || (SearchChartOrCategoryBrickUseCase = {}));
+/**
+ * Gets all possible artwork that this chart or category can show
+ * @param objectGraph
+ * @param data
+ * @param isForSeeAllPage Whether or not the chart or category is on the see-all page or not;
+ * this is because the see-all page should always have the `Density1` style
+ * @param style The style of the chart or category that will be rendered
+ * @returns All permutations of artowrk that the chart or category can show
+ */
+export function searchChartOrCategoryArtworkFromData(objectGraph, data, useCase, style) {
+ const artworkPath = "editorialArtwork.searchCategoryBrick";
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath);
+ if (serverData.isNullOrEmpty(artworkData)) {
+ return null;
+ }
+ let artworkStyle = style;
+ if (useCase === SearchChartOrCategoryBrickUseCase.seeAllPage) {
+ artworkStyle = models.GenericSearchPageShelfDisplayStyleDensity.Density1;
+ }
+ /// Crops = [LTR crop, RTL crop]
+ /// ContentModes = [ContentMode for LTR crop, ContentMode for RTL crop]
+ /// Note: These must be the same length
+ let crops = [];
+ let contentModes = [];
+ switch (artworkStyle) {
+ /// Tile
+ case models.GenericSearchPageShelfDisplayStyleDensity.Density1:
+ const width = useCase === SearchChartOrCategoryBrickUseCase.categoryBreakout ? "1191" : "2350";
+ artworkData["width"] = width;
+ artworkData["height"] = "670";
+ crops = ["SCB.ApSCBL01", "SCB.ApSCBL03"];
+ contentModes = [modelsBase.ArtworkContentMode.right, modelsBase.ArtworkContentMode.left];
+ break;
+ /// Pill
+ case models.GenericSearchPageShelfDisplayStyleDensity.Density2:
+ artworkData["width"] = "2482";
+ artworkData["height"] = "670";
+ crops = ["SCB.ApSCBS01", "SCB.ApSCBS02"];
+ contentModes = [modelsBase.ArtworkContentMode.left, modelsBase.ArtworkContentMode.right];
+ break;
+ /// Round
+ case models.GenericSearchPageShelfDisplayStyleDensity.Density3:
+ artworkData["width"] = "670";
+ artworkData["height"] = "670";
+ crops = ["cc"];
+ contentModes = [modelsBase.ArtworkContentMode.scaleAspectFit];
+ break;
+ default:
+ break;
+ }
+ return crops.map((crop, index) => {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode: crop,
+ contentMode: index < contentModes.length ? contentModes[index] : null,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ withJoeColorPlaceholder: true,
+ });
+ });
+}
+/**
+ * Create an icon artwork from the provided data.
+ * @param objectGraph The object graph.
+ * @param data The data object to pull icon data from.
+ * @param artworkOptions The options for creating the artwork.
+ * @param clientIdentifierOverride A client identifier override.
+ * @param productVariantData The product variant data to use to select the icon.
+ * @param attributePlatformOverride An override platform, from which to fetch the icon.
+ * @returns An `Artwork` object representing the icon.
+ */
+export function iconFromData(objectGraph, data, artworkOptions, clientIdentifierOverride, productVariantData, attributePlatformOverride = undefined) {
+ return validation.context("iconFromData", () => {
+ if (!data) {
+ validation.unexpectedNull("ignoredValue", "data");
+ return null;
+ }
+ const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : iconAttributePlatform(objectGraph, data, clientIdentifierOverride);
+ const usePrerenderedIconArtwork = shouldUsePrerenderedIconArtwork(objectGraph);
+ // The preferred client identifier to use when selecting the artwork.
+ // This client identifier here ensures that we always prefer pill artwork for messages and circular artwork for watch / vision.
+ // Unless there's an override specified where another artwork type needs to be used (for example, in developer pages).
+ const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier;
+ // Watch
+ const watchIcon = watchIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, usePrerenderedIconArtwork, attributePlatform);
+ if (isSome(watchIcon)) {
+ return watchIcon;
+ }
+ // Messages
+ const messagesIcon = messagesIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, attributePlatform);
+ if (isSome(messagesIcon)) {
+ return messagesIcon;
+ }
+ // In-App Purchases
+ const iapIcon = inAppPurchaseIconFromData(objectGraph, data, artworkOptions);
+ if (isSome(iapIcon)) {
+ return iapIcon;
+ }
+ // Bundles
+ const bundlesIcon = bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork);
+ if (isSome(bundlesIcon)) {
+ return bundlesIcon;
+ }
+ // Calculate variant data if one wasn't provided from caller.
+ if (serverData.isNull(productVariantData)) {
+ productVariantData = productPageVariants.productVariantDataForData(objectGraph, data);
+ }
+ const artworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "artwork", attributePlatform);
+ // tvOS
+ const tvIcon = tvIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform);
+ if (isSome(tvIcon)) {
+ return tvIcon;
+ }
+ // visionOS
+ const visionIcon = visionIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform);
+ if (isSome(visionIcon)) {
+ return visionIcon;
+ }
+ // macOS & iOS
+ return macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform);
+ });
+}
+/**
+ * Determines if a client is capable of showing pre-rendered icon artwork, and if the relevant
+ * feature / bag flags are enabled.
+ * @param objectGraph Current object graph
+ * @returns True if we should use prerendered icon artwork.
+ */
+export function shouldUsePrerenderedIconArtwork(objectGraph) {
+ const clientSupportsPrerenderedIconArtwork = objectGraph.client.isWatch || objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isWeb;
+ const isEnabledForUser = isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.iconArtworkRolloutRate);
+ return (isEnabledForUser &&
+ objectGraph.bag.enableIconArtwork &&
+ objectGraph.client.isIconArtworkCapable &&
+ clientSupportsPrerenderedIconArtwork);
+}
+function watchIconFromData(objectGraph, data, artworkOptions, clientIdentifier, usePrerenderedIconArtwork, attributePlatform) {
+ if (clientIdentifier !== client.watchIdentifier &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS") &&
+ !objectGraph.client.isWatch) {
+ return null;
+ }
+ // Attempt to use pre-rendered circular icon artwork first, if applicable
+ if (usePrerenderedIconArtwork) {
+ const iconArtworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularIconArtwork");
+ if (isSome(iconArtworkData)) {
+ return artworkFromApiArtwork(objectGraph, iconArtworkData, {
+ ...artworkOptions,
+ style: "roundPrerendered",
+ cropCode: "bb",
+ withJoeColorPlaceholder: true,
+ });
+ }
+ }
+ // Fallback to the legacy icon artwork
+ const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularArtwork");
+ if (isSome(artworkData)) {
+ const style = usePrerenderedIconArtwork ? "roundPrerendered" : "round";
+ const cropCode = usePrerenderedIconArtwork ? "ic" : undefined;
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: style,
+ cropCode: cropCode,
+ withJoeColorPlaceholder: true,
+ });
+ }
+ return null;
+}
+function messagesIconFromData(objectGraph, data, artworkOptions, clientIdentifier, attributePlatform) {
+ const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data);
+ const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data);
+ const shouldShowMessagesIcon = hasMessagesExtension && (clientIdentifier === client.messagesIdentifier || isHiddenFromSpringboard);
+ const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "ovalArtwork");
+ if (shouldShowMessagesIcon && serverData.isDefinedNonNull(artworkData)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "pill",
+ });
+ }
+ return null;
+}
+function inAppPurchaseIconFromData(objectGraph, data, artworkOptions) {
+ if (data.type !== "in-apps") {
+ return null;
+ }
+ const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork");
+ if (isSome(artworkData)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "iap",
+ });
+ }
+ return null;
+}
+function bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork) {
+ if (data.type !== "app-bundles") {
+ return null;
+ }
+ // Attempt to use pre-rendered icon artwork first, if applicable
+ if (usePrerenderedIconArtwork) {
+ const iconArtworkData = mediaAttributes.attributeAsDictionary(data, "iconArtwork");
+ if (isSome(iconArtworkData)) {
+ return artworkFromApiArtwork(objectGraph, iconArtworkData, {
+ ...artworkOptions,
+ style: "roundedRectPrerendered",
+ cropCode: "bb",
+ });
+ }
+ }
+ // Fallback to the legacy icon artwork
+ const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork");
+ if (isSome(artworkData)) {
+ const style = usePrerenderedIconArtwork ? "roundedRectPrerendered" : "roundedRect";
+ const cropCode = usePrerenderedIconArtwork ? "ia" : undefined;
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: style,
+ cropCode: cropCode,
+ allowingTransparency: true,
+ });
+ }
+ return null;
+}
+function tvIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) {
+ if (attributePlatform !== "appletvos" && clientIdentifier !== client.tvIdentifier) {
+ return null;
+ }
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "tvRect",
+ });
+}
+function visionIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) {
+ if (attributePlatform !== "xros" && clientIdentifier !== "VisionAppStore" /* ClientIdentifier.VisionAppStore */) {
+ return null;
+ }
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "round",
+ });
+}
+function macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform) {
+ const isMac = attributePlatform === "osx";
+ const allowTransparency = isMac && !preprocessor.GAMES_TARGET;
+ // Attempt to use pre-rendered icon artwork first, if applicable
+ if (usePrerenderedIconArtwork) {
+ const iconArtworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "iconArtwork", attributePlatform);
+ if (isSome(iconArtworkData)) {
+ return artworkFromApiArtwork(objectGraph, iconArtworkData, {
+ ...artworkOptions,
+ style: "roundedRectPrerendered",
+ cropCode: "bb",
+ allowingTransparency: allowTransparency,
+ });
+ }
+ }
+ // Fallback to the standard icon artwork
+ let style;
+ let cropCode;
+ if (usePrerenderedIconArtwork) {
+ style = "roundedRectPrerendered";
+ cropCode = isMac ? "ib" : "ia";
+ }
+ else {
+ style = isMac ? "unadorned" : "roundedRect";
+ cropCode = "bb";
+ }
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: style,
+ cropCode: cropCode,
+ allowingTransparency: allowTransparency,
+ });
+}
+/**
+ * Determines the best attribute platform to use for the icon.
+ * @param objectGraph Current object graph
+ * @param data The product data
+ * @param clientIdentifierOverride The client identifier override to use, if any
+ * @returns
+ */
+export function iconAttributePlatform(objectGraph, data, clientIdentifierOverride) {
+ switch (clientIdentifierOverride) {
+ case client.watchIdentifier:
+ case client.messagesIdentifier: {
+ return "ios";
+ }
+ case client.tvIdentifier: {
+ return "appletvos";
+ }
+ case "VisionAppStore" /* ClientIdentifier.VisionAppStore */: {
+ return "xros";
+ }
+ default: {
+ return contentAttributes.bestAttributePlatformFromData(objectGraph, data, clientIdentifierOverride);
+ }
+ }
+}
+/**
+ * Determine the media platform, given the app platform and screenshot type.
+ * @param appPlatform The app platform specific to this media.
+ * @param type The response screenshot type, which is applicable for both screenshots and trailers, for this media.
+ * @param supplementaryAppPlatforms
+ * @returns {MediaPlatform} The configured media platform object.
+ * TODO: legacy_export
+ */
+export function mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type, supplementaryAppPlatforms) {
+ if (!appPlatform) {
+ return null;
+ }
+ const systemImageName = systemImageNameForAppPlatform(appPlatform);
+ const deviceCornerRadius = deviceCornerRadiusFactorForMediaType(objectGraph, type);
+ const deviceBorderThickness = deviceBorderThicknessForMediaType(objectGraph, type);
+ const outerDeviceCornerRadius = deviceOuterCornerRadiusFactorForMediaType(objectGraph, type);
+ return new modelsBase.MediaPlatform(appPlatform, type, systemImageName, supplementaryAppPlatforms, deviceCornerRadius, deviceBorderThickness, outerDeviceCornerRadius);
+}
+/**
+ * Configures the trailers object from the platform data.
+ * @param data The platform data to use.
+ * @param videoConfiguration config to use for the trailers
+ * @param metricsOptions The metrics options to use.
+ * @param adamId The adamId for the lockup.
+ * @param isAd Whether the trailers are for an ad lockup. Defaults to false.
+ * @param cropCode The crop code to use for the video preview.
+ * @returns {Trailers} The configured trailers object.
+ */
+export function trailersFromData(objectGraph, data, videoConfiguration, metricsOptions, adamId, isAd = false, cropCode) {
+ const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoConfiguration, null, null, isAd, cropCode);
+ if (!platformVideos) {
+ return null;
+ }
+ const videoPreviews = platformVideos.videos;
+ const trailerVideos = [];
+ if (videoPreviews && videoPreviews.length > 0) {
+ for (const trailerVideo of videoPreviews) {
+ metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, trailerVideo, {
+ ...metricsOptions,
+ id: adamId,
+ });
+ trailerVideos.push(trailerVideo);
+ }
+ }
+ let trailers = null;
+ if (trailerVideos.length > 0) {
+ trailers = new modelsShelves.Trailers();
+ trailers.videos = trailerVideos;
+ trailers.mediaPlatform = platformVideos.mediaPlatform;
+ }
+ return trailers;
+}
+/**
+ * A convenience class for encapsulating a `Video` that is tied to a specific `MediaPlatform`.
+ */
+class PlatformVideos {
+ constructor(videos, mediaPlatform) {
+ this.videos = videos;
+ this.mediaPlatform = mediaPlatform;
+ }
+}
+/**
+ * Finds the best platform video previews to use for the given parameters.
+ * @param data The data from which to derive the platform videos.
+ * @param videoConfiguration A video configuration to use for the videos
+ * @param includedAppPlatforms If provided, restricts the resulting platform videos to only these platforms
+ * @param productVariantData
+ * @param isAd Whether the video preview data is for an ad. Defaults to false.
+ * @param cropCode The crop code to use for the preview artwork.
+ * @returns The best available platform videos.
+ */
+export function platformVideoPreviewFromData(objectGraph, data, videoConfiguration, includedAppPlatforms = null, productVariantData = null, isAd = false, cropCode) {
+ return validation.context("platformVideoPreviewFromData", () => {
+ if (serverData.isNull(productVariantData)) {
+ productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // create variant data if not provided.
+ }
+ const videoPreviewsByTypeData = videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd);
+ const videoPreviewsByType = {};
+ if (!videoPreviewsByTypeData) {
+ return null;
+ }
+ let sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, objectGraph.host.clientIdentifier, objectGraph.client.deviceType);
+ if (serverData.isDefinedNonNull(includedAppPlatforms)) {
+ // If we have a restricted set of included app platforms, use those platforms
+ // to build our sortedAppPlatforms array in the proper sort order
+ const includedSortedAppPlatforms = [];
+ for (const appPlatform of sortedAppPlatforms) {
+ if (includedAppPlatforms.includes(appPlatform)) {
+ includedSortedAppPlatforms.push(appPlatform);
+ }
+ }
+ sortedAppPlatforms = includedSortedAppPlatforms;
+ }
+ if (sortedAppPlatforms.length === 0) {
+ return null;
+ }
+ for (const appPlatform of sortedAppPlatforms) {
+ const types = mediaTypesForAppPlatform(objectGraph, appPlatform, objectGraph.client.screenSize);
+ for (const type of Object.keys(videoPreviewsByTypeData)) {
+ const videosDataForType = serverData.asArrayOrEmpty(videoPreviewsByTypeData, type);
+ const videosForType = [];
+ for (const video of videosDataForType) {
+ const previewFrame = serverData.asDictionary(video, "previewFrame");
+ if (!previewFrame) {
+ validation.unexpectedNull("ignoredValue", "object", `videoPreviewsByType.${type}.previewFrame`);
+ continue;
+ }
+ const videoUrl = serverData.asString(video, "video");
+ if (!videoUrl) {
+ validation.unexpectedNull("ignoredValue", "string", `videoPreviewsByType.${type}.video`);
+ continue;
+ }
+ const preview = artwork.createArtworkForResource(objectGraph, serverData.asString(previewFrame, "url"), serverData.asNumber(previewFrame, "width"), serverData.asNumber(previewFrame, "height"), null, null, serverData.asString(previewFrame, "checksum"));
+ if (serverData.isDefinedNonNull(cropCode)) {
+ preview.crop = cropCode;
+ }
+ videosForType.push(new modelsBase.Video(videoUrl, preview, videoConfiguration));
+ }
+ videoPreviewsByType[type] = videosForType;
+ }
+ for (const type of types) {
+ if (videoPreviewsByType[type]) {
+ return new PlatformVideos(videoPreviewsByType[type], mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type));
+ }
+ }
+ }
+ return null;
+ });
+}
+/**
+ * Configures the videos from some platform data.
+ * @param data The store platform data.
+ * @returns A list of `Video` objects.
+ */
+export function videoPreviewsFromData(objectGraph, data) {
+ return validation.context("videoPreviewsFromApiPlatformData", () => {
+ const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoDefaults.defaultVideoConfiguration(objectGraph));
+ if (platformVideos) {
+ return platformVideos.videos;
+ }
+ else {
+ return [];
+ }
+ });
+}
+/**
+ * Determines the `AppPlatform` to use, in order to determine appropriate `MediaType` for media.
+ * @param {AppPlatform} appPlatform The underlying `AppPlatform`.
+ * @returns {AppPlatform} The `AppPlatform` that we map to in order to select the appropriate `MediaType`.
+ */
+function selectionAppPlatformFromAppPlatform(objectGraph, appPlatform) {
+ if (appPlatform === "messages") {
+ switch (objectGraph.client.deviceType) {
+ case "pad": {
+ return "pad";
+ }
+ default: {
+ return "phone";
+ }
+ }
+ }
+ return appPlatform;
+}
+/**
+ * Provide the caller with an ordered array of screenshots for a given context. The first object can be used in search
+ * lockups, and the array will only contain screenshots for the supported app platforms.
+ *
+ * @param data The api product data (containing supported app platforms and screenshots)
+ * @param useCase
+ * @param includedAppPlatforms Optionally, a list of app platforms to confine the screenshots to.
+ * @param clientIdentifierOverride
+ * @param productVariantData
+ * @param isAd Whether the screenshots are being gathered for an ad lockup. Defaults to false.
+ * @returns An ordered array of screenshots for display on a product page
+ * */
+export function screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms = null, clientIdentifierOverride, productVariantData, isAd = false, cropCode) {
+ return validation.context("screenshotsFromData", () => {
+ const screenshots = [];
+ if (serverData.isNull(productVariantData)) {
+ productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // resolve if not resolved by caller.
+ }
+ let sortedAppPlatforms = includedAppPlatforms;
+ if (!sortedAppPlatforms || sortedAppPlatforms.length === 0) {
+ const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier;
+ let preferredDeviceType = objectGraph.client.deviceType;
+ if (preferredClientIdentifier === client.watchIdentifier) {
+ preferredDeviceType = "watch";
+ }
+ if (clientIdentifierOverride === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifierOverride === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ preferredDeviceType = "vision";
+ }
+ sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, preferredClientIdentifier, preferredDeviceType);
+ }
+ for (const appPlatform of sortedAppPlatforms) {
+ const supplementaryAppPlatforms = [];
+ let screenshotData;
+ if (appPlatform === "messages") {
+ screenshotData = messagesScreenshotsFromData(objectGraph, data, "ios");
+ if (supportsFunCameraFromData(objectGraph, data, "ios")) {
+ supplementaryAppPlatforms.push("faceTime");
+ }
+ }
+ else if (appPlatform === "tv" && !objectGraph.host.isTV) {
+ // For tvOS screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "appletvos");
+ }
+ else if (appPlatform === "vision" && !objectGraph.host.isVision) {
+ // For visionOS screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "xros");
+ }
+ else if (appPlatform === "mac" && !objectGraph.host.isMac) {
+ // For Mac screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "osx");
+ }
+ else if ((appPlatform === "phone" || appPlatform === "pad" || appPlatform === "watch") &&
+ !objectGraph.host.isiOS &&
+ !objectGraph.host.isWatch) {
+ // For iPhone / iPad / watch screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "ios");
+ }
+ else {
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd);
+ }
+ if (!screenshotData) {
+ continue;
+ }
+ const bestScreenshots = bestScreenshotData(objectGraph, screenshotData, appPlatform, useCase, supplementaryAppPlatforms, cropCode);
+ if (bestScreenshots) {
+ screenshots.push(bestScreenshots);
+ }
+ }
+ return screenshots;
+ });
+}
+/**
+ * Creates an array of product media from the given screenshots. If videos are desired
+ * to be inserted in the same media row, this must be done elsewhere.
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param screenshots The screenshots objects with which to configure the media.
+ * @return A list of product media objects.
+ */
+function productMediaFromScreenshots(objectGraph, data, screenshots) {
+ const allMedia = [];
+ if (screenshots && screenshots.length > 0) {
+ const allPlatforms = screenshots.map((platformScreenshots) => {
+ return platformScreenshots.mediaPlatform;
+ });
+ for (const screenshotsForPlatform of screenshots) {
+ // Create media items from all the screenshots.
+ const screenshotMediaItems = [];
+ for (const screenshotArtwork of screenshotsForPlatform.artwork) {
+ const screenshotItem = new modelsShelves.ProductMediaItem();
+ screenshotItem.screenshot = screenshotArtwork;
+ screenshotMediaItems.push(screenshotItem);
+ }
+ const platform = screenshotsForPlatform.mediaPlatform;
+ const productMedia = new modelsShelves.ProductMedia(screenshotMediaItems, platform, allPlatforms, descriptionOfMediaPlatform(objectGraph, platform), descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms), placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms));
+ allMedia.push(productMedia);
+ }
+ }
+ return allMedia;
+}
+/**
+ * Build a set of of `ProductMedia` from apps resource
+ * @param data Apps resource data
+ * @param useCase Artwork use case
+ * @param includedAppPlatforms What platforms are included.
+ * @param 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.
+ * @param clientIdentifierOverride
+ */
+export function productMediaFromData(objectGraph, data, useCase, includedAppPlatforms = null, productVariantData = null, clientIdentifierOverride) {
+ const screenshots = screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms, clientIdentifierOverride, productVariantData);
+ return productMediaFromScreenshots(objectGraph, data, screenshots);
+}
+/**
+ * Finds the best screenshot data from a response to use for the given parameters.
+ * @param data The data from which to derive the screenshots.
+ * @param appPlatform The app platform to which the screenshots belong.
+ * @param supplementaryAppPlatforms
+ * @returns The best available screenshots.
+ */
+function bestScreenshotData(objectGraph, data, appPlatform, useCase, supplementaryAppPlatforms, cropCode) {
+ const selectionPlatform = selectionAppPlatformFromAppPlatform(objectGraph, appPlatform);
+ const screenshotTypes = mediaTypesForAppPlatform(objectGraph, selectionPlatform, objectGraph.client.screenSize);
+ let bestScreenshot = null;
+ let bestScreenshotType;
+ for (let i = 0; i < screenshotTypes.length && !serverData.isDefinedNonNullNonEmpty(bestScreenshot); i++) {
+ bestScreenshot = serverData.asArrayOrEmpty(data, screenshotTypes[i]);
+ bestScreenshotType = screenshotTypes[i];
+ }
+ if (serverData.isDefinedNonNullNonEmpty(bestScreenshot)) {
+ const artworks = bestScreenshot.map(function (screenshotArtwork) {
+ return artworkFromApiArtwork(objectGraph, screenshotArtwork, {
+ useCase: useCase,
+ cropCode: cropCode,
+ });
+ });
+ const platform = mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, bestScreenshotType, supplementaryAppPlatforms);
+ const screenshots = new modelsBase.Screenshots(artworks, platform);
+ return screenshots;
+ }
+ return null;
+}
+/**
+ * Returns a list of sorted app platforms for displaying screenshots. This contains the sorting logic for screenshots.
+ *
+ * @param data Server data for the app
+ * @param clientIdentifier Identifier of the current client.
+ * @param deviceType Type of the current device.
+ * @returns A sorted list of AppPlatform values to use when displaying
+ * */
+export function sortedAppPlatformsFromData(objectGraph, data, clientIdentifier, deviceType) {
+ return derivedData.value(data, `sortedAppPlatformsFromData.${clientIdentifier}.${deviceType}`, () => {
+ var _a;
+ const supportedAppPlatforms = supportedAppPlatformsFromData(objectGraph, data);
+ const excludedAppPlatforms = [];
+ let sortedAppPlatforms = [];
+ const addAppPlatformIfPossible = function (appPlatform, excludePlatform) {
+ if (sortedAppPlatforms.indexOf(appPlatform) !== -1) {
+ return;
+ }
+ if (excludedAppPlatforms.indexOf(appPlatform) !== -1) {
+ return;
+ }
+ if (supportedAppPlatforms.indexOf(appPlatform) !== -1) {
+ sortedAppPlatforms.push(appPlatform);
+ if (excludePlatform) {
+ excludedAppPlatforms.push(excludePlatform);
+ }
+ }
+ };
+ // If there is an `AppPlatform` associated with the active `Intent`, give
+ // that first priority
+ if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform) {
+ addAppPlatformIfPossible(objectGraph.activeIntent.appPlatform);
+ }
+ if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ addAppPlatformIfPossible("vision");
+ }
+ // Next, priority is given to the client
+ switch (clientIdentifier) {
+ case client.watchIdentifier: {
+ addAppPlatformIfPossible("watch");
+ break;
+ }
+ case client.messagesIdentifier: {
+ addAppPlatformIfPossible("messages");
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ // Next the current device type
+ switch (deviceType) {
+ case "phone": {
+ addAppPlatformIfPossible("phone");
+ break;
+ }
+ case "pad": {
+ addAppPlatformIfPossible("pad");
+ break;
+ }
+ case "tv": {
+ addAppPlatformIfPossible("tv");
+ break;
+ }
+ case "watch": {
+ addAppPlatformIfPossible("watch");
+ break;
+ }
+ case "mac": {
+ addAppPlatformIfPossible("mac");
+ break;
+ }
+ case "vision": {
+ addAppPlatformIfPossible("vision");
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ // For Apple Silicon and visionOS, prefer iPad platform over iPhone
+ if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ addAppPlatformIfPossible("pad");
+ addAppPlatformIfPossible("phone");
+ }
+ if (objectGraph.appleSilicon.isSupportEnabled || objectGraph.client.isVision) {
+ addAppPlatformIfPossible("pad");
+ addAppPlatformIfPossible("phone");
+ }
+ else {
+ addAppPlatformIfPossible("phone");
+ addAppPlatformIfPossible("pad");
+ }
+ addAppPlatformIfPossible("mac");
+ addAppPlatformIfPossible("vision");
+ addAppPlatformIfPossible("tv");
+ addAppPlatformIfPossible("watch");
+ addAppPlatformIfPossible("messages");
+ sortedAppPlatforms = sortedAppPlatforms.filter(function (appPlatform) {
+ return excludedAppPlatforms.indexOf(appPlatform) === -1;
+ });
+ return sortedAppPlatforms;
+ });
+}
+/**
+ * For a given server data, this will return the Game Center features that the app supports
+ *
+ * @param data Server data for the app
+ * @returns An array of supported Game Center features
+ * */
+export function supportedGameCenterFeaturesFromData(data) {
+ var _a;
+ if (isNothing(data)) {
+ return undefined;
+ }
+ return ((_a = derivedData.value(data, "supportedGameCenterFeaturesFromData", () => {
+ const features = [];
+ const supportedGameCenterFeatures = serverData.asArrayOrEmpty(data, "attributes.supportedGameCenterFeatures");
+ if (supportedGameCenterFeatures.includes("achievements")) {
+ features.push("achievements");
+ }
+ if (supportedGameCenterFeatures.includes("challenges")) {
+ features.push("challenges");
+ }
+ if (supportedGameCenterFeatures.includes("leaderboards")) {
+ features.push("leaderboards");
+ }
+ if (supportedGameCenterFeatures.includes("multiplayer-activities")) {
+ features.push("multiplayer-activities");
+ }
+ return features;
+ })) !== null && _a !== void 0 ? _a : undefined);
+}
+/**
+ * For a given server data, returns whether the game is eligible for the Games App
+ * This will default to true since we generally expect apps we view in the Games app to be games.
+ * It will be unusual that this is evaluated to `false`.
+ *
+ * @param data Server data for the app
+ * @returns A boolean indicating whether game is eligible for display
+ * */
+export function isEligibleForGamesApp(data) {
+ var _a;
+ if (isNothing(data)) {
+ return true;
+ }
+ return (_a = serverData.asBoolean(data, "attributes.isEligibleForGamesApp")) !== null && _a !== void 0 ? _a : true;
+}
+/**
+ * For a given server data, this will return the platforms that the app supports
+ *
+ * @param data Server data for the app
+ * @returns An array of supported AppPlatforms
+ * */
+export function supportedAppPlatformsFromData(objectGraph, data) {
+ if (!data) {
+ return null;
+ }
+ return derivedData.value(data, "supportedAppPlatformsFromData", () => {
+ const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data, "ios");
+ const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data);
+ const isAppleWatchSupported = isAppleWatchSupportedFromData(objectGraph, data);
+ const serverDeviceFamilies = mediaAttributes.attributeAsArrayOrEmpty(data, "deviceFamilies");
+ const appPlatforms = [];
+ for (const serverDeviceFamily of serverDeviceFamilies) {
+ switch (serverDeviceFamily) {
+ case "iphone":
+ if (!isHiddenFromSpringboard) {
+ appPlatforms.push("phone");
+ }
+ break;
+ case "ipad":
+ if (!isHiddenFromSpringboard) {
+ appPlatforms.push("pad");
+ }
+ break;
+ case "tvos":
+ appPlatforms.push("tv");
+ break;
+ case "watch":
+ appPlatforms.push("watch");
+ break;
+ case "realityDevice":
+ appPlatforms.push("vision");
+ break;
+ default:
+ break;
+ }
+ }
+ if (hasMessagesExtension) {
+ appPlatforms.push("messages");
+ }
+ if (isAppleWatchSupported) {
+ appPlatforms.push("watch");
+ }
+ if (contentDeviceFamily.dataHasDeviceFamily(objectGraph, data, "mac")) {
+ appPlatforms.push("mac");
+ }
+ return appPlatforms;
+ });
+}
+/**
+ * Returns a localized, user-friendly description of all media platforms. This may be a comma delimited
+ * list of the platforms (including supplementary platforms), or it may be 'Only for ___', depending
+ * on the context.
+ *
+ * The localization keys used by this function are defined natively, and are updated using
+ * `tools/platform-media-localizations.py`. If the key doesn't exist, then the script needs to
+ * be updated to add the new combination/order of platforms.
+ *
+ * For failed attempts to localize the string, this function will fallback to a default order that is
+ * guaranteed to exist.
+ *
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param allPlatforms The list of platforms to describe.
+ * @returns The friendly description of all platforms.
+ */
+export function descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms) {
+ if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) {
+ const platform = allPlatforms[0];
+ const platformKey = platform.appPlatform.toUpperCase();
+ return objectGraph.loc.string(`ONLY_FOR_${platformKey}_APP`);
+ }
+ // Flatten all platform partial keys, including their supplementary platforms
+ let keys = allPlatforms.reduce((partialResult, platform) => partialResult.concat(platformLocalizationKeys(platform)), []);
+ try {
+ // Attempt to localize the constructed key
+ return objectGraph.loc.tryString(`PLATFORMS_${keys.join("_")}`);
+ }
+ catch (error) {
+ // If the key does not exist, a best attempt fallback string will be provided.
+ const fallbackOrder = ["PHONE", "PAD", "MAC", "VISION", "TV", "WATCH", "MESSAGES", "FACETIME"];
+ keys = fallbackOrder.filter((key) => keys.includes(key));
+ return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`);
+ }
+}
+/**
+ * Determines where to place the all platforms description, which is visible when the product media is collapsed, or there is only one platform.
+ * This is only used by iOS, visionOS & macOS. For tvOS & watchOS, we always put the media description at the bottom.
+ *
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param allPlatforms The list of platforms to describe.
+ * @returns Where to place the all platforms description.
+ */
+export function placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms) {
+ if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) {
+ return "top";
+ }
+ else {
+ return "bottom";
+ }
+}
+/**
+ * Determines whether we want to use the 'Only for ___' text to describe `allPlatforms`.
+ *
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param allPlatforms The list of platforms to describe.
+ * @returns Whether we want to use 'Only for ___' text to describe `allPlatforms`.
+ */
+function shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms) {
+ if (allPlatforms.length === 1) {
+ const platform = allPlatforms[0];
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ const isRunnableOnCurrentDevice = supportsPlatform(runnableAppPlatforms, platform.appPlatform);
+ const noSupplementaryPlatforms = platform.supplementaryAppPlatforms.length === 0;
+ const isForDifferentDevice = platform.appPlatform !== objectGraph.client.deviceType;
+ if (noSupplementaryPlatforms && isForDifferentDevice && !isRunnableOnCurrentDevice) {
+ return true;
+ }
+ }
+ return false;
+}
+/**
+ * Returns a localized description of a media platform, including any supplementary platforms.
+ * e.g. 'Mac' or 'iMessage, FaceTime'.
+ *
+ * @param objectGraph Object graph, used for localizing the string.
+ * @param allPlatforms The platform to describe.
+ * @returns The friendly description of the platform.
+ */
+export function descriptionOfMediaPlatform(objectGraph, platform) {
+ const keys = platformLocalizationKeys(platform);
+ return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`);
+}
+/**
+ * Returns an array of partial loc keys that represent a media platform.
+ * This consists the media's app platform + any supplementary platforms.
+ * e.g. ["MAC"] or ["MESSAGES", "FACETIME"]
+ *
+ * @param platform The media platform.
+ * @returns The list of partial loc key that represent the media platform.
+ */
+function platformLocalizationKeys(platform) {
+ const appPlatformKey = platform.appPlatform.toUpperCase();
+ const supplementaryPlatformKeys = platform.supplementaryAppPlatforms.map((supplementaryPlatform) => supplementaryPlatform.toUpperCase());
+ return [appPlatformKey].concat(supplementaryPlatformKeys);
+}
+/**
+ * Determines if a given app has a compatible iOS binary for this client.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries
+ * @returns {boolean} True when the app and device are halva.
+ */
+export function supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary) {
+ let isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible");
+ // Override for News in Moltres on Mac
+ if (preprocessor.GAMES_TARGET && data.id === "1066498020" && objectGraph.client.deviceType === "mac") {
+ isIOSBinaryMacOSCompatible = true;
+ }
+ return doesClientSupportMacOSCompatibleIOSBinary && isIOSBinaryMacOSCompatible;
+}
+/**
+ * Determines if a given app has a compatible iOS binary for the current client.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @returns {boolean} True when the app and device are visionOS.
+ */
+export function supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data) {
+ return (objectGraph.client.isVision &&
+ mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible"));
+}
+/**
+ * Determines if a given app has a compatible iOS binary for arbitrary clients.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @returns {boolean} True when the app can run on visionOS.
+ */
+export function supportsVisionOSCompatibleIOSBinaryOnAnyClient(data) {
+ return mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible");
+}
+/**
+ * Determines app binary traits.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @returns {string[]} The app binary traits.
+ */
+export function appBinaryTraitsFromData(objectGraph, data) {
+ if (!objectGraph.client.isiOS) {
+ return undefined;
+ }
+ let appBinaryTraits;
+ if (objectGraph.isAvailable(ads) &&
+ ["debug", "internal"].includes(objectGraph.client.buildType) &&
+ isSome(objectGraph.ads.fetchAppBinaryTraitsOverride)) {
+ // use client override for debugging internal builds
+ appBinaryTraits = objectGraph.ads.fetchAppBinaryTraitsOverride();
+ }
+ if (isNothing(appBinaryTraits)) {
+ // parse from server response
+ appBinaryTraits = mediaPlatformAttributes.platformAttributeAsArrayOrEmpty(data, "ios", "appBinaryTraits");
+ }
+ return appBinaryTraits;
+}
+/**
+ * Determines whether the product has external browser engine.
+ * @param objectGraph Current object graph
+ * @param data The product data
+ * @returns True if the product has external browser engine
+ */
+export function hasExternalBrowserForData(objectGraph, data) {
+ var _a;
+ const appBinaryTraits = appBinaryTraitsFromData(objectGraph, data);
+ const externalBrowserTraits = new Set(["uses-non-webkit-browser-engine", "is-custom-browser-engine-app"]);
+ return (_a = appBinaryTraits === null || appBinaryTraits === void 0 ? void 0 : appBinaryTraits.some((trait) => externalBrowserTraits.has(trait))) !== null && _a !== void 0 ? _a : false;
+}
+/**
+ * Determines minimum os version
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @param {boolean} isClientHalva Whether the client is halva.
+ * @returns {string} The minimum OS version.
+ */
+export function minimumOSVersionFromData(objectGraph, data, isClientHalva) {
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (supportsMacOSCompatibleIOSBinary) {
+ const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumMacOSVersion");
+ if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) {
+ return minimumOSVersion;
+ }
+ }
+ else if (supportsVisionOSCompatibleIOSBinary) {
+ const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumXROSVersion");
+ if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) {
+ return minimumOSVersion;
+ }
+ }
+ const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data);
+ return mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "minimumOSVersion");
+}
+/**
+ * Determines required capabilities for device.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @param {boolean} isClientHalva Whether the client is halva.
+ * @returns {string} The device capabilities to use.
+ */
+export function requiredCapabilitiesFromData(objectGraph, data, isClientHalva) {
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (supportsMacOSCompatibleIOSBinary) {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "macRequiredCapabilities");
+ }
+ else if (supportsVisionOSCompatibleIOSBinary) {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilitiesForRealityDevice");
+ }
+ else {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilities");
+ }
+}
+/**
+ * Returns the app platforms you can buy for on the given device.
+ *
+ * @param objectGraph The current object graph
+ * @param data The data for the app in question
+ * @param device The device type to check
+ * @param supportsMacOSCompatibleIOSBinary Whether device and app supports macOS compatible iOS binary
+ * @param supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @returns An array of supported app platforms
+ */
+function buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ let systemApps;
+ switch (device) {
+ case "phone":
+ systemApps = sad.systemApps(objectGraph);
+ if (isSome(data) && systemApps.isSystemAppFromData(data)) {
+ return ["phone", "watch", "messages"];
+ }
+ else {
+ return ["phone", "watch", "messages", "tv", "vision"];
+ }
+ case "pad":
+ systemApps = sad.systemApps(objectGraph);
+ if (isSome(data) && systemApps.isSystemAppFromData(data)) {
+ return ["phone", "pad", "messages"];
+ }
+ else {
+ return ["phone", "pad", "messages", "tv", "vision"];
+ }
+ case "tv":
+ return ["tv"];
+ case "watch":
+ return ["watch"];
+ case "mac":
+ if (supportsMacOSCompatibleIOSBinary) {
+ return ["mac", "phone", "pad"];
+ }
+ else {
+ return ["mac"];
+ }
+ case "vision":
+ if (supportsVisionOSCompatibleIOSBinary) {
+ return ["vision", "phone", "pad"];
+ }
+ else {
+ return ["vision"];
+ }
+ default:
+ return [];
+ }
+}
+/**
+ * Returns the app platforms you can preorder on for the given device.
+ *
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @returns {models.AppPlatform[]} An array of supported app platforms
+ */
+function preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ switch (device) {
+ case "phone":
+ return ["phone", "watch", "messages"];
+ case "pad":
+ return ["phone", "pad", "messages"];
+ case "tv":
+ return ["tv"];
+ case "watch":
+ return ["watch"];
+ case "mac":
+ if (supportsMacOSCompatibleIOSBinary) {
+ return ["mac", "phone", "pad"];
+ }
+ else {
+ return ["mac"];
+ }
+ case "vision":
+ if (supportsVisionOSCompatibleIOSBinary) {
+ return ["vision", "phone", "pad"];
+ }
+ else {
+ return ["vision"];
+ }
+ default:
+ return [];
+ }
+}
+/**
+ * Returns the app platforms you can run on the given device.
+ *
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @returns {models.AppPlatform[]} An array of supported app platforms
+ */
+export function runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ switch (device) {
+ case "phone":
+ return ["phone", "messages"];
+ case "pad":
+ return ["phone", "pad", "messages"];
+ case "tv":
+ return ["tv"];
+ case "watch":
+ return ["watch"];
+ case "mac":
+ if (supportsMacOSCompatibleIOSBinary) {
+ return ["mac", "phone", "pad"];
+ }
+ else {
+ return ["mac"];
+ }
+ case "vision":
+ if (supportsVisionOSCompatibleIOSBinary) {
+ return ["vision", "phone", "pad"];
+ }
+ else {
+ return ["vision"];
+ }
+ default:
+ return [];
+ }
+}
+/**
+ * Determines if a given piece of content supports the provided app platform
+ *
+ * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content
+ * @param {AppPlatform} platform The platform to check
+ * @returns {boolean} True if the platform is supported, false if not
+ */
+export function supportsPlatform(appPlatforms, platform) {
+ return appPlatforms.indexOf(platform) !== -1;
+}
+/**
+ * Determines if a given piece of content is buyable on the provided device.
+ *
+ * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary.
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @param {boolean} isMacOSAppBuyableOnDevice Whether a macOS app is buyable on this device (this enables additional criteria for Apple Silicon).
+ * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not
+ */
+export function buyableOnDevice(objectGraph, data, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppBuyableOnDevice = true) {
+ const platforms = buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ // Do any of the platforms supported by the device match any of the content's app platforms?
+ if (!platforms.some((platform) => supportsPlatform(appPlatforms, platform))) {
+ return false;
+ }
+ if (objectGraph.client.isMac && platforms.includes("mac")) {
+ return isMacOSAppBuyableOnDevice;
+ }
+ return true;
+}
+/**
+ * Determines macOS runnability info for apps and bundles on macOS.
+ */
+function macOSRunnabilityInfoFromData(objectGraph, data) {
+ var _a;
+ const runnabilityInfo = new RunnabilityInfo();
+ // Return most permissible runnability for non-macOS platforms.
+ if (objectGraph.client.deviceType !== "mac") {
+ return runnabilityInfo;
+ }
+ // Use media API attributes for non-bundles.
+ if (data.type !== "app-bundles") {
+ runnabilityInfo.runsOnIntel =
+ (_a = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "runsOnIntel", contentAttributes.defaultAttributePlatform(objectGraph))) !== null && _a !== void 0 ? _a : true;
+ runnabilityInfo.runsOnAppleSilicon = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "runsOnAppleSilicon", contentAttributes.defaultAttributePlatform(objectGraph));
+ runnabilityInfo.requiresRosetta = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresRosetta", contentAttributes.defaultAttributePlatform(objectGraph));
+ return runnabilityInfo;
+ }
+ const bundleAppsData = mediaRelationship.relationshipCollection(data, "apps");
+ // Return most permissible runnability when there no children available
+ if (bundleAppsData.length === 0) {
+ return runnabilityInfo;
+ }
+ // Synthesize runnability info from bundle apps
+ for (const appData of bundleAppsData) {
+ if (serverData.isNull(appData.attributes)) {
+ continue;
+ }
+ const appRunnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, appData);
+ runnabilityInfo.runsOnIntel = runnabilityInfo.runsOnIntel && appRunnabilityInfo.runsOnIntel;
+ runnabilityInfo.runsOnAppleSilicon =
+ runnabilityInfo.runsOnAppleSilicon && appRunnabilityInfo.runsOnAppleSilicon;
+ runnabilityInfo.requiresRosetta = runnabilityInfo.requiresRosetta || appRunnabilityInfo.requiresRosetta;
+ }
+ return runnabilityInfo;
+}
+/**
+ * Determines if a given macOS app is buyable on this device.
+ *
+ */
+export function isMacOSAppBuyableAndRunnableFromData(objectGraph, data, isAppleSiliconSupportEnabled, isRosettaAvailable) {
+ const runnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, data);
+ if (isAppleSiliconSupportEnabled) {
+ return (runnabilityInfo.runsOnAppleSilicon &&
+ (!runnabilityInfo.requiresRosetta || (runnabilityInfo.requiresRosetta && isRosettaAvailable)));
+ }
+ else {
+ return runnabilityInfo.runsOnIntel;
+ }
+}
+/**
+ * Determines if a given piece of content is preorderable on the provided device.
+ *
+ * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary.
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary.
+ * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not
+ */
+export function preorderableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ const platforms = preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ // Do any of the platforms supported by the device match any of the content's app platforms?
+ return platforms.some((platform) => supportsPlatform(appPlatforms, platform));
+}
+/**
+ * Determines if any of a given array of app platforms can be run on the provided device.
+ *
+ * @param appPlatforms The app platforms supported by a piece of content.
+ * @param device The device type to check.
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary.
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary.
+ * @returns `true` if any of the app platforms can be run on the given device; `false` otherwise.
+ */
+export function runnableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppRunnableOnDevice = true) {
+ const runnablePlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ // Do any of the platforms supported by the device match any of the content's app platforms?
+ if (!runnablePlatforms.some((platform) => supportsPlatform(appPlatforms, platform))) {
+ return false;
+ }
+ if (objectGraph.client.isMac && appPlatforms.includes("mac")) {
+ return isMacOSAppRunnableOnDevice;
+ }
+ return true;
+}
+/**
+ * Determines if a given piece of content is runnable on the provided device.
+ *
+ * @param data The product data to use.
+ * @param device
+ * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries
+ * @returns {boolean} True if the product can be run on the provided device, false if not
+ */
+export function runnableOnDeviceWithData(objectGraph, data, device, doesClientSupportMacOSCompatibleIOSBinary) {
+ // (1) Required capabilities mismatch
+ if (!lockups.deviceHasCapabilitiesFromData(objectGraph, data)) {
+ return false;
+ }
+ // (2) 32-bit only, unsupported deletable system app, doesn't meet minimum OS requirements, or doesn't support current platform
+ // Note that Filter.UnsupportedPlatform only checks if the product is buyable, not runnable
+ const filter = 2 /* filtering.Filter.ThirtyTwoBit */ |
+ 4 /* filtering.Filter.UnsupportedSystemDeletableApps */ |
+ 512 /* filtering.Filter.MinimumOSRequirement */ |
+ 128 /* filtering.Filter.UnsupportedPlatform */ |
+ 8192 /* filtering.Filter.MacOSRosetta */;
+ if (filtering.shouldFilter(objectGraph, data, filter)) {
+ return false;
+ }
+ // (3) Finally, check if any of the product platforms are supported on this device
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ const productAppPlatforms = supportedAppPlatformsFromData(objectGraph, data);
+ return runnableAppPlatforms.some((platform) => supportsPlatform(productAppPlatforms, platform));
+}
+/**
+ * Determines which screenshot keys (MediaType) we need to use to pull the appropriate screenshots
+ * from the server data.
+ *
+ * @param appPlatform The app platform requested
+ * @param screenSize The size of the screen being used to display the screenshots
+ * @returns An array of ScreenshotType strings that can be used on the server data
+ * */
+export function mediaTypesForAppPlatform(objectGraph, appPlatform, screenSize) {
+ switch (appPlatform) {
+ case "mac": {
+ return ["mac"];
+ }
+ case "watch": {
+ if (screenSize.isEqualTo(screenSizeWatchUltra) || screenSize.isEqualTo(screenSizeN230)) {
+ // 2022 is the preferred dropwell for Ultra devices
+ return ["appleWatch_2022", "appleWatch_2024", "appleWatch_2021", "appleWatch_2018", "appleWatch"];
+ }
+ else {
+ return ["appleWatch_2024", "appleWatch_2022", "appleWatch_2021", "appleWatch_2018", "appleWatch"];
+ }
+ }
+ case "tv": {
+ return ["appleTV"];
+ }
+ case "vision": {
+ return ["appleVisionPro"];
+ }
+ case "pad": {
+ const types = [];
+ if ((screenSize.isEqualTo(screenSizeIPadPro2018) ||
+ screenSize.isEqualTo(screenSizeIPadPro2018Landscape) ||
+ screenSize.isEqualTo(screenSizeJ720) ||
+ screenSize.isEqualTo(screenSizeJ720Landscape)) &&
+ objectGraph.client.screenCornerRadius > 0.0) {
+ types.push("ipadPro_2018");
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPadPro)) {
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ types.push("ipad_11");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPad11) ||
+ screenSize.isEqualTo(screenSizeIPad11Landscape) ||
+ screenSize.isEqualTo(screenSizeIPadJ310) ||
+ screenSize.isEqualTo(screenSizeIPadJ310Landscape) ||
+ screenSize.isEqualTo(screenSizeJ717) ||
+ screenSize.isEqualTo(screenSizeJ717Landscape)) {
+ types.push("ipad_11");
+ types.push("ipadPro_2018");
+ types.push("ipadPro");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPad105)) {
+ types.push("ipad_10_5");
+ types.push("ipad");
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPadAir2020)) {
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPad102)) {
+ types.push("ipad");
+ types.push("ipad_10_5");
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ }
+ else {
+ // Regardless of screen size match, we should add on 'some' iPad.
+ types.push("ipadPro_2018");
+ types.push("ipad_11");
+ types.push("ipad");
+ types.push("ipad_10_5");
+ types.push("ipadPro");
+ }
+ return types;
+ }
+ case "phone": {
+ /** Phone Best Match Policy **
+
+ The best match is given by |B| + |L| + |S|, where:
+ B: Exact type match
+ L: All types larger than the exact type, in increasing order
+ S: All types smaller than the exact type, in decreasing order
+
+ Example:
+ Types for iphone6 == [iphone6, iphone6+, iphone_5_8, iphone5, iphone]
+ Types for iphone5 == [iphone5, iphone6, iphone6+, iphone_5_8, iphone]
+
+ ** */
+ // Grab the exact match.
+ let perfectMatch;
+ if (screenSize.isEqualTo(screenSizeIphone65) || screenSize.isEqualTo(screenSizeIPhone134)) {
+ perfectMatch = "iphone_6_5";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone58) ||
+ screenSize.isEqualTo(screenSizeIPhone131) ||
+ screenSize.isEqualTo(screenSizeIPhone132)) {
+ perfectMatch = "iphone_5_8";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhoneOriginal)) {
+ perfectMatch = "iphone";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone5)) {
+ perfectMatch = "iphone5";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone6)) {
+ perfectMatch = "iphone6";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone6Plus)) {
+ perfectMatch = "iphone6+";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone61) || screenSize.isEqualTo(screenSizeD93)) {
+ perfectMatch = "iphone_d73";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone67) ||
+ screenSize.isEqualTo(screenSizeD94) ||
+ screenSize.isEqualTo(screenSizeD23)) {
+ perfectMatch = "iphone_d74";
+ }
+ else {
+ perfectMatch = "iphone_5_8";
+ }
+ // Append remaining types to our exact match.
+ const perfectMatchIndex = decreasingPhoneTypes.indexOf(perfectMatch);
+ const largerTypes = decreasingPhoneTypes.slice(0, perfectMatchIndex);
+ largerTypes.reverse();
+ const smallerTypes = decreasingPhoneTypes.slice(perfectMatchIndex + 1);
+ const perfectMatchArray = [perfectMatch];
+ return perfectMatchArray.concat(largerTypes, smallerTypes);
+ }
+ default: {
+ return [];
+ }
+ }
+}
+export function combinedFileSizeFromData(objectGraph, data) {
+ var _a;
+ if (serverData.isNull(data)) {
+ return null;
+ }
+ // This background asset information is for the work done in SydneyB
+ const backgroundAssetsInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfo");
+ // This background asset information is for the work done in SydneyE
+ const backgroundAssetsInfoWithOptional = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfoWithOptional");
+ const isIOSBinaryCompatibleWithMac = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, true);
+ const isMacOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac");
+ const isWebViewingMac = objectGraph.client.isWeb && ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) === "mac";
+ if ((objectGraph.client.isMac || isWebViewingMac || isMacOnly) && !isIOSBinaryCompatibleWithMac) {
+ const macFileSize = objectGraph.bag.enableProductPageInstallSize
+ ? macInstallSizeInBytesFromData(objectGraph, data)
+ : offers.macFileSizeInBytesFromData(objectGraph, data);
+ if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) {
+ const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(macFileSize, null, null, maxEssentialInstallSizeInBytes);
+ }
+ else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) {
+ const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes");
+ const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(macFileSize, maxDownloadSizeInBytes, maxInstallSizeInBytes, null);
+ }
+ return new modelsBase.CombinedFileSize(macFileSize, null, null, null);
+ }
+ else {
+ /* File Size: Our policy is to rely on thinned variant, device model, and universal (in that order). */
+ const fileSizeByDevice = mediaAttributes.attributeAsDictionary(data, "fileSizeByDevice");
+ if (fileSizeByDevice) {
+ /* thinnedApplicationVariantIdentifier can contain two device names. The preferred device, and a compatible device. */
+ let fileSizeKeys = [];
+ if (objectGraph.client.thinnedApplicationVariantIdentifier) {
+ fileSizeKeys = objectGraph.client.thinnedApplicationVariantIdentifier.split(" ");
+ }
+ fileSizeKeys = fileSizeKeys.concat([objectGraph.host.deviceModel, "universal"]);
+ for (const key of fileSizeKeys) {
+ const fileSizeValue = serverData.asNumber(fileSizeByDevice[key]);
+ if (fileSizeValue) {
+ if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) {
+ const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(fileSizeValue, null, null, maxEssentialInstallSizeInBytes);
+ }
+ else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) {
+ const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes");
+ const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(fileSizeValue, maxDownloadSizeInBytes, maxInstallSizeInBytes, null);
+ }
+ else {
+ return new modelsBase.CombinedFileSize(fileSizeValue, null, null, null);
+ }
+ }
+ }
+ }
+ }
+ return null;
+}
+/**
+ * Extract the file size and unit from a CombinedFileSize object.
+ * @param objectGraph Current object graph
+ * @param combinedFileSize The combined file size object
+ * @returns A FileSizeAndUnit object
+ */
+export function fileSizeAndUnitFromCombinedFileSize(objectGraph, combinedFileSize) {
+ let totalFileSize;
+ if (isSome(combinedFileSize.maxEssentialInstallSizeInBytes)) {
+ totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxEssentialInstallSizeInBytes;
+ }
+ else if (isSome(combinedFileSize.maxInstallSizeInBytes)) {
+ totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxInstallSizeInBytes;
+ }
+ else {
+ totalFileSize = combinedFileSize.fileSizeByDevice;
+ }
+ if (totalFileSize <= 0) {
+ return null;
+ }
+ // We split using all whitespace characters because in some locs a non-breaking space is used.
+ const parts = objectGraph.loc.fileSize(totalFileSize).trim().split(/\s+/);
+ if (parts.length !== 2) {
+ return null;
+ }
+ return {
+ size: parts[0],
+ unit: parts[1],
+ };
+}
+/**
+ * Extracts the install size for a macOS app.
+ * @param objectGraph Current object graph
+ * @param data Product page data
+ * @returns The install size for the Mac binary, in bytes
+ */
+function macInstallSizeInBytesFromData(objectGraph, data) {
+ const deviceData = mediaPlatformAttributes.platformAttributeAsDictionary(data, "osx", "installSizeByDeviceInBytes");
+ if (isNothing(deviceData)) {
+ return null;
+ }
+ // macOS does not support app thinning, so there is only ever one macOS device in this list. Unfortunately
+ // there is no known API that gives us this device name, so we resort to hard-coding for now.
+ const installSizeInBytes = deviceData["Mac"];
+ if (isNothing(installSizeInBytes)) {
+ return null;
+ }
+ return serverData.asNumber(installSizeInBytes);
+}
+/**
+ * Determines the primary langauge locale, from a given list of locales.
+ * @param objectGraph Current object graph
+ * @param locales The list of locales
+ * @returns A single LanguageLocale object, or null
+ */
+export function primaryLanguageLocaleFromLocales(objectGraph, locales) {
+ const languageCount = locales.length;
+ if (languageCount <= 0) {
+ return null;
+ }
+ return {
+ tag: serverData.asString(serverData.traverse(locales, "0.tag")).split("-")[0].toUpperCase(),
+ name: serverData.asString(serverData.traverse(locales, "0.name")),
+ };
+}
+/**
+ * Determines the uber artwork for the product, if there is any.
+ * @param {Data} The data for the product.
+ * @returns {models.Artwork} The artwork for the uber, or `null` if there is none.
+ * null.
+ */
+export function productUberFromData(objectGraph, data, options) {
+ let uberArtworkData;
+ let uberArtworkPath = null;
+ let fallbackUberArtworkPath = null;
+ let cropCode = null;
+ let fallbackCropCode = null;
+ switch (objectGraph.client.deviceType) {
+ case "mac":
+ if (options.supportsArcade) {
+ uberArtworkPath = "editorialArtwork.splashFullScreen";
+ cropCode = "sr";
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.centeredFullscreenBackground";
+ cropCode = "ep";
+ }
+ break;
+ case "tv":
+ if (options.presentedInTopShelf) {
+ uberArtworkPath = "editorialArtwork.topShelf";
+ cropCode = "sr";
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.splashFullScreen";
+ cropCode = "ta";
+ fallbackUberArtworkPath = "editorialArtwork.fullscreenBackground";
+ fallbackCropCode = "sr";
+ }
+ break;
+ case "vision":
+ uberArtworkPath = "editorialArtwork.productUberStatic16x9";
+ cropCode = "sr";
+ break;
+ default:
+ if (options.supportsArcade) {
+ if (options.prefersCompactVariant || objectGraph.client.isPhone) {
+ uberArtworkPath = "editorialArtwork.splashTall";
+ cropCode = "oc";
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.splashFullScreen";
+ cropCode = "oh";
+ }
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.bannerUber";
+ cropCode = "sr";
+ }
+ break;
+ }
+ uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, uberArtworkPath);
+ // If we don't have the desired artwork, we sometimes attempt to use other artwork as a fallback.
+ if (fallbackUberArtworkPath !== null && serverData.isNullOrEmpty(uberArtworkData)) {
+ uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, fallbackUberArtworkPath);
+ // Use the fallback crop if it's available.
+ if (fallbackCropCode !== null) {
+ cropCode = fallbackCropCode;
+ }
+ }
+ if (serverData.isDefinedNonNull(uberArtworkData) && serverData.isDefinedNonNull(cropCode)) {
+ return artworkFromApiArtwork(objectGraph, uberArtworkData, {
+ cropCode,
+ useCase: 21 /* ArtworkUseCase.Uber */,
+ withJoeColorPlaceholder: true,
+ overrideHeight: null,
+ overrideWidth: null,
+ });
+ }
+ return null;
+}
+/**
+ * Determines the logo artwork for the product, if there is any.
+ * @param {Data} The data for the product.
+ * @returns {models.Artwork} The artwork for the uber, or `null` if there is none.
+ * null.
+ */
+export function productLogoArtworkFromData(objectGraph, data) {
+ let artworkPath = null;
+ let cropCode = null;
+ switch (objectGraph.client.deviceType) {
+ case "tv":
+ artworkPath = "editorialArtwork.contentLogoTrimmed";
+ cropCode = "bb";
+ break;
+ default:
+ return null;
+ }
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath);
+ if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ withJoeColorPlaceholder: true,
+ });
+ }
+ return null;
+}
+/**
+ * Determines the editorial video for the product, if there is any.
+ * @returns {models.Video} The editorial video for the product, or `null` if there is none.
+ * null.
+ * @param data
+ * @param useCase
+ * @param preferredFlavorsOverride
+ */
+export function productEditorialVideoFromData(objectGraph, data, useCase, preferredFlavorsOverride, videoPreviewOverride) {
+ let preferredFlavors = [];
+ if (serverData.isDefinedNonNullNonEmpty(preferredFlavorsOverride)) {
+ preferredFlavors = preferredFlavorsOverride;
+ }
+ else {
+ switch (objectGraph.client.deviceType) {
+ case "mac":
+ case "tv":
+ preferredFlavors = ["splashVideo16x9"];
+ break;
+ case "pad":
+ preferredFlavors = ["splashVideo4x3"];
+ break;
+ case "vision":
+ preferredFlavors = ["productUberMotion16x9"];
+ break;
+ default:
+ preferredFlavors = ["splashVideo3x4"];
+ }
+ }
+ let uberEditorialVideoData = null;
+ let videoPreviewData = null;
+ for (const videoFlavor of preferredFlavors) {
+ uberEditorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ videoFlavor,
+ ]);
+ videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ videoFlavor,
+ "previewFrame",
+ ]);
+ if (serverData.isDefinedNonNullNonEmpty(uberEditorialVideoData)) {
+ break;
+ }
+ }
+ // Video Preview based on data, or externally provided override if any.
+ const videoPreview = videoPreviewOverride !== null && videoPreviewOverride !== void 0 ? videoPreviewOverride : artworkFromApiArtwork(objectGraph, videoPreviewData, {
+ useCase: useCase,
+ withJoeColorPlaceholder: true,
+ cropCode: "sr",
+ });
+ if (serverData.isDefinedNonNull(uberEditorialVideoData)) {
+ const videoUrl = serverData.asString(uberEditorialVideoData, "video");
+ if (serverData.isNull(videoUrl)) {
+ return null;
+ }
+ let playbackControls;
+ let autoplayPlaybackControls;
+ if (objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV) {
+ playbackControls = videoDefaults.standardControls(objectGraph);
+ autoplayPlaybackControls = {
+ muteUnmute: true,
+ };
+ }
+ else {
+ playbackControls = {};
+ autoplayPlaybackControls = {};
+ }
+ const configuration = {
+ allowsAutoPlay: true,
+ looping: true,
+ canPlayFullScreen: false,
+ playbackControls: playbackControls,
+ autoPlayPlaybackControls: autoplayPlaybackControls,
+ };
+ return new models.Video(videoUrl, videoPreview, configuration);
+ }
+ return null;
+}
+/**
+ * Determines the video for the poster lockup, if there is any.
+ * @param {Data} The data for the lockup.
+ * @param {useCase} The use case for this artwork.
+ * @returns {models.Video} The video for the poster lockup, or `null` if there is none.
+ * null.
+ */
+export function posterEditorialVideoFromData(objectGraph, data, useCase) {
+ const editorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ "posterCardVideo16x9",
+ ]);
+ const videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ "posterCardVideo16x9",
+ "previewFrame",
+ ]);
+ const cropCode = "sr";
+ const videoPreview = artworkFromApiArtwork(objectGraph, videoPreviewData, {
+ useCase: useCase,
+ withJoeColorPlaceholder: true,
+ cropCode: cropCode,
+ });
+ if (serverData.isDefinedNonNull(editorialVideoData)) {
+ const videoUrl = serverData.asString(editorialVideoData, "video");
+ if (serverData.isNull(videoUrl)) {
+ return null;
+ }
+ const configuration = {
+ allowsAutoPlay: true,
+ looping: true,
+ canPlayFullScreen: false,
+ playbackControls: videoDefaults.noControls(objectGraph),
+ autoPlayPlaybackControls: videoDefaults.noControls(objectGraph),
+ };
+ return new models.Video(videoUrl, videoPreview, configuration);
+ }
+ return null;
+}
+/**
+ * Determines the artwork for the poster lockup, if there is any.
+ * @param {Data} The data for the lockup.
+ * @returns {models.Artwork} The artwork for the poster lockup, or `null` if there is none.
+ * null.
+ */
+export function posterArtworkFromData(objectGraph, data) {
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.postCard");
+ const cropCode = "sr";
+ if (serverData.isDefinedNonNull(artworkData)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ withJoeColorPlaceholder: true,
+ });
+ }
+ return null;
+}
+/**
+ * Determines the artwork for the epic heading on a poster lockup, if there is any.
+ * @param {Data} The data for the product.
+ * @returns {models.Artwork} The artwork for the epic heading, or `null` if there is none.
+ * null.
+ */
+export function posterEpicHeadingArtworkFromData(objectGraph, data) {
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.epicHeading");
+ const cropCode = "bb";
+ if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) {
+ const epicHeadingArtwork = artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ });
+ if (objectGraph.client.isVision) {
+ epicHeadingArtwork.backgroundColor = color.named("clear");
+ }
+ return epicHeadingArtwork;
+ }
+ return null;
+}
+/**
+ * Fetch the most-landscape media from data. Hoisted from Arcade See All.
+ * Used for:
+ * - Arcade See All Media Lockups
+ * - Continue Playing Lockups
+ */
+export function editorialSplashVideoFromData(objectGraph, data, videoPreviewOverride) {
+ let preferredEditorialVideoFlavors = null;
+ switch (objectGraph.client.deviceType) {
+ case "mac":
+ case "tv":
+ case "phone":
+ case "vision":
+ preferredEditorialVideoFlavors = ["splashVideo16x9", "splashVideo4x3", "splashVideo3x4"];
+ break;
+ default:
+ preferredEditorialVideoFlavors = ["splashVideo4x3", "splashVideo16x9", "splashVideo3x4"];
+ }
+ return productEditorialVideoFromData(objectGraph, data, 21 /* ArtworkUseCase.Uber */, preferredEditorialVideoFlavors, videoPreviewOverride);
+}
+/**
+ * Determines the URL to use for the developer page.
+ * @param {Data} developerData The data for the "developer" relationship.
+ * @returns {string} The string form of the URL for the developer page, or `null` if the developer data is undefined or
+ * null.
+ */
+export function developerUrlFromDeveloperData(objectGraph, developerData) {
+ if (!serverData.isDefinedNonNull(developerData)) {
+ return null;
+ }
+ if (objectGraph.client.isWeb) {
+ return mediaAttributes.attributeAsString(developerData, "url");
+ }
+ return `${Protocol.internal}:/${Path.developer}/${Path.href}?${Parameters.href}=${developerData.href}`;
+}
+/**
+ * Determines the URL to use for the Charts page.
+ * @param {Data} data The data for the product.
+ * @returns {string} The string form of the URL for the charts page, or `null` if the data is undefined or
+ * null.
+ */
+export function chartUrlFromData(objectGraph, genre, chart) {
+ const request = new mediaDataFetching.Request(objectGraph)
+ .forType("charts")
+ .addingQuery("types", "apps")
+ .addingQuery("chart", chart)
+ .addingQuery("genre", genre)
+ .includingMacOSCompatibleIOSAppsWhenSupported(true);
+ return mediaUrlBuilder.buildURLFromRequest(objectGraph, request).toString();
+}
+/**
+ * Returns the key into the chart-position badge data for the given client name.
+ * @param clientIdentifier Identifier of the current client.
+ * @returns {string} The relevant key in the chart-position badge JSON data.
+ */
+export function badgeChartKeyForClientIdentifier(objectGraph, clientIdentifier) {
+ switch (clientIdentifier) {
+ case client.appStoreIdentifier:
+ case client.productPageExtensionIdentifier:
+ return "appStore";
+ case client.watchIdentifier:
+ return "watch";
+ case client.messagesIdentifier:
+ return "messages";
+ case client.tvIdentifier:
+ return "appletv";
+ default:
+ return null;
+ }
+}
+/**
+ * Internal function returning the name and asset name representing the
+ * storefront content rating for the provided rank.
+ * @param objectGraph The App Store object graph.
+ * @param rank A content rating rank from CX.
+ * @returns A tuple containing the name and asset name representing the rank,
+ * or `undefined` if rank is unknown/invalid.
+ */
+function storefrontContentRatingInfoForRank(objectGraph, rank) {
+ switch (rank) {
+ // Brazil Self-Rated
+ case 6:
+ return ["L", "br.l"];
+ case 7:
+ return ["10", "br.10"];
+ case 8:
+ return ["12", "br.12"];
+ case 9:
+ return ["14", "br.14"];
+ case 10:
+ return ["16", "br.16"];
+ case 11:
+ return ["18", "br.18"];
+ // Brazil Official
+ case 12:
+ return ["AL", "br.l.official"];
+ case 13:
+ return ["A10", "br.10.official"];
+ case 14:
+ return ["A12", "br.12.official"];
+ case 15:
+ return ["A14", "br.14.official"];
+ case 16:
+ return ["A16", "br.16.official"];
+ case 17:
+ return ["A18", "br.18.official"];
+ // Korea
+ case 20:
+ return ["All", "kr.all"];
+ case 21:
+ return ["12", "kr.12"];
+ case 22:
+ return ["15", "kr.15"];
+ // Australia
+ case 31:
+ return ["15+", "AgeRating-AU-15"];
+ case 32:
+ return ["R 18+", "AgeRating-AU-18"];
+ // France
+ case 47:
+ return ["18+", "AgeRating-FR-18"];
+ default:
+ return undefined;
+ }
+}
+/// Returns a localized title for the given app platform.
+export function appPlatformTitle(objectGraph, appPlatform) {
+ switch (appPlatform) {
+ case "phone":
+ return objectGraph.loc.string("AppPlatform.Phone");
+ case "pad":
+ return objectGraph.loc.string("AppPlatform.Pad");
+ case "vision":
+ return objectGraph.loc.string("AppPlatform.Vision");
+ case "tv":
+ return objectGraph.loc.string("AppPlatform.TV");
+ case "watch":
+ return objectGraph.loc.string("AppPlatform.Watch");
+ case "messages":
+ return objectGraph.loc.string("AppPlatform.Messages");
+ case "mac":
+ return objectGraph.loc.string("AppPlatform.Mac");
+ default:
+ return "";
+ }
+}
+/**
+ * Provides the name of the asset representing the storefront content rating
+ * for the provided `rank`.
+ * @param objectGraph The App Store object graph.
+ * @param rank A content rating rank from CX.
+ * @returns The asset name representing the `rank`, corresponding to a file on
+ * device, or `undefined` if rank is unknown/invalid.
+ */
+export function storefrontContentRatingResourceForRank(objectGraph, rank) {
+ var _a;
+ return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[1];
+}
+/**
+ * Provides a textual representation of the storefront content rating for the
+ * provided `rank`, e.g. "18+". This should match the main text displayed in
+ * the content rating pictogram from `storefrontContentRatingResourceForRank`.
+ * @param objectGraph The App Store object graph.
+ * @param rank A content rating rank from CX.
+ * @returns The textual version of the storefront content rating representing
+ * the `rank`, or `undefined` if rank is unknown/invalid.
+ */
+export function storefrontContentRatingNameForRank(objectGraph, rank) {
+ var _a;
+ return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[0];
+}
+export function promotionalTextFromData(objectGraph, data, productVariantData) {
+ return contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "promotionalText");
+}
+export function hasMessagesExtensionFromData(objectGraph, data, attributePlatform) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasMessagesExtension", attributePlatform);
+}
+export function supportsFunCameraFromData(objectGraph, data, attributePlatform) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsFunCamera", attributePlatform);
+}
+export function isHiddenFromSpringboardFromData(objectGraph, data) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isHiddenFromSpringboard");
+}
+function isAppleWatchSupportedFromData(objectGraph, data) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isAppleWatchSupported");
+}
+function messagesScreenshotsFromData(objectGraph, data, attributePlatform) {
+ return contentAttributes.contentAttributeAsDictionary(objectGraph, data, "messagesScreenshots", attributePlatform);
+}
+function screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) {
+ const attributeKey = isAd ? "customScreenshotsByTypeForAd" : "screenshotsByType";
+ return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform);
+}
+function videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) {
+ const attributeKey = isAd ? "customVideoPreviewsByTypeForAd" : "videoPreviewsByType";
+ return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform);
+}
+/**
+ * Whether Arcade is supported, based on the provided data.
+ * @param objectGraph The App Store object graph.
+ * @param data The data blob to check for Arcade support.
+ * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @returns A boolean indicating if Arcade is supported.
+ */
+export function isArcadeSupported(objectGraph, data, attributePlatformOverride = undefined) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade", attributePlatformOverride);
+}
+/**
+ * Try to get notes for some piece of content, giving preference to the enrichedEditorialnotes, falling back to editorialNotes,
+ * then finally to itunesNotes. For some data the notes are stored in the attributes not the platformAttributes.
+ * @param {Data} data
+ * @param {string} key
+ * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations
+ * * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @returns {string}
+ */
+export function notesFromData(objectGraph, data, key, enableEditorialCardOverrides = false, attributePlatformOverride = undefined) {
+ var _a, _b;
+ if (isNothing(data)) {
+ return null;
+ }
+ let note;
+ if (enableEditorialCardOverrides) {
+ const editorialCard = editorialCardFromData(data);
+ if (mediaAttributes.hasAttributes(editorialCard)) {
+ note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key], attributePlatformOverride);
+ }
+ }
+ note =
+ (_b = (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key], attributePlatformOverride)) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key], attributePlatformOverride)) !== null && _b !== void 0 ? _b : contentAttributes.contentAttributeAsString(objectGraph, data, ["itunesNotes", key], attributePlatformOverride);
+ return note;
+}
+/**
+ * Try and get notes for some piece of content editorialNotes
+ * @param {Data} data
+ * @param {string} key
+ * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations
+ * @returns {string}
+ */
+export function editorialNotesFromData(objectGraph, data, key, enableEditorialCardOverrides = false) {
+ var _a;
+ let note;
+ if (enableEditorialCardOverrides) {
+ const editorialCard = editorialCardFromData(data);
+ if (mediaAttributes.hasAttributes(editorialCard)) {
+ note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key]);
+ }
+ }
+ note =
+ (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key])) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key]);
+ return note;
+}
+/**
+ * Determines whether the provided data is for a macOS installer.
+ * @param data The data against which to check for a macOS installer.
+ */
+export function isMacOSInstaller(objectGraph, data) {
+ return derivedData.value(data, "isMacOSInstaller", () => {
+ const isMac = objectGraph.client.isMac;
+ if (!isMac) {
+ return false;
+ }
+ const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId");
+ if (!serverData.isDefinedNonNull(bundleId)) {
+ return false;
+ }
+ return bundleId.startsWith("com.apple.InstallAssistant");
+ });
+}
+/**
+ * Check whether an app is unsupported by the current companion configuration.
+ * @param data The data representing an app listing.
+ */
+export function isUnsupportedByCurrentCompanion(objectGraph, data) {
+ const deletableApps = sad.systemApps(objectGraph);
+ if (objectGraph.host.isWatch) {
+ // AppConduit will handle determining if SAD apps are supported
+ if (deletableApps.isUnsupportedDeletableSystemAppFromData(data)) {
+ return true;
+ }
+ else if (objectGraph.client.isTinkerWatch) {
+ return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") && !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ return false;
+ }
+ else {
+ if (isUnsupportedDeletableSystemAppFromData(objectGraph, data, objectGraph.client.isTinkerWatch)) {
+ return true;
+ }
+ else if (objectGraph.client.isTinkerWatch) {
+ if (deletableApps.isSystemAppFromData(data)) {
+ // We don't consider whether an app is marked as standalone with companion
+ // when running in standalone mode. We always want SAD apps to be installable.
+ return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isDeliveredInIOSAppForWatchOS") &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ else {
+ return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ }
+ else {
+ // We only allow standalone system apps to be installed when the watch
+ // is not running in standalone mode. This simplifies things for other teams.
+ return (objectGraph.client.isWatch &&
+ deletableApps.isSystemAppFromData(data) &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ }
+}
+/**
+ * @deprecated Use sad.isUnsupportedDeletableSystemAppFromData instead.
+ * Check whether a SAD app is unsupported by the current companion configuration.
+ *
+ * @param data The data representing an app listing.
+ * @param isTinkerWatch Whether the current device is a tinker watch
+ */
+export function isUnsupportedDeletableSystemAppFromData(objectGraph, data, isTinkerWatch) {
+ if (isTinkerWatch && sad.systemApps(objectGraph).isSystemAppFromData(data)) {
+ const watchBundleId = mediaAttributes.attributeAsString(data, "watchBundleId");
+ if (serverData.isDefinedNonNullNonEmpty(watchBundleId)) {
+ switch (watchBundleId) {
+ // rdar://63111354 (On Tinker device, able to attempt to download non-Tinker 1st and 3rd party app)
+ // These apps should prevented from installing on a Tinker device
+ case "com.apple.mobilemail.watchkitapp":
+ case "com.apple.news.watchkitapp":
+ case "com.apple.iBooks.watchkitapp":
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+ return false;
+}
+/**
+ * Device Sizes
+ *
+ * Please do not use these constants for anything but screenshots.
+ * Our code should not depend on absolute screen sizes for anything
+ * not related to selecting the correct screenshots to display. -km
+ */
+/// The screen size of iPhone 6.5" devices.
+export const screenSizeIphone65 = new modelsBase.Size(414.0, 896.0);
+/// The screen size of iPhone 5.8" devices.
+export const screenSizeIPhone58 = new modelsBase.Size(375.0, 812.0);
+/// The screen size of iPhone 6+ like devices.
+export const screenSizeIPhone6Plus = new modelsBase.Size(414.0, 736.0);
+/// The screen size of iPhone 6 like devices.
+export const screenSizeIPhone6 = new modelsBase.Size(375.0, 667.0);
+/// The screen size of iPhone 5 like devices.
+export const screenSizeIPhone5 = new modelsBase.Size(320.0, 568.0);
+/// The screen size of original iPhone like devices.
+export const screenSizeIPhoneOriginal = new modelsBase.Size(320.0, 480.0);
+/// The screen size of iPad and iPad mini devices.
+export const screenSizeIPad = new modelsBase.Size(768.0, 1024.0);
+/// The screen size of 7th and 8th gen 10.2" iPads.
+export const screenSizeIPad102 = new modelsBase.Size(810.0, 1080.0);
+/// The screen size of iPad pro 10.5" devices.
+export const screenSizeIPad105 = new modelsBase.Size(834.0, 1112.0);
+/// The screen size of iPad pro 11" devices.
+export const screenSizeIPad11 = new modelsBase.Size(834.0, 1194.0);
+/// The screen size of iPad Pro 11" devices in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeIPad11Landscape = new modelsBase.Size(1194.0, 834.0);
+/// The screen size of iPad pro 12.9" devices.
+export const screenSizeIPadPro = new modelsBase.Size(1024.0, 1366.0);
+/// The screen size of iPad pro 12.9" devices, with rounded corners.
+export const screenSizeIPadPro2018 = new modelsBase.Size(1024.0, 1366.0);
+/// The screen size of iPad pro 12.9" devices, with rounded corners, in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeIPadPro2018Landscape = new modelsBase.Size(1366.0, 1024.0);
+// The screen size of the J310 iPad device.
+export const screenSizeIPadJ310 = new modelsBase.Size(744.0, 1133.0);
+// The screen size of the J310 iPad device, in landscape orientation.
+// rdar: //83176176 (J310: kMGQMainScreenCanvasSizes reports width as largest dimension, contrary to all other iPads and UIKit)
+export const screenSizeIPadJ310Landscape = new modelsBase.Size(1133.0, 744.0);
+/// The screen size of J720/J721 devices.
+export const screenSizeJ720 = new modelsBase.Size(1032.0, 1376.0);
+/// The screen size of J720/J721, in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeJ720Landscape = new modelsBase.Size(1376.0, 1032.0);
+/// The screen size of J717/J718 devices.
+export const screenSizeJ717 = new modelsBase.Size(834.0, 1210.0);
+/// The screen size of J717/J718, in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeJ717Landscape = new modelsBase.Size(1210.0, 834.0);
+/// The screen size of the 42mm Apple Watch devices.
+export const screenSizeWatch = new modelsBase.Size(312.0, 390.0);
+/// The screen size of large 2018 Apple Watch devices.
+export const screenSizeWatch2018 = new modelsBase.Size(368.0, 448.0);
+// The screen size of the large 2021 Apple Watch devices.
+export const screenSizeWatch2021 = new modelsBase.Size(396.0, 484.0);
+// The screen size for the 2022 Apple Watch devices.
+export const screenSizeWatch2022 = new modelsBase.Size(410.0, 502.0);
+// The screen size for the 2024 Apple Watch devices.
+export const screenSizeWatch2024 = new modelsBase.Size(416.0, 496.0);
+// The screen size for the Apple Watch Ultra / Ultra 2 devices.
+export const screenSizeWatchUltra = new modelsBase.Size(410.0, 502.0);
+/// The screen size for iPad device.
+export const screenSizeIPadAir2020 = new modelsBase.Size(820.0, 1180.0);
+/// The screen size for iPhone devices.
+export const screenSizeIPhone131 = new modelsBase.Size(360.0, 780.0);
+export const screenSizeIPhone132 = new modelsBase.Size(390.0, 844.0);
+export const screenSizeIPhone134 = new modelsBase.Size(428.0, 926.0);
+// The screen size for a 6.1" D73-style device.
+export const screenSizeIPhone61 = new modelsBase.Size(393.0, 852.0);
+// The screen size for a 6.7" D74-style device.
+export const screenSizeIPhone67 = new modelsBase.Size(430.0, 932.0);
+/// The screen size for a D93 device.
+export const screenSizeD93 = new modelsBase.Size(402.0, 874.0);
+/// The screen size for a D94 device.
+export const screenSizeD94 = new modelsBase.Size(440.0, 956.0);
+/// The screen size for a D23 device.
+export const screenSizeD23 = new modelsBase.Size(420.0, 912.0);
+/// The screen size for a N230 device.
+export const screenSizeN230 = new modelsBase.Size(422.0, 514.0);
+/// All phone types, in order of decreasing size.
+const decreasingPhoneTypes = [
+ "iphone_d74",
+ "iphone_6_5",
+ "iphone_d73",
+ "iphone_5_8",
+ "iphone6+",
+ "iphone6",
+ "iphone5",
+ "iphone",
+];
+// region Device Corner Radius
+/**
+ * The reason we need to hardcode these is because we may want to display screenshots with rounding for a device that is
+ * not the one the user is currently browsing the store with. For example, imagine that the user is browsing the store
+ * with an iPhone 8 but ends up looking at an app that only has D22 screenshots. They should see the D22 screenshots
+ * according to the D22 corner rounding, and using the current client's `screenCornerRadius` would not give us the
+ * proper value.
+ */
+/// The device corner radius of iPad pro 12.9" devices from 2018.
+const deviceCornerRadiusIpadPro2018 = 18.0;
+/// The device corner radius of iPad pro 11" devices.
+const deviceCornerRadiusIpad11 = 18.0;
+/// The device corner radius of iPhone 6.5" devices.
+const deviceCornerRadiusIphone65 = 41.5;
+/// The device corner radius of iPhone 5.8" devices.
+const deviceCornerRadiusIphone58 = 39.0;
+/// The device corner radius of iPhone 6.1" devices.
+const deviceCornerRadiusIphone61 = 55.0;
+/// The device corner radius of iPhone 6.7" devices.
+const deviceCornerRadiusIphone67 = 55.0;
+/// The device corner radius of large 2018 Apple Watch devices.
+const deviceCornerRadiusWatch2018 = 34.0;
+/// The outer device corner radius of large Apple Watch devices.
+const outerDeviceCornerRadiusWatch = 30.0;
+/// The device border thickness for Apple Watch.
+const deviceBorderThicknessWatch = 13.0;
+/// The device border thickness for 2018 Apple Watch.
+const deviceBorderThicknessWatch2018 = 11.0;
+/// The device corner radius for 2021 Apple Watch.
+const deviceCornerRadiusWatch2021 = 55;
+/// The device border thickness for 2021 Apple Watch.
+const deviceBorderThicknessWatch2021 = 5.5;
+/// The device corner radius for 2022/2024 Apple Watch.
+const deviceCornerRadiusWatch2022 = 108;
+/// The outer device corner radius for 2022/2024 Apple Watch.
+const deviceOuterCornerRadiusWatch2022 = 112.5;
+/// The device border thickness for 2022/2024 Apple Watch.
+const deviceBorderThicknessWatch2022 = 4.5;
+export function currentAppPlatform(objectGraph) {
+ var _a;
+ switch (objectGraph.client.deviceType) {
+ case "web":
+ return unwrap((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform);
+ default:
+ return objectGraph.client.deviceType;
+ }
+}
+/// The SF Symbol name that represents the media type.
+export function systemImageNameForAppPlatform(appPlatform) {
+ switch (appPlatform) {
+ case "phone":
+ return "iphone";
+ case "pad":
+ return "ipad";
+ case "tv":
+ return "tv";
+ case "watch":
+ return "applewatch";
+ case "mac":
+ return "macbook";
+ case "messages":
+ return "message";
+ case "vision":
+ return "visionpro";
+ default:
+ unreachable(appPlatform);
+ }
+}
+/**
+ * Returns the factor by which to multiply an artwork's portrait-equivalent width, in order to compute the artwork's
+ * device-rounded corner radius. This is useful because we want to display screenshots for device-rounded screenshots
+ * with a corner radius that is scaled to that of the device screen.
+ *
+ * r = w * r'
+ * r' = R / W
+ *
+ * Where:
+ * r: scaled radius
+ * r': The return value of this function.
+ * R: device corner radius
+ * w: width at which the screenshot will be displayed
+ * W: device width
+ *
+ * We need to have this value here in the JS because we may need to display device-rounded screenshots on a device that
+ * does not have a device corner radius; there is no native API for querying the corner radius of various devices and,
+ * even if there were, we want to avoid specific screen-size checks in the native code.
+ * @param type The screenshot type vended by the server.
+ * @returns {number} The device corner radius factor, or null if the device does not have a corner radius.
+ */
+function deviceCornerRadiusFactorForMediaType(objectGraph, type) {
+ // Let's only bridge over and access client's properties if we need to.
+ switch (type) {
+ case "ipadPro_2018":
+ return deviceCornerRadiusIpadPro2018 / screenSizeIPadPro2018.width;
+ case "ipad_11":
+ return deviceCornerRadiusIpad11 / screenSizeIPad11.width;
+ case "iphone_6_5":
+ return deviceCornerRadiusIphone65 / screenSizeIphone65.width;
+ case "iphone_5_8":
+ return deviceCornerRadiusIphone58 / screenSizeIPhone58.width;
+ case "iphone_d73":
+ return deviceCornerRadiusIphone61 / screenSizeIPhone61.width;
+ case "iphone_d74":
+ return deviceCornerRadiusIphone67 / screenSizeIPhone67.width;
+ case "appleWatch_2018":
+ return deviceCornerRadiusWatch2018 / screenSizeWatch2018.width;
+ case "appleWatch_2021":
+ return deviceCornerRadiusWatch2021 / screenSizeWatch2021.width;
+ case "appleWatch_2022":
+ return deviceCornerRadiusWatch2022 / screenSizeWatch2022.width;
+ case "appleWatch_2024":
+ return deviceCornerRadiusWatch2022 / screenSizeWatch2024.width;
+ default:
+ return null;
+ }
+}
+function deviceOuterCornerRadiusFactorForMediaType(objectGraph, type) {
+ switch (type) {
+ case "appleWatch":
+ return outerDeviceCornerRadiusWatch / screenSizeWatch.width;
+ case "appleWatch_2022":
+ return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2022.width;
+ case "appleWatch_2024":
+ return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2024.width;
+ default:
+ return deviceCornerRadiusFactorForMediaType(objectGraph, type);
+ }
+}
+function deviceBorderThicknessForMediaType(objectGraph, type) {
+ switch (type) {
+ case "appleWatch":
+ return deviceBorderThicknessWatch / screenSizeWatch.width;
+ case "appleWatch_2018":
+ return deviceBorderThicknessWatch2018 / screenSizeWatch2018.width;
+ case "appleWatch_2021":
+ return deviceBorderThicknessWatch2021 / screenSizeWatch2021.width;
+ case "appleWatch_2022":
+ return deviceBorderThicknessWatch2022 / screenSizeWatch2022.width;
+ case "appleWatch_2024":
+ return deviceBorderThicknessWatch2022 / screenSizeWatch2024.width;
+ default:
+ return null;
+ }
+}
+// endregion
+/**
+ Returns a boolean indicating if the client's operating
+ system is the same or later than the specified version.
+
+ @param version The full version number to check against
+ @returns true if the operating system is the same or newer than the specified version; false otherwise.
+ */
+export function isOSAtLeastVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return true;
+ }
+ const versionComponents = version.split(".");
+ const majorVersion = serverData.asNumber(versionComponents[0]) || 0;
+ const minorVersion = serverData.asNumber(versionComponents[1]) || 0;
+ const patchVersion = serverData.asNumber(versionComponents[2]) || 0;
+ return objectGraph.host.isOSAtLeast(majorVersion, minorVersion, patchVersion);
+}
+/**
+ Returns a boolean indicating if the system version of the active, paired watch (if any) is
+ at least the provided version number.
+
+ @param version The full version number to check against
+ @returns true if an active, paired watch exists, and its operating system version is the same or newer than the specified version; false otherwise.
+ */
+export function isActivePairedWatchOSAtLeastVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return true;
+ }
+ const versionComponents = version.split(".");
+ const majorVersion = serverData.asNumber(versionComponents[0]) || 0;
+ const minorVersion = serverData.asNumber(versionComponents[1]) || 0;
+ const patchVersion = serverData.asNumber(versionComponents[2]) || 0;
+ return objectGraph.client.isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion);
+}
+/**
+ * Check whether the active paired device's OS is the same or greater than a given version.
+ */
+export function isActivePairedDeviceAtLeastVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return true;
+ }
+ return objectGraph.client.isPairedSystemVersionAtLeast(version);
+}
+/**
+ * Check whether the active paired device's OS is below a given version.
+ */
+export function isActivePairedWatchOSBelowVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return false;
+ }
+ return objectGraph.client.isActivePairedWatchSystemVersionBelow(version);
+}
+export function shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfStyle) {
+ if (objectGraph.client.isTV) {
+ switch (shelfStyle) {
+ case "upsellBreakout":
+ return true;
+ default:
+ return false;
+ }
+ }
+ else {
+ switch (shelfStyle) {
+ case "smallLockup":
+ case "mediumLockup":
+ case "appTrailerLockup":
+ case "screenshotsLockup":
+ case "mixedMediaLockup":
+ case "upsellBreakout":
+ case "arcadeShowcase":
+ return true;
+ default:
+ return false;
+ }
+ }
+}
+export function shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, displayStyle) {
+ switch (displayStyle) {
+ case "LockupSmall":
+ case "LockupLarge":
+ case "BreakoutLarge":
+ case "Hero":
+ case "EditorialLockupLarge":
+ case "EditorialLockupLargeVariant":
+ case "EditorialLockupMedium":
+ case "EditorialLockupMediumVariant":
+ case "StoryMedium":
+ return true;
+ default:
+ return false;
+ }
+}
+/**
+ * The dynamic date string used by apps coming soon.
+ */
+export function dynamicPreorderDateFromData(objectGraph, data, fallbackLabel) {
+ const preorderOffer = offers.offerDataFromData(objectGraph, data);
+ const isPreorder = serverData.asString(preorderOffer, "type") === "preorder";
+ if (isPreorder) {
+ const releaseDateRaw = serverData.asString(preorderOffer, "expectedReleaseDate");
+ const dateDisplayFormat = contentAttributes.contentAttributeAsString(objectGraph, data, "expectedReleaseDateDisplayFormat");
+ if (serverData.isDefinedNonNullNonEmpty(dateDisplayFormat)) {
+ if (serverData.isDefinedNonNullNonEmpty(releaseDateRaw)) {
+ const releaseDate = dateUtil.parseDateOmittingTimeFromString(releaseDateRaw);
+ const tokenFormatMap = {
+ "@@expectedDateMY@@": objectGraph.loc.string("PreOrder.Date.MonthYear"),
+ "@@expectedDateMDY@@": objectGraph.loc.string("PreOrder.Date.MonthDayYear"),
+ };
+ for (const [serverToken, dateFormat] of Object.entries(tokenFormatMap)) {
+ if (dateDisplayFormat.includes(serverToken)) {
+ let formattedDate = objectGraph.loc.formatDateWithContext(dateFormat, releaseDate, "middleOfSentence");
+ if (objectGraph.client.isTV) {
+ formattedDate = formattedDate.replace(/ /g, "\u00a0");
+ }
+ return dateDisplayFormat.replace(serverToken, formattedDate);
+ }
+ }
+ }
+ return dateDisplayFormat;
+ }
+ }
+ // There was no dynamic date to display
+ return fallbackLabel;
+}
+/**
+ * The primary content for an editorial item.
+ * @param data The data from which to derive the primary content.
+ */
+export function primaryContentForData(objectGraph, data) {
+ const primaryContent = mediaRelationship.relationshipData(objectGraph, data, "primary-content");
+ if (serverData.isDefinedNonNullNonEmpty(primaryContent)) {
+ return primaryContent;
+ }
+ // If an EI has canvasData, then in MAPI response its "primary-content" relationship will not include the
+ // primary content meta data. Instead, the primary content data will be included in the "card-contents" relationship.
+ if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isCanvasAvailable")) {
+ return mediaRelationship.relationshipData(objectGraph, data, "card-contents");
+ }
+ return null;
+}
+const grayColorHex = "#9BA9BD";
+export const grayColorCriteria = {
+ colorHex: grayColorHex,
+ maxSaturation: 4,
+ maxBrightness: 9,
+};
+/**
+ * The list of tag background colors we can use to match an app icon background color against, based on saturation and brightness
+ */
+const saturationBrightnessBasedTagColorBuckets = [
+ grayColorCriteria, // Gray
+];
+/**
+ * The list of tag background colors we can use to match an app icon background color against
+ */
+const hueBasedTagColorBuckets = [
+ { colorHex: "#F7816F", minHue: 0, maxHue: 16 },
+ { colorHex: "#FF9034", minHue: 17, maxHue: 33 },
+ { colorHex: "#E3B059", minHue: 34, maxHue: 59 },
+ { colorHex: "#74BD66", minHue: 60, maxHue: 129 },
+ { colorHex: "#72C792", minHue: 130, maxHue: 169 },
+ { colorHex: "#61BFE2", minHue: 170, maxHue: 209 },
+ { colorHex: "#6EA3E9", minHue: 210, maxHue: 239 },
+ { colorHex: "#7D69FA", minHue: 240, maxHue: 259 },
+ { colorHex: "#B363F7", minHue: 260, maxHue: 289 },
+ { colorHex: "#EE7CBD", minHue: 290, maxHue: 360 }, // Pink
+];
+/**
+ * Find the matching tag color for an icon's background color
+ *
+ * @param iconBackgroundColor The bgColor from the icon artwork data
+ * @returns The closest matching color from the HI provided set of tag background colors for this icon color
+ */
+export function closestTagBackgroundColorForIcon(iconBackgroundColor) {
+ var _a;
+ return ((_a = color.findColorBucketForColor(iconBackgroundColor, saturationBrightnessBasedTagColorBuckets, hueBasedTagColorBuckets)) !== null && _a !== void 0 ? _a : color.fromHex(grayColorHex));
+}
+/**
+ * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object
+ * @param data An EditorialArtwork JSON object from MAPI
+ * @returns A JoeColorSet from parsing the input RGB values
+ */
+export function joeColorHexSetFromData(data) {
+ var _a, _b, _c, _d, _e;
+ const textGradient = [];
+ for (const gradientColorHex of serverData.asArrayOrEmpty(data, "textGradient")) {
+ if (isSome(gradientColorHex) && serverData.isString(gradientColorHex)) {
+ textGradient.push(gradientColorHex);
+ }
+ }
+ return {
+ bgColor: (_a = serverData.asString(data, "bgColor")) !== null && _a !== void 0 ? _a : undefined,
+ textColor1: (_b = serverData.asString(data, "textColor1")) !== null && _b !== void 0 ? _b : undefined,
+ textColor2: (_c = serverData.asString(data, "textColor2")) !== null && _c !== void 0 ? _c : undefined,
+ textColor3: (_d = serverData.asString(data, "textColor3")) !== null && _d !== void 0 ? _d : undefined,
+ textColor4: (_e = serverData.asString(data, "textColor4")) !== null && _e !== void 0 ? _e : undefined,
+ textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined,
+ };
+}
+/**
+ * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object
+ * @param data An EditorialArtwork JSON object from MAPI
+ * @returns A JoeColorSet from parsing the input RGB values
+ */
+export function joeColorSetFromData(data) {
+ var _a, _b, _c, _d, _e, _f;
+ const joeColorHexSet = joeColorHexSetFromData(data);
+ const textGradient = [];
+ for (const gradientColorHex of (_a = joeColorHexSet.textGradient) !== null && _a !== void 0 ? _a : []) {
+ const gradientColor = color.fromHex(gradientColorHex);
+ if (isSome(gradientColor)) {
+ textGradient.push(gradientColor);
+ }
+ }
+ return {
+ bgColor: (_b = color.fromHex(joeColorHexSet.bgColor)) !== null && _b !== void 0 ? _b : undefined,
+ textColor1: (_c = color.fromHex(joeColorHexSet.textColor1)) !== null && _c !== void 0 ? _c : undefined,
+ textColor2: (_d = color.fromHex(joeColorHexSet.textColor2)) !== null && _d !== void 0 ? _d : undefined,
+ textColor3: (_e = color.fromHex(joeColorHexSet.textColor3)) !== null && _e !== void 0 ? _e : undefined,
+ textColor4: (_f = color.fromHex(joeColorHexSet.textColor4)) !== null && _f !== void 0 ? _f : undefined,
+ textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined,
+ };
+}
+/**
+ * Attempt to find the first non-gray placeholder color
+ * @param joeColorSet The joe color set for a given icon
+ * @returns The color hext value to use for the joe color placeholder
+ */
+export function bestJoeColorPlaceholderSelectionLogic(joeColorSet) {
+ const joeColorKeys = [
+ "bgColor",
+ "textColor1",
+ "textColor2",
+ "textColor3",
+ "textColor4",
+ ];
+ for (const joeColorKey of joeColorKeys) {
+ const joeColorHex = joeColorSet[joeColorKey];
+ if (!serverData.isString(joeColorHex)) {
+ continue;
+ }
+ const isGrayColor = color.doesColorMeetCriteria(color.fromHex(joeColorHex), grayColorCriteria);
+ if (!isGrayColor) {
+ return joeColorHex;
+ }
+ }
+ return null;
+}
+//# sourceMappingURL=content.js.map \ No newline at end of file