diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/content/content.js | |
init commit
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.js | 2820 |
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 |
