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