this repo has no description
1import * as validation from "@jet/environment/json/validation"; 2import { isNothing, isSome, unwrapOptional as unwrap } from "@jet/environment/types/optional"; 3import * as models from "../../api/models"; 4import * as modelsBase from "../../api/models/base"; 5import * as modelsShelves from "../../api/models/shelves"; 6import { ads } from "../../api/typings/constants"; 7import * as derivedData from "../../foundation/json-parsing/derived-data"; 8import * as serverData from "../../foundation/json-parsing/server-data"; 9import * as mediaAttributes from "../../foundation/media/attributes"; 10import * as mediaDataFetching from "../../foundation/media/data-fetching"; 11import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; 12import * as mediaRelationship from "../../foundation/media/relationships"; 13import * as mediaUrlBuilder from "../../foundation/media/url-builder"; 14import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; 15import * as color from "../../foundation/util/color-util"; 16import * as dateUtil from "../../foundation/util/date-util"; 17import { unreachable } from "../../foundation/util/errors"; 18import { isDefinedNonNullNonEmpty } from "@apple-media-services/media-api"; 19import { editorialCardFromData } from "../../foundation/media/associations"; 20import * as client from "../../foundation/wrappers/client"; 21import * as videoDefaults from "../constants/video-constants"; 22import * as filtering from "../filtering"; 23import * as lockups from "../lockups/lockups"; 24import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; 25import * as metricsHelpersLocation from "../metrics/helpers/location"; 26import * as metricsHelpersMedia from "../metrics/helpers/media"; 27import * as offers from "../offers/offers"; 28import * as productPageVariants from "../product-page/product-page-variants"; 29import * as artwork from "./artwork/artwork"; 30import * as contentAttributes from "./attributes"; 31import * as contentDeviceFamily from "./device-family"; 32import * as sad from "./sad"; 33import { isFeatureEnabledForCurrentUser } from "../util/lottery"; 34class RunnabilityInfo { 35 constructor() { 36 this.runsOnIntel = true; 37 this.runsOnAppleSilicon = true; 38 this.requiresRosetta = false; 39 } 40} 41/** 42 * Determines a reasonable artwork use case from a given shelf style 43 * 44 * @param shelfStyle The shelf style to consider 45 */ 46export function artworkUseCaseFromShelfStyle(objectGraph, shelfStyle) { 47 switch (shelfStyle) { 48 case "inAppPurchaseLockup": 49 case "appShowcase": 50 case "smallLockup": { 51 return 1 /* ArtworkUseCase.LockupIconSmall */; 52 break; 53 } 54 case "mediumLockup": { 55 return 2 /* ArtworkUseCase.LockupIconMedium */; 56 break; 57 } 58 case "largeLockup": { 59 return 3 /* ArtworkUseCase.LockupIconLarge */; 60 break; 61 } 62 default: { 63 return 0 /* ArtworkUseCase.Default */; 64 } 65 } 66} 67/** 68 * Convert an API artwork object into an Artwork model object. 69 * @param artwork The artwork in API format. 70 * @returns An `Artwork` object. 71 */ 72export function artworkFromApiArtwork(objectGraph, artworkData, options) { 73 return validation.context("artworkFromApiArtwork", () => { 74 var _a, _b, _c; 75 const allowingTransparency = serverData.isDefinedNonNull(options.allowingTransparency) 76 ? options.allowingTransparency 77 : false; 78 const useJoeColorDefault = objectGraph.client.isVision || objectGraph.client.isWeb; 79 const withJoeColorPlaceholder = serverData.isDefinedNonNull(options.withJoeColorPlaceholder) 80 ? options.withJoeColorPlaceholder 81 : useJoeColorDefault; 82 const artworkUrl = serverData.asString(artworkData, "url"); 83 if (serverData.isNull(artworkUrl)) { 84 return null; 85 } 86 // Whether wide gamut is supported 87 const supportsWideGamut = serverData.asBooleanOrFalse(artworkData, "hasP3"); 88 // Add base variant 89 const variants = [ 90 artwork.createArtworkVariantForClient(objectGraph, allowingTransparency, supportsWideGamut, options.useCase), 91 ]; 92 // Add layered image variant 93 const supportsLayeredImage = serverData.asBooleanOrFalse(artworkData, "supportsLayeredImage"); 94 if (supportsLayeredImage && (objectGraph.client.isTV || objectGraph.client.isVision)) { 95 variants.push(artwork.createArtworkVariantForFormat(objectGraph, "lcr", supportsWideGamut, options.useCase)); 96 } 97 // Artwork Placeholder Color 98 // If we indicate the image could be transparent then we don't want a placeholder background 99 let placeholderBackgroundColor = null; 100 if (allowingTransparency) { 101 placeholderBackgroundColor = color.named("clear"); 102 } 103 else if (withJoeColorPlaceholder) { 104 const joeColorHexSet = joeColorHexSetFromData(artworkData); 105 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"); 106 const apiBackgroundColor = color.fromHex(placeholderColorHex); 107 if (!serverData.isNull(apiBackgroundColor)) { 108 placeholderBackgroundColor = apiBackgroundColor; 109 } 110 } 111 // If we don't want clear, joe color, or joe color fails to parse then fall back to default background 112 if (serverData.isNull(placeholderBackgroundColor) && !objectGraph.client.isVision) { 113 placeholderBackgroundColor = color.named("placeholderBackground"); 114 } 115 const textColorKey = (_c = options.overrideTextColorKey) !== null && _c !== void 0 ? _c : "textColor1"; 116 const apiTextColor = color.fromHex(serverData.asString(artworkData, textColorKey)); 117 const artworkModel = new modelsBase.Artwork(artworkUrl, options.overrideWidth || serverData.asNumber(artworkData, "width"), options.overrideHeight || serverData.asNumber(artworkData, "height"), variants); 118 artworkModel.backgroundColor = placeholderBackgroundColor; 119 artworkModel.checksum = serverData.asString(artworkData, "checksum"); 120 if (serverData.isDefinedNonNull(apiTextColor)) { 121 artworkModel.textColor = apiTextColor; 122 } 123 if (serverData.isDefinedNonNull(options.style)) { 124 artworkModel.style = options.style; 125 } 126 if (serverData.isDefinedNonNull(options.cropCode)) { 127 artworkModel.crop = options.cropCode; 128 } 129 if (serverData.isDefinedNonNull(options.contentMode)) { 130 artworkModel.contentMode = options.contentMode; 131 } 132 return artworkModel; 133 }); 134} 135export function impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions) { 136 return validation.context("impressionableAppIconFromData", () => { 137 const rawArtwork = iconFromData(objectGraph, data, artworkOptions); 138 if (!serverData.isDefinedNonNull(rawArtwork)) { 139 return null; 140 } 141 const icon = new models.ImpressionableArtwork(rawArtwork); 142 const title = mediaAttributes.attributeAsString(data, "name"); 143 const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, title, metricsOptions); 144 metricsHelpersImpressions.addImpressionFields(objectGraph, icon, metricsImpressionOptions); 145 return icon; 146 }); 147} 148/** 149 * Batch method for `impressionableAppIconFromData`. Doesn't push location stack or increment location counter, matching other behavior with other icon grids. 150 * @param dataCollection Data container array with app data. 151 * @param metricsOptions Metrics blob containing information about page and location. 152 * @returns Array of `ImpressionableArtwork` 153 */ 154export function impressionableAppIconsFromDataCollection(objectGraph, dataCollection, metricsOptions, artworkOptions) { 155 return validation.context("impressionableAppIconFromData", () => { 156 const icons = []; 157 if (serverData.isNullOrEmpty(metricsOptions.targetType)) { 158 metricsOptions.targetType = "artwork"; 159 } 160 for (const data of dataCollection) { 161 const icon = impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions); 162 if (icon) { 163 icons.push(icon); 164 metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); 165 } 166 } 167 return icons; 168 }); 169} 170/** 171 * Defines possible use cases for SearchChartOrCategoryBrick. 172 */ 173export var SearchChartOrCategoryBrickUseCase; 174(function (SearchChartOrCategoryBrickUseCase) { 175 SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["seeAllPage"] = 0] = "seeAllPage"; 176 SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["categoryBreakout"] = 1] = "categoryBreakout"; 177 SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["other"] = 2] = "other"; 178})(SearchChartOrCategoryBrickUseCase || (SearchChartOrCategoryBrickUseCase = {})); 179/** 180 * Gets all possible artwork that this chart or category can show 181 * @param objectGraph 182 * @param data 183 * @param isForSeeAllPage Whether or not the chart or category is on the see-all page or not; 184 * this is because the see-all page should always have the `Density1` style 185 * @param style The style of the chart or category that will be rendered 186 * @returns All permutations of artowrk that the chart or category can show 187 */ 188export function searchChartOrCategoryArtworkFromData(objectGraph, data, useCase, style) { 189 const artworkPath = "editorialArtwork.searchCategoryBrick"; 190 const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath); 191 if (serverData.isNullOrEmpty(artworkData)) { 192 return null; 193 } 194 let artworkStyle = style; 195 if (useCase === SearchChartOrCategoryBrickUseCase.seeAllPage) { 196 artworkStyle = models.GenericSearchPageShelfDisplayStyleDensity.Density1; 197 } 198 /// Crops = [LTR crop, RTL crop] 199 /// ContentModes = [ContentMode for LTR crop, ContentMode for RTL crop] 200 /// Note: These must be the same length 201 let crops = []; 202 let contentModes = []; 203 switch (artworkStyle) { 204 /// Tile 205 case models.GenericSearchPageShelfDisplayStyleDensity.Density1: 206 const width = useCase === SearchChartOrCategoryBrickUseCase.categoryBreakout ? "1191" : "2350"; 207 artworkData["width"] = width; 208 artworkData["height"] = "670"; 209 crops = ["SCB.ApSCBL01", "SCB.ApSCBL03"]; 210 contentModes = [modelsBase.ArtworkContentMode.right, modelsBase.ArtworkContentMode.left]; 211 break; 212 /// Pill 213 case models.GenericSearchPageShelfDisplayStyleDensity.Density2: 214 artworkData["width"] = "2482"; 215 artworkData["height"] = "670"; 216 crops = ["SCB.ApSCBS01", "SCB.ApSCBS02"]; 217 contentModes = [modelsBase.ArtworkContentMode.left, modelsBase.ArtworkContentMode.right]; 218 break; 219 /// Round 220 case models.GenericSearchPageShelfDisplayStyleDensity.Density3: 221 artworkData["width"] = "670"; 222 artworkData["height"] = "670"; 223 crops = ["cc"]; 224 contentModes = [modelsBase.ArtworkContentMode.scaleAspectFit]; 225 break; 226 default: 227 break; 228 } 229 return crops.map((crop, index) => { 230 return artworkFromApiArtwork(objectGraph, artworkData, { 231 cropCode: crop, 232 contentMode: index < contentModes.length ? contentModes[index] : null, 233 useCase: 0 /* ArtworkUseCase.Default */, 234 withJoeColorPlaceholder: true, 235 }); 236 }); 237} 238/** 239 * Create an icon artwork from the provided data. 240 * @param objectGraph The object graph. 241 * @param data The data object to pull icon data from. 242 * @param artworkOptions The options for creating the artwork. 243 * @param clientIdentifierOverride A client identifier override. 244 * @param productVariantData The product variant data to use to select the icon. 245 * @param attributePlatformOverride An override platform, from which to fetch the icon. 246 * @returns An `Artwork` object representing the icon. 247 */ 248export function iconFromData(objectGraph, data, artworkOptions, clientIdentifierOverride, productVariantData, attributePlatformOverride = undefined) { 249 return validation.context("iconFromData", () => { 250 if (!data) { 251 validation.unexpectedNull("ignoredValue", "data"); 252 return null; 253 } 254 const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : iconAttributePlatform(objectGraph, data, clientIdentifierOverride); 255 const usePrerenderedIconArtwork = shouldUsePrerenderedIconArtwork(objectGraph); 256 // The preferred client identifier to use when selecting the artwork. 257 // This client identifier here ensures that we always prefer pill artwork for messages and circular artwork for watch / vision. 258 // Unless there's an override specified where another artwork type needs to be used (for example, in developer pages). 259 const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier; 260 // Watch 261 const watchIcon = watchIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, usePrerenderedIconArtwork, attributePlatform); 262 if (isSome(watchIcon)) { 263 return watchIcon; 264 } 265 // Messages 266 const messagesIcon = messagesIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, attributePlatform); 267 if (isSome(messagesIcon)) { 268 return messagesIcon; 269 } 270 // In-App Purchases 271 const iapIcon = inAppPurchaseIconFromData(objectGraph, data, artworkOptions); 272 if (isSome(iapIcon)) { 273 return iapIcon; 274 } 275 // Bundles 276 const bundlesIcon = bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork); 277 if (isSome(bundlesIcon)) { 278 return bundlesIcon; 279 } 280 // Calculate variant data if one wasn't provided from caller. 281 if (serverData.isNull(productVariantData)) { 282 productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); 283 } 284 const artworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "artwork", attributePlatform); 285 // tvOS 286 const tvIcon = tvIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform); 287 if (isSome(tvIcon)) { 288 return tvIcon; 289 } 290 // visionOS 291 const visionIcon = visionIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform); 292 if (isSome(visionIcon)) { 293 return visionIcon; 294 } 295 // macOS & iOS 296 return macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform); 297 }); 298} 299/** 300 * Determines if a client is capable of showing pre-rendered icon artwork, and if the relevant 301 * feature / bag flags are enabled. 302 * @param objectGraph Current object graph 303 * @returns True if we should use prerendered icon artwork. 304 */ 305export function shouldUsePrerenderedIconArtwork(objectGraph) { 306 const clientSupportsPrerenderedIconArtwork = objectGraph.client.isWatch || objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isWeb; 307 const isEnabledForUser = isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.iconArtworkRolloutRate); 308 return (isEnabledForUser && 309 objectGraph.bag.enableIconArtwork && 310 objectGraph.client.isIconArtworkCapable && 311 clientSupportsPrerenderedIconArtwork); 312} 313function watchIconFromData(objectGraph, data, artworkOptions, clientIdentifier, usePrerenderedIconArtwork, attributePlatform) { 314 if (clientIdentifier !== client.watchIdentifier && 315 !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS") && 316 !objectGraph.client.isWatch) { 317 return null; 318 } 319 // Attempt to use pre-rendered circular icon artwork first, if applicable 320 if (usePrerenderedIconArtwork) { 321 const iconArtworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularIconArtwork"); 322 if (isSome(iconArtworkData)) { 323 return artworkFromApiArtwork(objectGraph, iconArtworkData, { 324 ...artworkOptions, 325 style: "roundPrerendered", 326 cropCode: "bb", 327 withJoeColorPlaceholder: true, 328 }); 329 } 330 } 331 // Fallback to the legacy icon artwork 332 const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularArtwork"); 333 if (isSome(artworkData)) { 334 const style = usePrerenderedIconArtwork ? "roundPrerendered" : "round"; 335 const cropCode = usePrerenderedIconArtwork ? "ic" : undefined; 336 return artworkFromApiArtwork(objectGraph, artworkData, { 337 ...artworkOptions, 338 style: style, 339 cropCode: cropCode, 340 withJoeColorPlaceholder: true, 341 }); 342 } 343 return null; 344} 345function messagesIconFromData(objectGraph, data, artworkOptions, clientIdentifier, attributePlatform) { 346 const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data); 347 const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data); 348 const shouldShowMessagesIcon = hasMessagesExtension && (clientIdentifier === client.messagesIdentifier || isHiddenFromSpringboard); 349 const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "ovalArtwork"); 350 if (shouldShowMessagesIcon && serverData.isDefinedNonNull(artworkData)) { 351 return artworkFromApiArtwork(objectGraph, artworkData, { 352 ...artworkOptions, 353 style: "pill", 354 }); 355 } 356 return null; 357} 358function inAppPurchaseIconFromData(objectGraph, data, artworkOptions) { 359 if (data.type !== "in-apps") { 360 return null; 361 } 362 const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork"); 363 if (isSome(artworkData)) { 364 return artworkFromApiArtwork(objectGraph, artworkData, { 365 ...artworkOptions, 366 style: "iap", 367 }); 368 } 369 return null; 370} 371function bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork) { 372 if (data.type !== "app-bundles") { 373 return null; 374 } 375 // Attempt to use pre-rendered icon artwork first, if applicable 376 if (usePrerenderedIconArtwork) { 377 const iconArtworkData = mediaAttributes.attributeAsDictionary(data, "iconArtwork"); 378 if (isSome(iconArtworkData)) { 379 return artworkFromApiArtwork(objectGraph, iconArtworkData, { 380 ...artworkOptions, 381 style: "roundedRectPrerendered", 382 cropCode: "bb", 383 }); 384 } 385 } 386 // Fallback to the legacy icon artwork 387 const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork"); 388 if (isSome(artworkData)) { 389 const style = usePrerenderedIconArtwork ? "roundedRectPrerendered" : "roundedRect"; 390 const cropCode = usePrerenderedIconArtwork ? "ia" : undefined; 391 return artworkFromApiArtwork(objectGraph, artworkData, { 392 ...artworkOptions, 393 style: style, 394 cropCode: cropCode, 395 allowingTransparency: true, 396 }); 397 } 398 return null; 399} 400function tvIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) { 401 if (attributePlatform !== "appletvos" && clientIdentifier !== client.tvIdentifier) { 402 return null; 403 } 404 return artworkFromApiArtwork(objectGraph, artworkData, { 405 ...artworkOptions, 406 style: "tvRect", 407 }); 408} 409function visionIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) { 410 if (attributePlatform !== "xros" && clientIdentifier !== "VisionAppStore" /* ClientIdentifier.VisionAppStore */) { 411 return null; 412 } 413 return artworkFromApiArtwork(objectGraph, artworkData, { 414 ...artworkOptions, 415 style: "round", 416 }); 417} 418function macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform) { 419 const isMac = attributePlatform === "osx"; 420 const allowTransparency = isMac && !preprocessor.GAMES_TARGET; 421 // Attempt to use pre-rendered icon artwork first, if applicable 422 if (usePrerenderedIconArtwork) { 423 const iconArtworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "iconArtwork", attributePlatform); 424 if (isSome(iconArtworkData)) { 425 return artworkFromApiArtwork(objectGraph, iconArtworkData, { 426 ...artworkOptions, 427 style: "roundedRectPrerendered", 428 cropCode: "bb", 429 allowingTransparency: allowTransparency, 430 }); 431 } 432 } 433 // Fallback to the standard icon artwork 434 let style; 435 let cropCode; 436 if (usePrerenderedIconArtwork) { 437 style = "roundedRectPrerendered"; 438 cropCode = isMac ? "ib" : "ia"; 439 } 440 else { 441 style = isMac ? "unadorned" : "roundedRect"; 442 cropCode = "bb"; 443 } 444 return artworkFromApiArtwork(objectGraph, artworkData, { 445 ...artworkOptions, 446 style: style, 447 cropCode: cropCode, 448 allowingTransparency: allowTransparency, 449 }); 450} 451/** 452 * Determines the best attribute platform to use for the icon. 453 * @param objectGraph Current object graph 454 * @param data The product data 455 * @param clientIdentifierOverride The client identifier override to use, if any 456 * @returns 457 */ 458export function iconAttributePlatform(objectGraph, data, clientIdentifierOverride) { 459 switch (clientIdentifierOverride) { 460 case client.watchIdentifier: 461 case client.messagesIdentifier: { 462 return "ios"; 463 } 464 case client.tvIdentifier: { 465 return "appletvos"; 466 } 467 case "VisionAppStore" /* ClientIdentifier.VisionAppStore */: { 468 return "xros"; 469 } 470 default: { 471 return contentAttributes.bestAttributePlatformFromData(objectGraph, data, clientIdentifierOverride); 472 } 473 } 474} 475/** 476 * Determine the media platform, given the app platform and screenshot type. 477 * @param appPlatform The app platform specific to this media. 478 * @param type The response screenshot type, which is applicable for both screenshots and trailers, for this media. 479 * @param supplementaryAppPlatforms 480 * @returns {MediaPlatform} The configured media platform object. 481 * TODO: legacy_export 482 */ 483export function mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type, supplementaryAppPlatforms) { 484 if (!appPlatform) { 485 return null; 486 } 487 const systemImageName = systemImageNameForAppPlatform(appPlatform); 488 const deviceCornerRadius = deviceCornerRadiusFactorForMediaType(objectGraph, type); 489 const deviceBorderThickness = deviceBorderThicknessForMediaType(objectGraph, type); 490 const outerDeviceCornerRadius = deviceOuterCornerRadiusFactorForMediaType(objectGraph, type); 491 return new modelsBase.MediaPlatform(appPlatform, type, systemImageName, supplementaryAppPlatforms, deviceCornerRadius, deviceBorderThickness, outerDeviceCornerRadius); 492} 493/** 494 * Configures the trailers object from the platform data. 495 * @param data The platform data to use. 496 * @param videoConfiguration config to use for the trailers 497 * @param metricsOptions The metrics options to use. 498 * @param adamId The adamId for the lockup. 499 * @param isAd Whether the trailers are for an ad lockup. Defaults to false. 500 * @param cropCode The crop code to use for the video preview. 501 * @returns {Trailers} The configured trailers object. 502 */ 503export function trailersFromData(objectGraph, data, videoConfiguration, metricsOptions, adamId, isAd = false, cropCode) { 504 const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoConfiguration, null, null, isAd, cropCode); 505 if (!platformVideos) { 506 return null; 507 } 508 const videoPreviews = platformVideos.videos; 509 const trailerVideos = []; 510 if (videoPreviews && videoPreviews.length > 0) { 511 for (const trailerVideo of videoPreviews) { 512 metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, trailerVideo, { 513 ...metricsOptions, 514 id: adamId, 515 }); 516 trailerVideos.push(trailerVideo); 517 } 518 } 519 let trailers = null; 520 if (trailerVideos.length > 0) { 521 trailers = new modelsShelves.Trailers(); 522 trailers.videos = trailerVideos; 523 trailers.mediaPlatform = platformVideos.mediaPlatform; 524 } 525 return trailers; 526} 527/** 528 * A convenience class for encapsulating a `Video` that is tied to a specific `MediaPlatform`. 529 */ 530class PlatformVideos { 531 constructor(videos, mediaPlatform) { 532 this.videos = videos; 533 this.mediaPlatform = mediaPlatform; 534 } 535} 536/** 537 * Finds the best platform video previews to use for the given parameters. 538 * @param data The data from which to derive the platform videos. 539 * @param videoConfiguration A video configuration to use for the videos 540 * @param includedAppPlatforms If provided, restricts the resulting platform videos to only these platforms 541 * @param productVariantData 542 * @param isAd Whether the video preview data is for an ad. Defaults to false. 543 * @param cropCode The crop code to use for the preview artwork. 544 * @returns The best available platform videos. 545 */ 546export function platformVideoPreviewFromData(objectGraph, data, videoConfiguration, includedAppPlatforms = null, productVariantData = null, isAd = false, cropCode) { 547 return validation.context("platformVideoPreviewFromData", () => { 548 if (serverData.isNull(productVariantData)) { 549 productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // create variant data if not provided. 550 } 551 const videoPreviewsByTypeData = videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd); 552 const videoPreviewsByType = {}; 553 if (!videoPreviewsByTypeData) { 554 return null; 555 } 556 let sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, objectGraph.host.clientIdentifier, objectGraph.client.deviceType); 557 if (serverData.isDefinedNonNull(includedAppPlatforms)) { 558 // If we have a restricted set of included app platforms, use those platforms 559 // to build our sortedAppPlatforms array in the proper sort order 560 const includedSortedAppPlatforms = []; 561 for (const appPlatform of sortedAppPlatforms) { 562 if (includedAppPlatforms.includes(appPlatform)) { 563 includedSortedAppPlatforms.push(appPlatform); 564 } 565 } 566 sortedAppPlatforms = includedSortedAppPlatforms; 567 } 568 if (sortedAppPlatforms.length === 0) { 569 return null; 570 } 571 for (const appPlatform of sortedAppPlatforms) { 572 const types = mediaTypesForAppPlatform(objectGraph, appPlatform, objectGraph.client.screenSize); 573 for (const type of Object.keys(videoPreviewsByTypeData)) { 574 const videosDataForType = serverData.asArrayOrEmpty(videoPreviewsByTypeData, type); 575 const videosForType = []; 576 for (const video of videosDataForType) { 577 const previewFrame = serverData.asDictionary(video, "previewFrame"); 578 if (!previewFrame) { 579 validation.unexpectedNull("ignoredValue", "object", `videoPreviewsByType.${type}.previewFrame`); 580 continue; 581 } 582 const videoUrl = serverData.asString(video, "video"); 583 if (!videoUrl) { 584 validation.unexpectedNull("ignoredValue", "string", `videoPreviewsByType.${type}.video`); 585 continue; 586 } 587 const preview = artwork.createArtworkForResource(objectGraph, serverData.asString(previewFrame, "url"), serverData.asNumber(previewFrame, "width"), serverData.asNumber(previewFrame, "height"), null, null, serverData.asString(previewFrame, "checksum")); 588 if (serverData.isDefinedNonNull(cropCode)) { 589 preview.crop = cropCode; 590 } 591 videosForType.push(new modelsBase.Video(videoUrl, preview, videoConfiguration)); 592 } 593 videoPreviewsByType[type] = videosForType; 594 } 595 for (const type of types) { 596 if (videoPreviewsByType[type]) { 597 return new PlatformVideos(videoPreviewsByType[type], mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type)); 598 } 599 } 600 } 601 return null; 602 }); 603} 604/** 605 * Configures the videos from some platform data. 606 * @param data The store platform data. 607 * @returns A list of `Video` objects. 608 */ 609export function videoPreviewsFromData(objectGraph, data) { 610 return validation.context("videoPreviewsFromApiPlatformData", () => { 611 const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoDefaults.defaultVideoConfiguration(objectGraph)); 612 if (platformVideos) { 613 return platformVideos.videos; 614 } 615 else { 616 return []; 617 } 618 }); 619} 620/** 621 * Determines the `AppPlatform` to use, in order to determine appropriate `MediaType` for media. 622 * @param {AppPlatform} appPlatform The underlying `AppPlatform`. 623 * @returns {AppPlatform} The `AppPlatform` that we map to in order to select the appropriate `MediaType`. 624 */ 625function selectionAppPlatformFromAppPlatform(objectGraph, appPlatform) { 626 if (appPlatform === "messages") { 627 switch (objectGraph.client.deviceType) { 628 case "pad": { 629 return "pad"; 630 } 631 default: { 632 return "phone"; 633 } 634 } 635 } 636 return appPlatform; 637} 638/** 639 * Provide the caller with an ordered array of screenshots for a given context. The first object can be used in search 640 * lockups, and the array will only contain screenshots for the supported app platforms. 641 * 642 * @param data The api product data (containing supported app platforms and screenshots) 643 * @param useCase 644 * @param includedAppPlatforms Optionally, a list of app platforms to confine the screenshots to. 645 * @param clientIdentifierOverride 646 * @param productVariantData 647 * @param isAd Whether the screenshots are being gathered for an ad lockup. Defaults to false. 648 * @returns An ordered array of screenshots for display on a product page 649 * */ 650export function screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms = null, clientIdentifierOverride, productVariantData, isAd = false, cropCode) { 651 return validation.context("screenshotsFromData", () => { 652 const screenshots = []; 653 if (serverData.isNull(productVariantData)) { 654 productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // resolve if not resolved by caller. 655 } 656 let sortedAppPlatforms = includedAppPlatforms; 657 if (!sortedAppPlatforms || sortedAppPlatforms.length === 0) { 658 const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier; 659 let preferredDeviceType = objectGraph.client.deviceType; 660 if (preferredClientIdentifier === client.watchIdentifier) { 661 preferredDeviceType = "watch"; 662 } 663 if (clientIdentifierOverride === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || 664 clientIdentifierOverride === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { 665 preferredDeviceType = "vision"; 666 } 667 sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, preferredClientIdentifier, preferredDeviceType); 668 } 669 for (const appPlatform of sortedAppPlatforms) { 670 const supplementaryAppPlatforms = []; 671 let screenshotData; 672 if (appPlatform === "messages") { 673 screenshotData = messagesScreenshotsFromData(objectGraph, data, "ios"); 674 if (supportsFunCameraFromData(objectGraph, data, "ios")) { 675 supplementaryAppPlatforms.push("faceTime"); 676 } 677 } 678 else if (appPlatform === "tv" && !objectGraph.host.isTV) { 679 // For tvOS screenshots displayed on other platforms. 680 screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "appletvos"); 681 } 682 else if (appPlatform === "vision" && !objectGraph.host.isVision) { 683 // For visionOS screenshots displayed on other platforms. 684 screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "xros"); 685 } 686 else if (appPlatform === "mac" && !objectGraph.host.isMac) { 687 // For Mac screenshots displayed on other platforms. 688 screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "osx"); 689 } 690 else if ((appPlatform === "phone" || appPlatform === "pad" || appPlatform === "watch") && 691 !objectGraph.host.isiOS && 692 !objectGraph.host.isWatch) { 693 // For iPhone / iPad / watch screenshots displayed on other platforms. 694 screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "ios"); 695 } 696 else { 697 screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd); 698 } 699 if (!screenshotData) { 700 continue; 701 } 702 const bestScreenshots = bestScreenshotData(objectGraph, screenshotData, appPlatform, useCase, supplementaryAppPlatforms, cropCode); 703 if (bestScreenshots) { 704 screenshots.push(bestScreenshots); 705 } 706 } 707 return screenshots; 708 }); 709} 710/** 711 * Creates an array of product media from the given screenshots. If videos are desired 712 * to be inserted in the same media row, this must be done elsewhere. 713 * @param objectGraph The object graph. 714 * @param data Apps resource data. 715 * @param screenshots The screenshots objects with which to configure the media. 716 * @return A list of product media objects. 717 */ 718function productMediaFromScreenshots(objectGraph, data, screenshots) { 719 const allMedia = []; 720 if (screenshots && screenshots.length > 0) { 721 const allPlatforms = screenshots.map((platformScreenshots) => { 722 return platformScreenshots.mediaPlatform; 723 }); 724 for (const screenshotsForPlatform of screenshots) { 725 // Create media items from all the screenshots. 726 const screenshotMediaItems = []; 727 for (const screenshotArtwork of screenshotsForPlatform.artwork) { 728 const screenshotItem = new modelsShelves.ProductMediaItem(); 729 screenshotItem.screenshot = screenshotArtwork; 730 screenshotMediaItems.push(screenshotItem); 731 } 732 const platform = screenshotsForPlatform.mediaPlatform; 733 const productMedia = new modelsShelves.ProductMedia(screenshotMediaItems, platform, allPlatforms, descriptionOfMediaPlatform(objectGraph, platform), descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms), placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms)); 734 allMedia.push(productMedia); 735 } 736 } 737 return allMedia; 738} 739/** 740 * Build a set of of `ProductMedia` from apps resource 741 * @param data Apps resource data 742 * @param useCase Artwork use case 743 * @param includedAppPlatforms What platforms are included. 744 * @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. 745 * @param clientIdentifierOverride 746 */ 747export function productMediaFromData(objectGraph, data, useCase, includedAppPlatforms = null, productVariantData = null, clientIdentifierOverride) { 748 const screenshots = screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms, clientIdentifierOverride, productVariantData); 749 return productMediaFromScreenshots(objectGraph, data, screenshots); 750} 751/** 752 * Finds the best screenshot data from a response to use for the given parameters. 753 * @param data The data from which to derive the screenshots. 754 * @param appPlatform The app platform to which the screenshots belong. 755 * @param supplementaryAppPlatforms 756 * @returns The best available screenshots. 757 */ 758function bestScreenshotData(objectGraph, data, appPlatform, useCase, supplementaryAppPlatforms, cropCode) { 759 const selectionPlatform = selectionAppPlatformFromAppPlatform(objectGraph, appPlatform); 760 const screenshotTypes = mediaTypesForAppPlatform(objectGraph, selectionPlatform, objectGraph.client.screenSize); 761 let bestScreenshot = null; 762 let bestScreenshotType; 763 for (let i = 0; i < screenshotTypes.length && !serverData.isDefinedNonNullNonEmpty(bestScreenshot); i++) { 764 bestScreenshot = serverData.asArrayOrEmpty(data, screenshotTypes[i]); 765 bestScreenshotType = screenshotTypes[i]; 766 } 767 if (serverData.isDefinedNonNullNonEmpty(bestScreenshot)) { 768 const artworks = bestScreenshot.map(function (screenshotArtwork) { 769 return artworkFromApiArtwork(objectGraph, screenshotArtwork, { 770 useCase: useCase, 771 cropCode: cropCode, 772 }); 773 }); 774 const platform = mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, bestScreenshotType, supplementaryAppPlatforms); 775 const screenshots = new modelsBase.Screenshots(artworks, platform); 776 return screenshots; 777 } 778 return null; 779} 780/** 781 * Returns a list of sorted app platforms for displaying screenshots. This contains the sorting logic for screenshots. 782 * 783 * @param data Server data for the app 784 * @param clientIdentifier Identifier of the current client. 785 * @param deviceType Type of the current device. 786 * @returns A sorted list of AppPlatform values to use when displaying 787 * */ 788export function sortedAppPlatformsFromData(objectGraph, data, clientIdentifier, deviceType) { 789 return derivedData.value(data, `sortedAppPlatformsFromData.${clientIdentifier}.${deviceType}`, () => { 790 var _a; 791 const supportedAppPlatforms = supportedAppPlatformsFromData(objectGraph, data); 792 const excludedAppPlatforms = []; 793 let sortedAppPlatforms = []; 794 const addAppPlatformIfPossible = function (appPlatform, excludePlatform) { 795 if (sortedAppPlatforms.indexOf(appPlatform) !== -1) { 796 return; 797 } 798 if (excludedAppPlatforms.indexOf(appPlatform) !== -1) { 799 return; 800 } 801 if (supportedAppPlatforms.indexOf(appPlatform) !== -1) { 802 sortedAppPlatforms.push(appPlatform); 803 if (excludePlatform) { 804 excludedAppPlatforms.push(excludePlatform); 805 } 806 } 807 }; 808 // If there is an `AppPlatform` associated with the active `Intent`, give 809 // that first priority 810 if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform) { 811 addAppPlatformIfPossible(objectGraph.activeIntent.appPlatform); 812 } 813 if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || 814 clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { 815 addAppPlatformIfPossible("vision"); 816 } 817 // Next, priority is given to the client 818 switch (clientIdentifier) { 819 case client.watchIdentifier: { 820 addAppPlatformIfPossible("watch"); 821 break; 822 } 823 case client.messagesIdentifier: { 824 addAppPlatformIfPossible("messages"); 825 break; 826 } 827 default: { 828 break; 829 } 830 } 831 // Next the current device type 832 switch (deviceType) { 833 case "phone": { 834 addAppPlatformIfPossible("phone"); 835 break; 836 } 837 case "pad": { 838 addAppPlatformIfPossible("pad"); 839 break; 840 } 841 case "tv": { 842 addAppPlatformIfPossible("tv"); 843 break; 844 } 845 case "watch": { 846 addAppPlatformIfPossible("watch"); 847 break; 848 } 849 case "mac": { 850 addAppPlatformIfPossible("mac"); 851 break; 852 } 853 case "vision": { 854 addAppPlatformIfPossible("vision"); 855 break; 856 } 857 default: { 858 break; 859 } 860 } 861 // For Apple Silicon and visionOS, prefer iPad platform over iPhone 862 if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || 863 clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { 864 addAppPlatformIfPossible("pad"); 865 addAppPlatformIfPossible("phone"); 866 } 867 if (objectGraph.appleSilicon.isSupportEnabled || objectGraph.client.isVision) { 868 addAppPlatformIfPossible("pad"); 869 addAppPlatformIfPossible("phone"); 870 } 871 else { 872 addAppPlatformIfPossible("phone"); 873 addAppPlatformIfPossible("pad"); 874 } 875 addAppPlatformIfPossible("mac"); 876 addAppPlatformIfPossible("vision"); 877 addAppPlatformIfPossible("tv"); 878 addAppPlatformIfPossible("watch"); 879 addAppPlatformIfPossible("messages"); 880 sortedAppPlatforms = sortedAppPlatforms.filter(function (appPlatform) { 881 return excludedAppPlatforms.indexOf(appPlatform) === -1; 882 }); 883 return sortedAppPlatforms; 884 }); 885} 886/** 887 * For a given server data, this will return the Game Center features that the app supports 888 * 889 * @param data Server data for the app 890 * @returns An array of supported Game Center features 891 * */ 892export function supportedGameCenterFeaturesFromData(data) { 893 var _a; 894 if (isNothing(data)) { 895 return undefined; 896 } 897 return ((_a = derivedData.value(data, "supportedGameCenterFeaturesFromData", () => { 898 const features = []; 899 const supportedGameCenterFeatures = serverData.asArrayOrEmpty(data, "attributes.supportedGameCenterFeatures"); 900 if (supportedGameCenterFeatures.includes("achievements")) { 901 features.push("achievements"); 902 } 903 if (supportedGameCenterFeatures.includes("challenges")) { 904 features.push("challenges"); 905 } 906 if (supportedGameCenterFeatures.includes("leaderboards")) { 907 features.push("leaderboards"); 908 } 909 if (supportedGameCenterFeatures.includes("multiplayer-activities")) { 910 features.push("multiplayer-activities"); 911 } 912 return features; 913 })) !== null && _a !== void 0 ? _a : undefined); 914} 915/** 916 * For a given server data, returns whether the game is eligible for the Games App 917 * This will default to true since we generally expect apps we view in the Games app to be games. 918 * It will be unusual that this is evaluated to `false`. 919 * 920 * @param data Server data for the app 921 * @returns A boolean indicating whether game is eligible for display 922 * */ 923export function isEligibleForGamesApp(data) { 924 var _a; 925 if (isNothing(data)) { 926 return true; 927 } 928 return (_a = serverData.asBoolean(data, "attributes.isEligibleForGamesApp")) !== null && _a !== void 0 ? _a : true; 929} 930/** 931 * For a given server data, this will return the platforms that the app supports 932 * 933 * @param data Server data for the app 934 * @returns An array of supported AppPlatforms 935 * */ 936export function supportedAppPlatformsFromData(objectGraph, data) { 937 if (!data) { 938 return null; 939 } 940 return derivedData.value(data, "supportedAppPlatformsFromData", () => { 941 const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data, "ios"); 942 const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data); 943 const isAppleWatchSupported = isAppleWatchSupportedFromData(objectGraph, data); 944 const serverDeviceFamilies = mediaAttributes.attributeAsArrayOrEmpty(data, "deviceFamilies"); 945 const appPlatforms = []; 946 for (const serverDeviceFamily of serverDeviceFamilies) { 947 switch (serverDeviceFamily) { 948 case "iphone": 949 if (!isHiddenFromSpringboard) { 950 appPlatforms.push("phone"); 951 } 952 break; 953 case "ipad": 954 if (!isHiddenFromSpringboard) { 955 appPlatforms.push("pad"); 956 } 957 break; 958 case "tvos": 959 appPlatforms.push("tv"); 960 break; 961 case "watch": 962 appPlatforms.push("watch"); 963 break; 964 case "realityDevice": 965 appPlatforms.push("vision"); 966 break; 967 default: 968 break; 969 } 970 } 971 if (hasMessagesExtension) { 972 appPlatforms.push("messages"); 973 } 974 if (isAppleWatchSupported) { 975 appPlatforms.push("watch"); 976 } 977 if (contentDeviceFamily.dataHasDeviceFamily(objectGraph, data, "mac")) { 978 appPlatforms.push("mac"); 979 } 980 return appPlatforms; 981 }); 982} 983/** 984 * Returns a localized, user-friendly description of all media platforms. This may be a comma delimited 985 * list of the platforms (including supplementary platforms), or it may be 'Only for ___', depending 986 * on the context. 987 * 988 * The localization keys used by this function are defined natively, and are updated using 989 * `tools/platform-media-localizations.py`. If the key doesn't exist, then the script needs to 990 * be updated to add the new combination/order of platforms. 991 * 992 * For failed attempts to localize the string, this function will fallback to a default order that is 993 * guaranteed to exist. 994 * 995 * @param objectGraph The object graph. 996 * @param data Apps resource data. 997 * @param allPlatforms The list of platforms to describe. 998 * @returns The friendly description of all platforms. 999 */ 1000export function descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms) { 1001 if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) { 1002 const platform = allPlatforms[0]; 1003 const platformKey = platform.appPlatform.toUpperCase(); 1004 return objectGraph.loc.string(`ONLY_FOR_${platformKey}_APP`); 1005 } 1006 // Flatten all platform partial keys, including their supplementary platforms 1007 let keys = allPlatforms.reduce((partialResult, platform) => partialResult.concat(platformLocalizationKeys(platform)), []); 1008 try { 1009 // Attempt to localize the constructed key 1010 return objectGraph.loc.tryString(`PLATFORMS_${keys.join("_")}`); 1011 } 1012 catch (error) { 1013 // If the key does not exist, a best attempt fallback string will be provided. 1014 const fallbackOrder = ["PHONE", "PAD", "MAC", "VISION", "TV", "WATCH", "MESSAGES", "FACETIME"]; 1015 keys = fallbackOrder.filter((key) => keys.includes(key)); 1016 return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`); 1017 } 1018} 1019/** 1020 * Determines where to place the all platforms description, which is visible when the product media is collapsed, or there is only one platform. 1021 * This is only used by iOS, visionOS & macOS. For tvOS & watchOS, we always put the media description at the bottom. 1022 * 1023 * @param objectGraph The object graph. 1024 * @param data Apps resource data. 1025 * @param allPlatforms The list of platforms to describe. 1026 * @returns Where to place the all platforms description. 1027 */ 1028export function placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms) { 1029 if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) { 1030 return "top"; 1031 } 1032 else { 1033 return "bottom"; 1034 } 1035} 1036/** 1037 * Determines whether we want to use the 'Only for ___' text to describe `allPlatforms`. 1038 * 1039 * @param objectGraph The object graph. 1040 * @param data Apps resource data. 1041 * @param allPlatforms The list of platforms to describe. 1042 * @returns Whether we want to use 'Only for ___' text to describe `allPlatforms`. 1043 */ 1044function shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms) { 1045 if (allPlatforms.length === 1) { 1046 const platform = allPlatforms[0]; 1047 const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); 1048 const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); 1049 const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); 1050 const isRunnableOnCurrentDevice = supportsPlatform(runnableAppPlatforms, platform.appPlatform); 1051 const noSupplementaryPlatforms = platform.supplementaryAppPlatforms.length === 0; 1052 const isForDifferentDevice = platform.appPlatform !== objectGraph.client.deviceType; 1053 if (noSupplementaryPlatforms && isForDifferentDevice && !isRunnableOnCurrentDevice) { 1054 return true; 1055 } 1056 } 1057 return false; 1058} 1059/** 1060 * Returns a localized description of a media platform, including any supplementary platforms. 1061 * e.g. 'Mac' or 'iMessage, FaceTime'. 1062 * 1063 * @param objectGraph Object graph, used for localizing the string. 1064 * @param allPlatforms The platform to describe. 1065 * @returns The friendly description of the platform. 1066 */ 1067export function descriptionOfMediaPlatform(objectGraph, platform) { 1068 const keys = platformLocalizationKeys(platform); 1069 return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`); 1070} 1071/** 1072 * Returns an array of partial loc keys that represent a media platform. 1073 * This consists the media's app platform + any supplementary platforms. 1074 * e.g. ["MAC"] or ["MESSAGES", "FACETIME"] 1075 * 1076 * @param platform The media platform. 1077 * @returns The list of partial loc key that represent the media platform. 1078 */ 1079function platformLocalizationKeys(platform) { 1080 const appPlatformKey = platform.appPlatform.toUpperCase(); 1081 const supplementaryPlatformKeys = platform.supplementaryAppPlatforms.map((supplementaryPlatform) => supplementaryPlatform.toUpperCase()); 1082 return [appPlatformKey].concat(supplementaryPlatformKeys); 1083} 1084/** 1085 * Determines if a given app has a compatible iOS binary for this client. 1086 * 1087 * @param {mediaDataStructure.Data} data The product data to use. 1088 * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries 1089 * @returns {boolean} True when the app and device are halva. 1090 */ 1091export function supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary) { 1092 let isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible"); 1093 // Override for News in Moltres on Mac 1094 if (preprocessor.GAMES_TARGET && data.id === "1066498020" && objectGraph.client.deviceType === "mac") { 1095 isIOSBinaryMacOSCompatible = true; 1096 } 1097 return doesClientSupportMacOSCompatibleIOSBinary && isIOSBinaryMacOSCompatible; 1098} 1099/** 1100 * Determines if a given app has a compatible iOS binary for the current client. 1101 * 1102 * @param {mediaDataStructure.Data} data The product data to use. 1103 * @returns {boolean} True when the app and device are visionOS. 1104 */ 1105export function supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data) { 1106 return (objectGraph.client.isVision && 1107 mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible")); 1108} 1109/** 1110 * Determines if a given app has a compatible iOS binary for arbitrary clients. 1111 * 1112 * @param {mediaDataStructure.Data} data The product data to use. 1113 * @returns {boolean} True when the app can run on visionOS. 1114 */ 1115export function supportsVisionOSCompatibleIOSBinaryOnAnyClient(data) { 1116 return mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible"); 1117} 1118/** 1119 * Determines app binary traits. 1120 * 1121 * @param {mediaDataStructure.Data} data The product data to use. 1122 * @returns {string[]} The app binary traits. 1123 */ 1124export function appBinaryTraitsFromData(objectGraph, data) { 1125 if (!objectGraph.client.isiOS) { 1126 return undefined; 1127 } 1128 let appBinaryTraits; 1129 if (objectGraph.isAvailable(ads) && 1130 ["debug", "internal"].includes(objectGraph.client.buildType) && 1131 isSome(objectGraph.ads.fetchAppBinaryTraitsOverride)) { 1132 // use client override for debugging internal builds 1133 appBinaryTraits = objectGraph.ads.fetchAppBinaryTraitsOverride(); 1134 } 1135 if (isNothing(appBinaryTraits)) { 1136 // parse from server response 1137 appBinaryTraits = mediaPlatformAttributes.platformAttributeAsArrayOrEmpty(data, "ios", "appBinaryTraits"); 1138 } 1139 return appBinaryTraits; 1140} 1141/** 1142 * Determines whether the product has external browser engine. 1143 * @param objectGraph Current object graph 1144 * @param data The product data 1145 * @returns True if the product has external browser engine 1146 */ 1147export function hasExternalBrowserForData(objectGraph, data) { 1148 var _a; 1149 const appBinaryTraits = appBinaryTraitsFromData(objectGraph, data); 1150 const externalBrowserTraits = new Set(["uses-non-webkit-browser-engine", "is-custom-browser-engine-app"]); 1151 return (_a = appBinaryTraits === null || appBinaryTraits === void 0 ? void 0 : appBinaryTraits.some((trait) => externalBrowserTraits.has(trait))) !== null && _a !== void 0 ? _a : false; 1152} 1153/** 1154 * Determines minimum os version 1155 * 1156 * @param {mediaDataStructure.Data} data The product data to use. 1157 * @param {boolean} isClientHalva Whether the client is halva. 1158 * @returns {string} The minimum OS version. 1159 */ 1160export function minimumOSVersionFromData(objectGraph, data, isClientHalva) { 1161 const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva); 1162 const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); 1163 if (supportsMacOSCompatibleIOSBinary) { 1164 const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumMacOSVersion"); 1165 if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) { 1166 return minimumOSVersion; 1167 } 1168 } 1169 else if (supportsVisionOSCompatibleIOSBinary) { 1170 const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumXROSVersion"); 1171 if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) { 1172 return minimumOSVersion; 1173 } 1174 } 1175 const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); 1176 return mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "minimumOSVersion"); 1177} 1178/** 1179 * Determines required capabilities for device. 1180 * 1181 * @param {mediaDataStructure.Data} data The product data to use. 1182 * @param {boolean} isClientHalva Whether the client is halva. 1183 * @returns {string} The device capabilities to use. 1184 */ 1185export function requiredCapabilitiesFromData(objectGraph, data, isClientHalva) { 1186 const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva); 1187 const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); 1188 if (supportsMacOSCompatibleIOSBinary) { 1189 return contentAttributes.contentAttributeAsString(objectGraph, data, "macRequiredCapabilities"); 1190 } 1191 else if (supportsVisionOSCompatibleIOSBinary) { 1192 return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilitiesForRealityDevice"); 1193 } 1194 else { 1195 return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilities"); 1196 } 1197} 1198/** 1199 * Returns the app platforms you can buy for on the given device. 1200 * 1201 * @param objectGraph The current object graph 1202 * @param data The data for the app in question 1203 * @param device The device type to check 1204 * @param supportsMacOSCompatibleIOSBinary Whether device and app supports macOS compatible iOS binary 1205 * @param supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary 1206 * @returns An array of supported app platforms 1207 */ 1208function buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { 1209 let systemApps; 1210 switch (device) { 1211 case "phone": 1212 systemApps = sad.systemApps(objectGraph); 1213 if (isSome(data) && systemApps.isSystemAppFromData(data)) { 1214 return ["phone", "watch", "messages"]; 1215 } 1216 else { 1217 return ["phone", "watch", "messages", "tv", "vision"]; 1218 } 1219 case "pad": 1220 systemApps = sad.systemApps(objectGraph); 1221 if (isSome(data) && systemApps.isSystemAppFromData(data)) { 1222 return ["phone", "pad", "messages"]; 1223 } 1224 else { 1225 return ["phone", "pad", "messages", "tv", "vision"]; 1226 } 1227 case "tv": 1228 return ["tv"]; 1229 case "watch": 1230 return ["watch"]; 1231 case "mac": 1232 if (supportsMacOSCompatibleIOSBinary) { 1233 return ["mac", "phone", "pad"]; 1234 } 1235 else { 1236 return ["mac"]; 1237 } 1238 case "vision": 1239 if (supportsVisionOSCompatibleIOSBinary) { 1240 return ["vision", "phone", "pad"]; 1241 } 1242 else { 1243 return ["vision"]; 1244 } 1245 default: 1246 return []; 1247 } 1248} 1249/** 1250 * Returns the app platforms you can preorder on for the given device. 1251 * 1252 * @param {DeviceType} device The device type to check 1253 * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary 1254 * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary 1255 * @returns {models.AppPlatform[]} An array of supported app platforms 1256 */ 1257function preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { 1258 switch (device) { 1259 case "phone": 1260 return ["phone", "watch", "messages"]; 1261 case "pad": 1262 return ["phone", "pad", "messages"]; 1263 case "tv": 1264 return ["tv"]; 1265 case "watch": 1266 return ["watch"]; 1267 case "mac": 1268 if (supportsMacOSCompatibleIOSBinary) { 1269 return ["mac", "phone", "pad"]; 1270 } 1271 else { 1272 return ["mac"]; 1273 } 1274 case "vision": 1275 if (supportsVisionOSCompatibleIOSBinary) { 1276 return ["vision", "phone", "pad"]; 1277 } 1278 else { 1279 return ["vision"]; 1280 } 1281 default: 1282 return []; 1283 } 1284} 1285/** 1286 * Returns the app platforms you can run on the given device. 1287 * 1288 * @param {DeviceType} device The device type to check 1289 * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary 1290 * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary 1291 * @returns {models.AppPlatform[]} An array of supported app platforms 1292 */ 1293export function runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { 1294 switch (device) { 1295 case "phone": 1296 return ["phone", "messages"]; 1297 case "pad": 1298 return ["phone", "pad", "messages"]; 1299 case "tv": 1300 return ["tv"]; 1301 case "watch": 1302 return ["watch"]; 1303 case "mac": 1304 if (supportsMacOSCompatibleIOSBinary) { 1305 return ["mac", "phone", "pad"]; 1306 } 1307 else { 1308 return ["mac"]; 1309 } 1310 case "vision": 1311 if (supportsVisionOSCompatibleIOSBinary) { 1312 return ["vision", "phone", "pad"]; 1313 } 1314 else { 1315 return ["vision"]; 1316 } 1317 default: 1318 return []; 1319 } 1320} 1321/** 1322 * Determines if a given piece of content supports the provided app platform 1323 * 1324 * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content 1325 * @param {AppPlatform} platform The platform to check 1326 * @returns {boolean} True if the platform is supported, false if not 1327 */ 1328export function supportsPlatform(appPlatforms, platform) { 1329 return appPlatforms.indexOf(platform) !== -1; 1330} 1331/** 1332 * Determines if a given piece of content is buyable on the provided device. 1333 * 1334 * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content 1335 * @param {DeviceType} device The device type to check 1336 * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary. 1337 * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary 1338 * @param {boolean} isMacOSAppBuyableOnDevice Whether a macOS app is buyable on this device (this enables additional criteria for Apple Silicon). 1339 * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not 1340 */ 1341export function buyableOnDevice(objectGraph, data, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppBuyableOnDevice = true) { 1342 const platforms = buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); 1343 // Do any of the platforms supported by the device match any of the content's app platforms? 1344 if (!platforms.some((platform) => supportsPlatform(appPlatforms, platform))) { 1345 return false; 1346 } 1347 if (objectGraph.client.isMac && platforms.includes("mac")) { 1348 return isMacOSAppBuyableOnDevice; 1349 } 1350 return true; 1351} 1352/** 1353 * Determines macOS runnability info for apps and bundles on macOS. 1354 */ 1355function macOSRunnabilityInfoFromData(objectGraph, data) { 1356 var _a; 1357 const runnabilityInfo = new RunnabilityInfo(); 1358 // Return most permissible runnability for non-macOS platforms. 1359 if (objectGraph.client.deviceType !== "mac") { 1360 return runnabilityInfo; 1361 } 1362 // Use media API attributes for non-bundles. 1363 if (data.type !== "app-bundles") { 1364 runnabilityInfo.runsOnIntel = 1365 (_a = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "runsOnIntel", contentAttributes.defaultAttributePlatform(objectGraph))) !== null && _a !== void 0 ? _a : true; 1366 runnabilityInfo.runsOnAppleSilicon = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "runsOnAppleSilicon", contentAttributes.defaultAttributePlatform(objectGraph)); 1367 runnabilityInfo.requiresRosetta = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresRosetta", contentAttributes.defaultAttributePlatform(objectGraph)); 1368 return runnabilityInfo; 1369 } 1370 const bundleAppsData = mediaRelationship.relationshipCollection(data, "apps"); 1371 // Return most permissible runnability when there no children available 1372 if (bundleAppsData.length === 0) { 1373 return runnabilityInfo; 1374 } 1375 // Synthesize runnability info from bundle apps 1376 for (const appData of bundleAppsData) { 1377 if (serverData.isNull(appData.attributes)) { 1378 continue; 1379 } 1380 const appRunnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, appData); 1381 runnabilityInfo.runsOnIntel = runnabilityInfo.runsOnIntel && appRunnabilityInfo.runsOnIntel; 1382 runnabilityInfo.runsOnAppleSilicon = 1383 runnabilityInfo.runsOnAppleSilicon && appRunnabilityInfo.runsOnAppleSilicon; 1384 runnabilityInfo.requiresRosetta = runnabilityInfo.requiresRosetta || appRunnabilityInfo.requiresRosetta; 1385 } 1386 return runnabilityInfo; 1387} 1388/** 1389 * Determines if a given macOS app is buyable on this device. 1390 * 1391 */ 1392export function isMacOSAppBuyableAndRunnableFromData(objectGraph, data, isAppleSiliconSupportEnabled, isRosettaAvailable) { 1393 const runnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, data); 1394 if (isAppleSiliconSupportEnabled) { 1395 return (runnabilityInfo.runsOnAppleSilicon && 1396 (!runnabilityInfo.requiresRosetta || (runnabilityInfo.requiresRosetta && isRosettaAvailable))); 1397 } 1398 else { 1399 return runnabilityInfo.runsOnIntel; 1400 } 1401} 1402/** 1403 * Determines if a given piece of content is preorderable on the provided device. 1404 * 1405 * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content 1406 * @param {DeviceType} device The device type to check 1407 * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary. 1408 * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary. 1409 * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not 1410 */ 1411export function preorderableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { 1412 const platforms = preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); 1413 // Do any of the platforms supported by the device match any of the content's app platforms? 1414 return platforms.some((platform) => supportsPlatform(appPlatforms, platform)); 1415} 1416/** 1417 * Determines if any of a given array of app platforms can be run on the provided device. 1418 * 1419 * @param appPlatforms The app platforms supported by a piece of content. 1420 * @param device The device type to check. 1421 * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary. 1422 * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary. 1423 * @returns `true` if any of the app platforms can be run on the given device; `false` otherwise. 1424 */ 1425export function runnableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppRunnableOnDevice = true) { 1426 const runnablePlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); 1427 // Do any of the platforms supported by the device match any of the content's app platforms? 1428 if (!runnablePlatforms.some((platform) => supportsPlatform(appPlatforms, platform))) { 1429 return false; 1430 } 1431 if (objectGraph.client.isMac && appPlatforms.includes("mac")) { 1432 return isMacOSAppRunnableOnDevice; 1433 } 1434 return true; 1435} 1436/** 1437 * Determines if a given piece of content is runnable on the provided device. 1438 * 1439 * @param data The product data to use. 1440 * @param device 1441 * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries 1442 * @returns {boolean} True if the product can be run on the provided device, false if not 1443 */ 1444export function runnableOnDeviceWithData(objectGraph, data, device, doesClientSupportMacOSCompatibleIOSBinary) { 1445 // (1) Required capabilities mismatch 1446 if (!lockups.deviceHasCapabilitiesFromData(objectGraph, data)) { 1447 return false; 1448 } 1449 // (2) 32-bit only, unsupported deletable system app, doesn't meet minimum OS requirements, or doesn't support current platform 1450 // Note that Filter.UnsupportedPlatform only checks if the product is buyable, not runnable 1451 const filter = 2 /* filtering.Filter.ThirtyTwoBit */ | 1452 4 /* filtering.Filter.UnsupportedSystemDeletableApps */ | 1453 512 /* filtering.Filter.MinimumOSRequirement */ | 1454 128 /* filtering.Filter.UnsupportedPlatform */ | 1455 8192 /* filtering.Filter.MacOSRosetta */; 1456 if (filtering.shouldFilter(objectGraph, data, filter)) { 1457 return false; 1458 } 1459 // (3) Finally, check if any of the product platforms are supported on this device 1460 const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary); 1461 const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); 1462 const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); 1463 const productAppPlatforms = supportedAppPlatformsFromData(objectGraph, data); 1464 return runnableAppPlatforms.some((platform) => supportsPlatform(productAppPlatforms, platform)); 1465} 1466/** 1467 * Determines which screenshot keys (MediaType) we need to use to pull the appropriate screenshots 1468 * from the server data. 1469 * 1470 * @param appPlatform The app platform requested 1471 * @param screenSize The size of the screen being used to display the screenshots 1472 * @returns An array of ScreenshotType strings that can be used on the server data 1473 * */ 1474export function mediaTypesForAppPlatform(objectGraph, appPlatform, screenSize) { 1475 switch (appPlatform) { 1476 case "mac": { 1477 return ["mac"]; 1478 } 1479 case "watch": { 1480 if (screenSize.isEqualTo(screenSizeWatchUltra) || screenSize.isEqualTo(screenSizeN230)) { 1481 // 2022 is the preferred dropwell for Ultra devices 1482 return ["appleWatch_2022", "appleWatch_2024", "appleWatch_2021", "appleWatch_2018", "appleWatch"]; 1483 } 1484 else { 1485 return ["appleWatch_2024", "appleWatch_2022", "appleWatch_2021", "appleWatch_2018", "appleWatch"]; 1486 } 1487 } 1488 case "tv": { 1489 return ["appleTV"]; 1490 } 1491 case "vision": { 1492 return ["appleVisionPro"]; 1493 } 1494 case "pad": { 1495 const types = []; 1496 if ((screenSize.isEqualTo(screenSizeIPadPro2018) || 1497 screenSize.isEqualTo(screenSizeIPadPro2018Landscape) || 1498 screenSize.isEqualTo(screenSizeJ720) || 1499 screenSize.isEqualTo(screenSizeJ720Landscape)) && 1500 objectGraph.client.screenCornerRadius > 0.0) { 1501 types.push("ipadPro_2018"); 1502 types.push("ipad_11"); 1503 types.push("ipadPro"); 1504 types.push("ipad_10_5"); 1505 types.push("ipad"); 1506 } 1507 else if (screenSize.isEqualTo(screenSizeIPadPro)) { 1508 types.push("ipadPro"); 1509 types.push("ipadPro_2018"); 1510 types.push("ipad_11"); 1511 types.push("ipad_10_5"); 1512 types.push("ipad"); 1513 } 1514 else if (screenSize.isEqualTo(screenSizeIPad11) || 1515 screenSize.isEqualTo(screenSizeIPad11Landscape) || 1516 screenSize.isEqualTo(screenSizeIPadJ310) || 1517 screenSize.isEqualTo(screenSizeIPadJ310Landscape) || 1518 screenSize.isEqualTo(screenSizeJ717) || 1519 screenSize.isEqualTo(screenSizeJ717Landscape)) { 1520 types.push("ipad_11"); 1521 types.push("ipadPro_2018"); 1522 types.push("ipadPro"); 1523 types.push("ipad_10_5"); 1524 types.push("ipad"); 1525 } 1526 else if (screenSize.isEqualTo(screenSizeIPad105)) { 1527 types.push("ipad_10_5"); 1528 types.push("ipad"); 1529 types.push("ipad_11"); 1530 types.push("ipadPro"); 1531 types.push("ipadPro_2018"); 1532 } 1533 else if (screenSize.isEqualTo(screenSizeIPadAir2020)) { 1534 types.push("ipad_11"); 1535 types.push("ipadPro"); 1536 types.push("ipadPro_2018"); 1537 types.push("ipad_10_5"); 1538 types.push("ipad"); 1539 } 1540 else if (screenSize.isEqualTo(screenSizeIPad102)) { 1541 types.push("ipad"); 1542 types.push("ipad_10_5"); 1543 types.push("ipad_11"); 1544 types.push("ipadPro"); 1545 types.push("ipadPro_2018"); 1546 } 1547 else { 1548 // Regardless of screen size match, we should add on 'some' iPad. 1549 types.push("ipadPro_2018"); 1550 types.push("ipad_11"); 1551 types.push("ipad"); 1552 types.push("ipad_10_5"); 1553 types.push("ipadPro"); 1554 } 1555 return types; 1556 } 1557 case "phone": { 1558 /** Phone Best Match Policy ** 1559 1560 The best match is given by |B| + |L| + |S|, where: 1561 B: Exact type match 1562 L: All types larger than the exact type, in increasing order 1563 S: All types smaller than the exact type, in decreasing order 1564 1565 Example: 1566 Types for iphone6 == [iphone6, iphone6+, iphone_5_8, iphone5, iphone] 1567 Types for iphone5 == [iphone5, iphone6, iphone6+, iphone_5_8, iphone] 1568 1569 ** */ 1570 // Grab the exact match. 1571 let perfectMatch; 1572 if (screenSize.isEqualTo(screenSizeIphone65) || screenSize.isEqualTo(screenSizeIPhone134)) { 1573 perfectMatch = "iphone_6_5"; 1574 } 1575 else if (screenSize.isEqualTo(screenSizeIPhone58) || 1576 screenSize.isEqualTo(screenSizeIPhone131) || 1577 screenSize.isEqualTo(screenSizeIPhone132)) { 1578 perfectMatch = "iphone_5_8"; 1579 } 1580 else if (screenSize.isEqualTo(screenSizeIPhoneOriginal)) { 1581 perfectMatch = "iphone"; 1582 } 1583 else if (screenSize.isEqualTo(screenSizeIPhone5)) { 1584 perfectMatch = "iphone5"; 1585 } 1586 else if (screenSize.isEqualTo(screenSizeIPhone6)) { 1587 perfectMatch = "iphone6"; 1588 } 1589 else if (screenSize.isEqualTo(screenSizeIPhone6Plus)) { 1590 perfectMatch = "iphone6+"; 1591 } 1592 else if (screenSize.isEqualTo(screenSizeIPhone61) || screenSize.isEqualTo(screenSizeD93)) { 1593 perfectMatch = "iphone_d73"; 1594 } 1595 else if (screenSize.isEqualTo(screenSizeIPhone67) || 1596 screenSize.isEqualTo(screenSizeD94) || 1597 screenSize.isEqualTo(screenSizeD23)) { 1598 perfectMatch = "iphone_d74"; 1599 } 1600 else { 1601 perfectMatch = "iphone_5_8"; 1602 } 1603 // Append remaining types to our exact match. 1604 const perfectMatchIndex = decreasingPhoneTypes.indexOf(perfectMatch); 1605 const largerTypes = decreasingPhoneTypes.slice(0, perfectMatchIndex); 1606 largerTypes.reverse(); 1607 const smallerTypes = decreasingPhoneTypes.slice(perfectMatchIndex + 1); 1608 const perfectMatchArray = [perfectMatch]; 1609 return perfectMatchArray.concat(largerTypes, smallerTypes); 1610 } 1611 default: { 1612 return []; 1613 } 1614 } 1615} 1616export function combinedFileSizeFromData(objectGraph, data) { 1617 var _a; 1618 if (serverData.isNull(data)) { 1619 return null; 1620 } 1621 // This background asset information is for the work done in SydneyB 1622 const backgroundAssetsInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfo"); 1623 // This background asset information is for the work done in SydneyE 1624 const backgroundAssetsInfoWithOptional = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfoWithOptional"); 1625 const isIOSBinaryCompatibleWithMac = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, true); 1626 const isMacOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac"); 1627 const isWebViewingMac = objectGraph.client.isWeb && ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) === "mac"; 1628 if ((objectGraph.client.isMac || isWebViewingMac || isMacOnly) && !isIOSBinaryCompatibleWithMac) { 1629 const macFileSize = objectGraph.bag.enableProductPageInstallSize 1630 ? macInstallSizeInBytesFromData(objectGraph, data) 1631 : offers.macFileSizeInBytesFromData(objectGraph, data); 1632 if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) { 1633 const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes"); 1634 return new modelsBase.CombinedFileSize(macFileSize, null, null, maxEssentialInstallSizeInBytes); 1635 } 1636 else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) { 1637 const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes"); 1638 const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes"); 1639 return new modelsBase.CombinedFileSize(macFileSize, maxDownloadSizeInBytes, maxInstallSizeInBytes, null); 1640 } 1641 return new modelsBase.CombinedFileSize(macFileSize, null, null, null); 1642 } 1643 else { 1644 /* File Size: Our policy is to rely on thinned variant, device model, and universal (in that order). */ 1645 const fileSizeByDevice = mediaAttributes.attributeAsDictionary(data, "fileSizeByDevice"); 1646 if (fileSizeByDevice) { 1647 /* thinnedApplicationVariantIdentifier can contain two device names. The preferred device, and a compatible device. */ 1648 let fileSizeKeys = []; 1649 if (objectGraph.client.thinnedApplicationVariantIdentifier) { 1650 fileSizeKeys = objectGraph.client.thinnedApplicationVariantIdentifier.split(" "); 1651 } 1652 fileSizeKeys = fileSizeKeys.concat([objectGraph.host.deviceModel, "universal"]); 1653 for (const key of fileSizeKeys) { 1654 const fileSizeValue = serverData.asNumber(fileSizeByDevice[key]); 1655 if (fileSizeValue) { 1656 if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) { 1657 const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes"); 1658 return new modelsBase.CombinedFileSize(fileSizeValue, null, null, maxEssentialInstallSizeInBytes); 1659 } 1660 else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) { 1661 const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes"); 1662 const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes"); 1663 return new modelsBase.CombinedFileSize(fileSizeValue, maxDownloadSizeInBytes, maxInstallSizeInBytes, null); 1664 } 1665 else { 1666 return new modelsBase.CombinedFileSize(fileSizeValue, null, null, null); 1667 } 1668 } 1669 } 1670 } 1671 } 1672 return null; 1673} 1674/** 1675 * Extract the file size and unit from a CombinedFileSize object. 1676 * @param objectGraph Current object graph 1677 * @param combinedFileSize The combined file size object 1678 * @returns A FileSizeAndUnit object 1679 */ 1680export function fileSizeAndUnitFromCombinedFileSize(objectGraph, combinedFileSize) { 1681 let totalFileSize; 1682 if (isSome(combinedFileSize.maxEssentialInstallSizeInBytes)) { 1683 totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxEssentialInstallSizeInBytes; 1684 } 1685 else if (isSome(combinedFileSize.maxInstallSizeInBytes)) { 1686 totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxInstallSizeInBytes; 1687 } 1688 else { 1689 totalFileSize = combinedFileSize.fileSizeByDevice; 1690 } 1691 if (totalFileSize <= 0) { 1692 return null; 1693 } 1694 // We split using all whitespace characters because in some locs a non-breaking space is used. 1695 const parts = objectGraph.loc.fileSize(totalFileSize).trim().split(/\s+/); 1696 if (parts.length !== 2) { 1697 return null; 1698 } 1699 return { 1700 size: parts[0], 1701 unit: parts[1], 1702 }; 1703} 1704/** 1705 * Extracts the install size for a macOS app. 1706 * @param objectGraph Current object graph 1707 * @param data Product page data 1708 * @returns The install size for the Mac binary, in bytes 1709 */ 1710function macInstallSizeInBytesFromData(objectGraph, data) { 1711 const deviceData = mediaPlatformAttributes.platformAttributeAsDictionary(data, "osx", "installSizeByDeviceInBytes"); 1712 if (isNothing(deviceData)) { 1713 return null; 1714 } 1715 // macOS does not support app thinning, so there is only ever one macOS device in this list. Unfortunately 1716 // there is no known API that gives us this device name, so we resort to hard-coding for now. 1717 const installSizeInBytes = deviceData["Mac"]; 1718 if (isNothing(installSizeInBytes)) { 1719 return null; 1720 } 1721 return serverData.asNumber(installSizeInBytes); 1722} 1723/** 1724 * Determines the primary langauge locale, from a given list of locales. 1725 * @param objectGraph Current object graph 1726 * @param locales The list of locales 1727 * @returns A single LanguageLocale object, or null 1728 */ 1729export function primaryLanguageLocaleFromLocales(objectGraph, locales) { 1730 const languageCount = locales.length; 1731 if (languageCount <= 0) { 1732 return null; 1733 } 1734 return { 1735 tag: serverData.asString(serverData.traverse(locales, "0.tag")).split("-")[0].toUpperCase(), 1736 name: serverData.asString(serverData.traverse(locales, "0.name")), 1737 }; 1738} 1739/** 1740 * Determines the uber artwork for the product, if there is any. 1741 * @param {Data} The data for the product. 1742 * @returns {models.Artwork} The artwork for the uber, or `null` if there is none. 1743 * null. 1744 */ 1745export function productUberFromData(objectGraph, data, options) { 1746 let uberArtworkData; 1747 let uberArtworkPath = null; 1748 let fallbackUberArtworkPath = null; 1749 let cropCode = null; 1750 let fallbackCropCode = null; 1751 switch (objectGraph.client.deviceType) { 1752 case "mac": 1753 if (options.supportsArcade) { 1754 uberArtworkPath = "editorialArtwork.splashFullScreen"; 1755 cropCode = "sr"; 1756 } 1757 else { 1758 uberArtworkPath = "editorialArtwork.centeredFullscreenBackground"; 1759 cropCode = "ep"; 1760 } 1761 break; 1762 case "tv": 1763 if (options.presentedInTopShelf) { 1764 uberArtworkPath = "editorialArtwork.topShelf"; 1765 cropCode = "sr"; 1766 } 1767 else { 1768 uberArtworkPath = "editorialArtwork.splashFullScreen"; 1769 cropCode = "ta"; 1770 fallbackUberArtworkPath = "editorialArtwork.fullscreenBackground"; 1771 fallbackCropCode = "sr"; 1772 } 1773 break; 1774 case "vision": 1775 uberArtworkPath = "editorialArtwork.productUberStatic16x9"; 1776 cropCode = "sr"; 1777 break; 1778 default: 1779 if (options.supportsArcade) { 1780 if (options.prefersCompactVariant || objectGraph.client.isPhone) { 1781 uberArtworkPath = "editorialArtwork.splashTall"; 1782 cropCode = "oc"; 1783 } 1784 else { 1785 uberArtworkPath = "editorialArtwork.splashFullScreen"; 1786 cropCode = "oh"; 1787 } 1788 } 1789 else { 1790 uberArtworkPath = "editorialArtwork.bannerUber"; 1791 cropCode = "sr"; 1792 } 1793 break; 1794 } 1795 uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, uberArtworkPath); 1796 // If we don't have the desired artwork, we sometimes attempt to use other artwork as a fallback. 1797 if (fallbackUberArtworkPath !== null && serverData.isNullOrEmpty(uberArtworkData)) { 1798 uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, fallbackUberArtworkPath); 1799 // Use the fallback crop if it's available. 1800 if (fallbackCropCode !== null) { 1801 cropCode = fallbackCropCode; 1802 } 1803 } 1804 if (serverData.isDefinedNonNull(uberArtworkData) && serverData.isDefinedNonNull(cropCode)) { 1805 return artworkFromApiArtwork(objectGraph, uberArtworkData, { 1806 cropCode, 1807 useCase: 21 /* ArtworkUseCase.Uber */, 1808 withJoeColorPlaceholder: true, 1809 overrideHeight: null, 1810 overrideWidth: null, 1811 }); 1812 } 1813 return null; 1814} 1815/** 1816 * Determines the logo artwork for the product, if there is any. 1817 * @param {Data} The data for the product. 1818 * @returns {models.Artwork} The artwork for the uber, or `null` if there is none. 1819 * null. 1820 */ 1821export function productLogoArtworkFromData(objectGraph, data) { 1822 let artworkPath = null; 1823 let cropCode = null; 1824 switch (objectGraph.client.deviceType) { 1825 case "tv": 1826 artworkPath = "editorialArtwork.contentLogoTrimmed"; 1827 cropCode = "bb"; 1828 break; 1829 default: 1830 return null; 1831 } 1832 const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath); 1833 if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) { 1834 return artworkFromApiArtwork(objectGraph, artworkData, { 1835 cropCode, 1836 useCase: 0 /* ArtworkUseCase.Default */, 1837 withJoeColorPlaceholder: true, 1838 }); 1839 } 1840 return null; 1841} 1842/** 1843 * Determines the editorial video for the product, if there is any. 1844 * @returns {models.Video} The editorial video for the product, or `null` if there is none. 1845 * null. 1846 * @param data 1847 * @param useCase 1848 * @param preferredFlavorsOverride 1849 */ 1850export function productEditorialVideoFromData(objectGraph, data, useCase, preferredFlavorsOverride, videoPreviewOverride) { 1851 let preferredFlavors = []; 1852 if (serverData.isDefinedNonNullNonEmpty(preferredFlavorsOverride)) { 1853 preferredFlavors = preferredFlavorsOverride; 1854 } 1855 else { 1856 switch (objectGraph.client.deviceType) { 1857 case "mac": 1858 case "tv": 1859 preferredFlavors = ["splashVideo16x9"]; 1860 break; 1861 case "pad": 1862 preferredFlavors = ["splashVideo4x3"]; 1863 break; 1864 case "vision": 1865 preferredFlavors = ["productUberMotion16x9"]; 1866 break; 1867 default: 1868 preferredFlavors = ["splashVideo3x4"]; 1869 } 1870 } 1871 let uberEditorialVideoData = null; 1872 let videoPreviewData = null; 1873 for (const videoFlavor of preferredFlavors) { 1874 uberEditorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ 1875 "editorialVideo", 1876 videoFlavor, 1877 ]); 1878 videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ 1879 "editorialVideo", 1880 videoFlavor, 1881 "previewFrame", 1882 ]); 1883 if (serverData.isDefinedNonNullNonEmpty(uberEditorialVideoData)) { 1884 break; 1885 } 1886 } 1887 // Video Preview based on data, or externally provided override if any. 1888 const videoPreview = videoPreviewOverride !== null && videoPreviewOverride !== void 0 ? videoPreviewOverride : artworkFromApiArtwork(objectGraph, videoPreviewData, { 1889 useCase: useCase, 1890 withJoeColorPlaceholder: true, 1891 cropCode: "sr", 1892 }); 1893 if (serverData.isDefinedNonNull(uberEditorialVideoData)) { 1894 const videoUrl = serverData.asString(uberEditorialVideoData, "video"); 1895 if (serverData.isNull(videoUrl)) { 1896 return null; 1897 } 1898 let playbackControls; 1899 let autoplayPlaybackControls; 1900 if (objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV) { 1901 playbackControls = videoDefaults.standardControls(objectGraph); 1902 autoplayPlaybackControls = { 1903 muteUnmute: true, 1904 }; 1905 } 1906 else { 1907 playbackControls = {}; 1908 autoplayPlaybackControls = {}; 1909 } 1910 const configuration = { 1911 allowsAutoPlay: true, 1912 looping: true, 1913 canPlayFullScreen: false, 1914 playbackControls: playbackControls, 1915 autoPlayPlaybackControls: autoplayPlaybackControls, 1916 }; 1917 return new models.Video(videoUrl, videoPreview, configuration); 1918 } 1919 return null; 1920} 1921/** 1922 * Determines the video for the poster lockup, if there is any. 1923 * @param {Data} The data for the lockup. 1924 * @param {useCase} The use case for this artwork. 1925 * @returns {models.Video} The video for the poster lockup, or `null` if there is none. 1926 * null. 1927 */ 1928export function posterEditorialVideoFromData(objectGraph, data, useCase) { 1929 const editorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ 1930 "editorialVideo", 1931 "posterCardVideo16x9", 1932 ]); 1933 const videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ 1934 "editorialVideo", 1935 "posterCardVideo16x9", 1936 "previewFrame", 1937 ]); 1938 const cropCode = "sr"; 1939 const videoPreview = artworkFromApiArtwork(objectGraph, videoPreviewData, { 1940 useCase: useCase, 1941 withJoeColorPlaceholder: true, 1942 cropCode: cropCode, 1943 }); 1944 if (serverData.isDefinedNonNull(editorialVideoData)) { 1945 const videoUrl = serverData.asString(editorialVideoData, "video"); 1946 if (serverData.isNull(videoUrl)) { 1947 return null; 1948 } 1949 const configuration = { 1950 allowsAutoPlay: true, 1951 looping: true, 1952 canPlayFullScreen: false, 1953 playbackControls: videoDefaults.noControls(objectGraph), 1954 autoPlayPlaybackControls: videoDefaults.noControls(objectGraph), 1955 }; 1956 return new models.Video(videoUrl, videoPreview, configuration); 1957 } 1958 return null; 1959} 1960/** 1961 * Determines the artwork for the poster lockup, if there is any. 1962 * @param {Data} The data for the lockup. 1963 * @returns {models.Artwork} The artwork for the poster lockup, or `null` if there is none. 1964 * null. 1965 */ 1966export function posterArtworkFromData(objectGraph, data) { 1967 const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.postCard"); 1968 const cropCode = "sr"; 1969 if (serverData.isDefinedNonNull(artworkData)) { 1970 return artworkFromApiArtwork(objectGraph, artworkData, { 1971 cropCode, 1972 useCase: 0 /* ArtworkUseCase.Default */, 1973 withJoeColorPlaceholder: true, 1974 }); 1975 } 1976 return null; 1977} 1978/** 1979 * Determines the artwork for the epic heading on a poster lockup, if there is any. 1980 * @param {Data} The data for the product. 1981 * @returns {models.Artwork} The artwork for the epic heading, or `null` if there is none. 1982 * null. 1983 */ 1984export function posterEpicHeadingArtworkFromData(objectGraph, data) { 1985 const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.epicHeading"); 1986 const cropCode = "bb"; 1987 if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) { 1988 const epicHeadingArtwork = artworkFromApiArtwork(objectGraph, artworkData, { 1989 cropCode, 1990 useCase: 0 /* ArtworkUseCase.Default */, 1991 }); 1992 if (objectGraph.client.isVision) { 1993 epicHeadingArtwork.backgroundColor = color.named("clear"); 1994 } 1995 return epicHeadingArtwork; 1996 } 1997 return null; 1998} 1999/** 2000 * Fetch the most-landscape media from data. Hoisted from Arcade See All. 2001 * Used for: 2002 * - Arcade See All Media Lockups 2003 * - Continue Playing Lockups 2004 */ 2005export function editorialSplashVideoFromData(objectGraph, data, videoPreviewOverride) { 2006 let preferredEditorialVideoFlavors = null; 2007 switch (objectGraph.client.deviceType) { 2008 case "mac": 2009 case "tv": 2010 case "phone": 2011 case "vision": 2012 preferredEditorialVideoFlavors = ["splashVideo16x9", "splashVideo4x3", "splashVideo3x4"]; 2013 break; 2014 default: 2015 preferredEditorialVideoFlavors = ["splashVideo4x3", "splashVideo16x9", "splashVideo3x4"]; 2016 } 2017 return productEditorialVideoFromData(objectGraph, data, 21 /* ArtworkUseCase.Uber */, preferredEditorialVideoFlavors, videoPreviewOverride); 2018} 2019/** 2020 * Determines the URL to use for the developer page. 2021 * @param {Data} developerData The data for the "developer" relationship. 2022 * @returns {string} The string form of the URL for the developer page, or `null` if the developer data is undefined or 2023 * null. 2024 */ 2025export function developerUrlFromDeveloperData(objectGraph, developerData) { 2026 if (!serverData.isDefinedNonNull(developerData)) { 2027 return null; 2028 } 2029 if (objectGraph.client.isWeb) { 2030 return mediaAttributes.attributeAsString(developerData, "url"); 2031 } 2032 return `${Protocol.internal}:/${Path.developer}/${Path.href}?${Parameters.href}=${developerData.href}`; 2033} 2034/** 2035 * Determines the URL to use for the Charts page. 2036 * @param {Data} data The data for the product. 2037 * @returns {string} The string form of the URL for the charts page, or `null` if the data is undefined or 2038 * null. 2039 */ 2040export function chartUrlFromData(objectGraph, genre, chart) { 2041 const request = new mediaDataFetching.Request(objectGraph) 2042 .forType("charts") 2043 .addingQuery("types", "apps") 2044 .addingQuery("chart", chart) 2045 .addingQuery("genre", genre) 2046 .includingMacOSCompatibleIOSAppsWhenSupported(true); 2047 return mediaUrlBuilder.buildURLFromRequest(objectGraph, request).toString(); 2048} 2049/** 2050 * Returns the key into the chart-position badge data for the given client name. 2051 * @param clientIdentifier Identifier of the current client. 2052 * @returns {string} The relevant key in the chart-position badge JSON data. 2053 */ 2054export function badgeChartKeyForClientIdentifier(objectGraph, clientIdentifier) { 2055 switch (clientIdentifier) { 2056 case client.appStoreIdentifier: 2057 case client.productPageExtensionIdentifier: 2058 return "appStore"; 2059 case client.watchIdentifier: 2060 return "watch"; 2061 case client.messagesIdentifier: 2062 return "messages"; 2063 case client.tvIdentifier: 2064 return "appletv"; 2065 default: 2066 return null; 2067 } 2068} 2069/** 2070 * Internal function returning the name and asset name representing the 2071 * storefront content rating for the provided rank. 2072 * @param objectGraph The App Store object graph. 2073 * @param rank A content rating rank from CX. 2074 * @returns A tuple containing the name and asset name representing the rank, 2075 * or `undefined` if rank is unknown/invalid. 2076 */ 2077function storefrontContentRatingInfoForRank(objectGraph, rank) { 2078 switch (rank) { 2079 // Brazil Self-Rated 2080 case 6: 2081 return ["L", "br.l"]; 2082 case 7: 2083 return ["10", "br.10"]; 2084 case 8: 2085 return ["12", "br.12"]; 2086 case 9: 2087 return ["14", "br.14"]; 2088 case 10: 2089 return ["16", "br.16"]; 2090 case 11: 2091 return ["18", "br.18"]; 2092 // Brazil Official 2093 case 12: 2094 return ["AL", "br.l.official"]; 2095 case 13: 2096 return ["A10", "br.10.official"]; 2097 case 14: 2098 return ["A12", "br.12.official"]; 2099 case 15: 2100 return ["A14", "br.14.official"]; 2101 case 16: 2102 return ["A16", "br.16.official"]; 2103 case 17: 2104 return ["A18", "br.18.official"]; 2105 // Korea 2106 case 20: 2107 return ["All", "kr.all"]; 2108 case 21: 2109 return ["12", "kr.12"]; 2110 case 22: 2111 return ["15", "kr.15"]; 2112 // Australia 2113 case 31: 2114 return ["15+", "AgeRating-AU-15"]; 2115 case 32: 2116 return ["R 18+", "AgeRating-AU-18"]; 2117 // France 2118 case 47: 2119 return ["18+", "AgeRating-FR-18"]; 2120 default: 2121 return undefined; 2122 } 2123} 2124/// Returns a localized title for the given app platform. 2125export function appPlatformTitle(objectGraph, appPlatform) { 2126 switch (appPlatform) { 2127 case "phone": 2128 return objectGraph.loc.string("AppPlatform.Phone"); 2129 case "pad": 2130 return objectGraph.loc.string("AppPlatform.Pad"); 2131 case "vision": 2132 return objectGraph.loc.string("AppPlatform.Vision"); 2133 case "tv": 2134 return objectGraph.loc.string("AppPlatform.TV"); 2135 case "watch": 2136 return objectGraph.loc.string("AppPlatform.Watch"); 2137 case "messages": 2138 return objectGraph.loc.string("AppPlatform.Messages"); 2139 case "mac": 2140 return objectGraph.loc.string("AppPlatform.Mac"); 2141 default: 2142 return ""; 2143 } 2144} 2145/** 2146 * Provides the name of the asset representing the storefront content rating 2147 * for the provided `rank`. 2148 * @param objectGraph The App Store object graph. 2149 * @param rank A content rating rank from CX. 2150 * @returns The asset name representing the `rank`, corresponding to a file on 2151 * device, or `undefined` if rank is unknown/invalid. 2152 */ 2153export function storefrontContentRatingResourceForRank(objectGraph, rank) { 2154 var _a; 2155 return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[1]; 2156} 2157/** 2158 * Provides a textual representation of the storefront content rating for the 2159 * provided `rank`, e.g. "18+". This should match the main text displayed in 2160 * the content rating pictogram from `storefrontContentRatingResourceForRank`. 2161 * @param objectGraph The App Store object graph. 2162 * @param rank A content rating rank from CX. 2163 * @returns The textual version of the storefront content rating representing 2164 * the `rank`, or `undefined` if rank is unknown/invalid. 2165 */ 2166export function storefrontContentRatingNameForRank(objectGraph, rank) { 2167 var _a; 2168 return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[0]; 2169} 2170export function promotionalTextFromData(objectGraph, data, productVariantData) { 2171 return contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "promotionalText"); 2172} 2173export function hasMessagesExtensionFromData(objectGraph, data, attributePlatform) { 2174 return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasMessagesExtension", attributePlatform); 2175} 2176export function supportsFunCameraFromData(objectGraph, data, attributePlatform) { 2177 return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsFunCamera", attributePlatform); 2178} 2179export function isHiddenFromSpringboardFromData(objectGraph, data) { 2180 return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isHiddenFromSpringboard"); 2181} 2182function isAppleWatchSupportedFromData(objectGraph, data) { 2183 return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isAppleWatchSupported"); 2184} 2185function messagesScreenshotsFromData(objectGraph, data, attributePlatform) { 2186 return contentAttributes.contentAttributeAsDictionary(objectGraph, data, "messagesScreenshots", attributePlatform); 2187} 2188function screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) { 2189 const attributeKey = isAd ? "customScreenshotsByTypeForAd" : "screenshotsByType"; 2190 return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform); 2191} 2192function videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) { 2193 const attributeKey = isAd ? "customVideoPreviewsByTypeForAd" : "videoPreviewsByType"; 2194 return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform); 2195} 2196/** 2197 * Whether Arcade is supported, based on the provided data. 2198 * @param objectGraph The App Store object graph. 2199 * @param data The data blob to check for Arcade support. 2200 * @param attributePlatformOverride An override platform, from which to fetch the attribute. 2201 * @returns A boolean indicating if Arcade is supported. 2202 */ 2203export function isArcadeSupported(objectGraph, data, attributePlatformOverride = undefined) { 2204 return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade", attributePlatformOverride); 2205} 2206/** 2207 * Try to get notes for some piece of content, giving preference to the enrichedEditorialnotes, falling back to editorialNotes, 2208 * then finally to itunesNotes. For some data the notes are stored in the attributes not the platformAttributes. 2209 * @param {Data} data 2210 * @param {string} key 2211 * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations 2212 * * @param attributePlatformOverride An override platform, from which to fetch the attribute. 2213 * @returns {string} 2214 */ 2215export function notesFromData(objectGraph, data, key, enableEditorialCardOverrides = false, attributePlatformOverride = undefined) { 2216 var _a, _b; 2217 if (isNothing(data)) { 2218 return null; 2219 } 2220 let note; 2221 if (enableEditorialCardOverrides) { 2222 const editorialCard = editorialCardFromData(data); 2223 if (mediaAttributes.hasAttributes(editorialCard)) { 2224 note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key], attributePlatformOverride); 2225 } 2226 } 2227 note = 2228 (_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); 2229 return note; 2230} 2231/** 2232 * Try and get notes for some piece of content editorialNotes 2233 * @param {Data} data 2234 * @param {string} key 2235 * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations 2236 * @returns {string} 2237 */ 2238export function editorialNotesFromData(objectGraph, data, key, enableEditorialCardOverrides = false) { 2239 var _a; 2240 let note; 2241 if (enableEditorialCardOverrides) { 2242 const editorialCard = editorialCardFromData(data); 2243 if (mediaAttributes.hasAttributes(editorialCard)) { 2244 note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key]); 2245 } 2246 } 2247 note = 2248 (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key])) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key]); 2249 return note; 2250} 2251/** 2252 * Determines whether the provided data is for a macOS installer. 2253 * @param data The data against which to check for a macOS installer. 2254 */ 2255export function isMacOSInstaller(objectGraph, data) { 2256 return derivedData.value(data, "isMacOSInstaller", () => { 2257 const isMac = objectGraph.client.isMac; 2258 if (!isMac) { 2259 return false; 2260 } 2261 const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); 2262 if (!serverData.isDefinedNonNull(bundleId)) { 2263 return false; 2264 } 2265 return bundleId.startsWith("com.apple.InstallAssistant"); 2266 }); 2267} 2268/** 2269 * Check whether an app is unsupported by the current companion configuration. 2270 * @param data The data representing an app listing. 2271 */ 2272export function isUnsupportedByCurrentCompanion(objectGraph, data) { 2273 const deletableApps = sad.systemApps(objectGraph); 2274 if (objectGraph.host.isWatch) { 2275 // AppConduit will handle determining if SAD apps are supported 2276 if (deletableApps.isUnsupportedDeletableSystemAppFromData(data)) { 2277 return true; 2278 } 2279 else if (objectGraph.client.isTinkerWatch) { 2280 return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") && !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); 2281 } 2282 return false; 2283 } 2284 else { 2285 if (isUnsupportedDeletableSystemAppFromData(objectGraph, data, objectGraph.client.isTinkerWatch)) { 2286 return true; 2287 } 2288 else if (objectGraph.client.isTinkerWatch) { 2289 if (deletableApps.isSystemAppFromData(data)) { 2290 // We don't consider whether an app is marked as standalone with companion 2291 // when running in standalone mode. We always want SAD apps to be installable. 2292 return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isDeliveredInIOSAppForWatchOS") && 2293 !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); 2294 } 2295 else { 2296 return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") && 2297 !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); 2298 } 2299 } 2300 else { 2301 // We only allow standalone system apps to be installed when the watch 2302 // is not running in standalone mode. This simplifies things for other teams. 2303 return (objectGraph.client.isWatch && 2304 deletableApps.isSystemAppFromData(data) && 2305 !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); 2306 } 2307 } 2308} 2309/** 2310 * @deprecated Use sad.isUnsupportedDeletableSystemAppFromData instead. 2311 * Check whether a SAD app is unsupported by the current companion configuration. 2312 * 2313 * @param data The data representing an app listing. 2314 * @param isTinkerWatch Whether the current device is a tinker watch 2315 */ 2316export function isUnsupportedDeletableSystemAppFromData(objectGraph, data, isTinkerWatch) { 2317 if (isTinkerWatch && sad.systemApps(objectGraph).isSystemAppFromData(data)) { 2318 const watchBundleId = mediaAttributes.attributeAsString(data, "watchBundleId"); 2319 if (serverData.isDefinedNonNullNonEmpty(watchBundleId)) { 2320 switch (watchBundleId) { 2321 // rdar://63111354 (On Tinker device, able to attempt to download non-Tinker 1st and 3rd party app) 2322 // These apps should prevented from installing on a Tinker device 2323 case "com.apple.mobilemail.watchkitapp": 2324 case "com.apple.news.watchkitapp": 2325 case "com.apple.iBooks.watchkitapp": 2326 return true; 2327 default: 2328 return false; 2329 } 2330 } 2331 } 2332 return false; 2333} 2334/** 2335 * Device Sizes 2336 * 2337 * Please do not use these constants for anything but screenshots. 2338 * Our code should not depend on absolute screen sizes for anything 2339 * not related to selecting the correct screenshots to display. -km 2340 */ 2341/// The screen size of iPhone 6.5" devices. 2342export const screenSizeIphone65 = new modelsBase.Size(414.0, 896.0); 2343/// The screen size of iPhone 5.8" devices. 2344export const screenSizeIPhone58 = new modelsBase.Size(375.0, 812.0); 2345/// The screen size of iPhone 6+ like devices. 2346export const screenSizeIPhone6Plus = new modelsBase.Size(414.0, 736.0); 2347/// The screen size of iPhone 6 like devices. 2348export const screenSizeIPhone6 = new modelsBase.Size(375.0, 667.0); 2349/// The screen size of iPhone 5 like devices. 2350export const screenSizeIPhone5 = new modelsBase.Size(320.0, 568.0); 2351/// The screen size of original iPhone like devices. 2352export const screenSizeIPhoneOriginal = new modelsBase.Size(320.0, 480.0); 2353/// The screen size of iPad and iPad mini devices. 2354export const screenSizeIPad = new modelsBase.Size(768.0, 1024.0); 2355/// The screen size of 7th and 8th gen 10.2" iPads. 2356export const screenSizeIPad102 = new modelsBase.Size(810.0, 1080.0); 2357/// The screen size of iPad pro 10.5" devices. 2358export const screenSizeIPad105 = new modelsBase.Size(834.0, 1112.0); 2359/// The screen size of iPad pro 11" devices. 2360export const screenSizeIPad11 = new modelsBase.Size(834.0, 1194.0); 2361/// The screen size of iPad Pro 11" devices in landscape orientation. 2362/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. 2363export const screenSizeIPad11Landscape = new modelsBase.Size(1194.0, 834.0); 2364/// The screen size of iPad pro 12.9" devices. 2365export const screenSizeIPadPro = new modelsBase.Size(1024.0, 1366.0); 2366/// The screen size of iPad pro 12.9" devices, with rounded corners. 2367export const screenSizeIPadPro2018 = new modelsBase.Size(1024.0, 1366.0); 2368/// The screen size of iPad pro 12.9" devices, with rounded corners, in landscape orientation. 2369/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. 2370export const screenSizeIPadPro2018Landscape = new modelsBase.Size(1366.0, 1024.0); 2371// The screen size of the J310 iPad device. 2372export const screenSizeIPadJ310 = new modelsBase.Size(744.0, 1133.0); 2373// The screen size of the J310 iPad device, in landscape orientation. 2374// rdar: //83176176 (J310: kMGQMainScreenCanvasSizes reports width as largest dimension, contrary to all other iPads and UIKit) 2375export const screenSizeIPadJ310Landscape = new modelsBase.Size(1133.0, 744.0); 2376/// The screen size of J720/J721 devices. 2377export const screenSizeJ720 = new modelsBase.Size(1032.0, 1376.0); 2378/// The screen size of J720/J721, in landscape orientation. 2379/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. 2380export const screenSizeJ720Landscape = new modelsBase.Size(1376.0, 1032.0); 2381/// The screen size of J717/J718 devices. 2382export const screenSizeJ717 = new modelsBase.Size(834.0, 1210.0); 2383/// The screen size of J717/J718, in landscape orientation. 2384/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. 2385export const screenSizeJ717Landscape = new modelsBase.Size(1210.0, 834.0); 2386/// The screen size of the 42mm Apple Watch devices. 2387export const screenSizeWatch = new modelsBase.Size(312.0, 390.0); 2388/// The screen size of large 2018 Apple Watch devices. 2389export const screenSizeWatch2018 = new modelsBase.Size(368.0, 448.0); 2390// The screen size of the large 2021 Apple Watch devices. 2391export const screenSizeWatch2021 = new modelsBase.Size(396.0, 484.0); 2392// The screen size for the 2022 Apple Watch devices. 2393export const screenSizeWatch2022 = new modelsBase.Size(410.0, 502.0); 2394// The screen size for the 2024 Apple Watch devices. 2395export const screenSizeWatch2024 = new modelsBase.Size(416.0, 496.0); 2396// The screen size for the Apple Watch Ultra / Ultra 2 devices. 2397export const screenSizeWatchUltra = new modelsBase.Size(410.0, 502.0); 2398/// The screen size for iPad device. 2399export const screenSizeIPadAir2020 = new modelsBase.Size(820.0, 1180.0); 2400/// The screen size for iPhone devices. 2401export const screenSizeIPhone131 = new modelsBase.Size(360.0, 780.0); 2402export const screenSizeIPhone132 = new modelsBase.Size(390.0, 844.0); 2403export const screenSizeIPhone134 = new modelsBase.Size(428.0, 926.0); 2404// The screen size for a 6.1" D73-style device. 2405export const screenSizeIPhone61 = new modelsBase.Size(393.0, 852.0); 2406// The screen size for a 6.7" D74-style device. 2407export const screenSizeIPhone67 = new modelsBase.Size(430.0, 932.0); 2408/// The screen size for a D93 device. 2409export const screenSizeD93 = new modelsBase.Size(402.0, 874.0); 2410/// The screen size for a D94 device. 2411export const screenSizeD94 = new modelsBase.Size(440.0, 956.0); 2412/// The screen size for a D23 device. 2413export const screenSizeD23 = new modelsBase.Size(420.0, 912.0); 2414/// The screen size for a N230 device. 2415export const screenSizeN230 = new modelsBase.Size(422.0, 514.0); 2416/// All phone types, in order of decreasing size. 2417const decreasingPhoneTypes = [ 2418 "iphone_d74", 2419 "iphone_6_5", 2420 "iphone_d73", 2421 "iphone_5_8", 2422 "iphone6+", 2423 "iphone6", 2424 "iphone5", 2425 "iphone", 2426]; 2427// region Device Corner Radius 2428/** 2429 * The reason we need to hardcode these is because we may want to display screenshots with rounding for a device that is 2430 * not the one the user is currently browsing the store with. For example, imagine that the user is browsing the store 2431 * with an iPhone 8 but ends up looking at an app that only has D22 screenshots. They should see the D22 screenshots 2432 * according to the D22 corner rounding, and using the current client's `screenCornerRadius` would not give us the 2433 * proper value. 2434 */ 2435/// The device corner radius of iPad pro 12.9" devices from 2018. 2436const deviceCornerRadiusIpadPro2018 = 18.0; 2437/// The device corner radius of iPad pro 11" devices. 2438const deviceCornerRadiusIpad11 = 18.0; 2439/// The device corner radius of iPhone 6.5" devices. 2440const deviceCornerRadiusIphone65 = 41.5; 2441/// The device corner radius of iPhone 5.8" devices. 2442const deviceCornerRadiusIphone58 = 39.0; 2443/// The device corner radius of iPhone 6.1" devices. 2444const deviceCornerRadiusIphone61 = 55.0; 2445/// The device corner radius of iPhone 6.7" devices. 2446const deviceCornerRadiusIphone67 = 55.0; 2447/// The device corner radius of large 2018 Apple Watch devices. 2448const deviceCornerRadiusWatch2018 = 34.0; 2449/// The outer device corner radius of large Apple Watch devices. 2450const outerDeviceCornerRadiusWatch = 30.0; 2451/// The device border thickness for Apple Watch. 2452const deviceBorderThicknessWatch = 13.0; 2453/// The device border thickness for 2018 Apple Watch. 2454const deviceBorderThicknessWatch2018 = 11.0; 2455/// The device corner radius for 2021 Apple Watch. 2456const deviceCornerRadiusWatch2021 = 55; 2457/// The device border thickness for 2021 Apple Watch. 2458const deviceBorderThicknessWatch2021 = 5.5; 2459/// The device corner radius for 2022/2024 Apple Watch. 2460const deviceCornerRadiusWatch2022 = 108; 2461/// The outer device corner radius for 2022/2024 Apple Watch. 2462const deviceOuterCornerRadiusWatch2022 = 112.5; 2463/// The device border thickness for 2022/2024 Apple Watch. 2464const deviceBorderThicknessWatch2022 = 4.5; 2465export function currentAppPlatform(objectGraph) { 2466 var _a; 2467 switch (objectGraph.client.deviceType) { 2468 case "web": 2469 return unwrap((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform); 2470 default: 2471 return objectGraph.client.deviceType; 2472 } 2473} 2474/// The SF Symbol name that represents the media type. 2475export function systemImageNameForAppPlatform(appPlatform) { 2476 switch (appPlatform) { 2477 case "phone": 2478 return "iphone"; 2479 case "pad": 2480 return "ipad"; 2481 case "tv": 2482 return "tv"; 2483 case "watch": 2484 return "applewatch"; 2485 case "mac": 2486 return "macbook"; 2487 case "messages": 2488 return "message"; 2489 case "vision": 2490 return "visionpro"; 2491 default: 2492 unreachable(appPlatform); 2493 } 2494} 2495/** 2496 * Returns the factor by which to multiply an artwork's portrait-equivalent width, in order to compute the artwork's 2497 * device-rounded corner radius. This is useful because we want to display screenshots for device-rounded screenshots 2498 * with a corner radius that is scaled to that of the device screen. 2499 * 2500 * r = w * r' 2501 * r' = R / W 2502 * 2503 * Where: 2504 * r: scaled radius 2505 * r': The return value of this function. 2506 * R: device corner radius 2507 * w: width at which the screenshot will be displayed 2508 * W: device width 2509 * 2510 * We need to have this value here in the JS because we may need to display device-rounded screenshots on a device that 2511 * does not have a device corner radius; there is no native API for querying the corner radius of various devices and, 2512 * even if there were, we want to avoid specific screen-size checks in the native code. 2513 * @param type The screenshot type vended by the server. 2514 * @returns {number} The device corner radius factor, or null if the device does not have a corner radius. 2515 */ 2516function deviceCornerRadiusFactorForMediaType(objectGraph, type) { 2517 // Let's only bridge over and access client's properties if we need to. 2518 switch (type) { 2519 case "ipadPro_2018": 2520 return deviceCornerRadiusIpadPro2018 / screenSizeIPadPro2018.width; 2521 case "ipad_11": 2522 return deviceCornerRadiusIpad11 / screenSizeIPad11.width; 2523 case "iphone_6_5": 2524 return deviceCornerRadiusIphone65 / screenSizeIphone65.width; 2525 case "iphone_5_8": 2526 return deviceCornerRadiusIphone58 / screenSizeIPhone58.width; 2527 case "iphone_d73": 2528 return deviceCornerRadiusIphone61 / screenSizeIPhone61.width; 2529 case "iphone_d74": 2530 return deviceCornerRadiusIphone67 / screenSizeIPhone67.width; 2531 case "appleWatch_2018": 2532 return deviceCornerRadiusWatch2018 / screenSizeWatch2018.width; 2533 case "appleWatch_2021": 2534 return deviceCornerRadiusWatch2021 / screenSizeWatch2021.width; 2535 case "appleWatch_2022": 2536 return deviceCornerRadiusWatch2022 / screenSizeWatch2022.width; 2537 case "appleWatch_2024": 2538 return deviceCornerRadiusWatch2022 / screenSizeWatch2024.width; 2539 default: 2540 return null; 2541 } 2542} 2543function deviceOuterCornerRadiusFactorForMediaType(objectGraph, type) { 2544 switch (type) { 2545 case "appleWatch": 2546 return outerDeviceCornerRadiusWatch / screenSizeWatch.width; 2547 case "appleWatch_2022": 2548 return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2022.width; 2549 case "appleWatch_2024": 2550 return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2024.width; 2551 default: 2552 return deviceCornerRadiusFactorForMediaType(objectGraph, type); 2553 } 2554} 2555function deviceBorderThicknessForMediaType(objectGraph, type) { 2556 switch (type) { 2557 case "appleWatch": 2558 return deviceBorderThicknessWatch / screenSizeWatch.width; 2559 case "appleWatch_2018": 2560 return deviceBorderThicknessWatch2018 / screenSizeWatch2018.width; 2561 case "appleWatch_2021": 2562 return deviceBorderThicknessWatch2021 / screenSizeWatch2021.width; 2563 case "appleWatch_2022": 2564 return deviceBorderThicknessWatch2022 / screenSizeWatch2022.width; 2565 case "appleWatch_2024": 2566 return deviceBorderThicknessWatch2022 / screenSizeWatch2024.width; 2567 default: 2568 return null; 2569 } 2570} 2571// endregion 2572/** 2573 Returns a boolean indicating if the client's operating 2574 system is the same or later than the specified version. 2575 2576 @param version The full version number to check against 2577 @returns true if the operating system is the same or newer than the specified version; false otherwise. 2578 */ 2579export function isOSAtLeastVersion(objectGraph, version) { 2580 if (serverData.isNull(version) || version.length === 0) { 2581 return true; 2582 } 2583 const versionComponents = version.split("."); 2584 const majorVersion = serverData.asNumber(versionComponents[0]) || 0; 2585 const minorVersion = serverData.asNumber(versionComponents[1]) || 0; 2586 const patchVersion = serverData.asNumber(versionComponents[2]) || 0; 2587 return objectGraph.host.isOSAtLeast(majorVersion, minorVersion, patchVersion); 2588} 2589/** 2590 Returns a boolean indicating if the system version of the active, paired watch (if any) is 2591 at least the provided version number. 2592 2593 @param version The full version number to check against 2594 @returns true if an active, paired watch exists, and its operating system version is the same or newer than the specified version; false otherwise. 2595 */ 2596export function isActivePairedWatchOSAtLeastVersion(objectGraph, version) { 2597 if (serverData.isNull(version) || version.length === 0) { 2598 return true; 2599 } 2600 const versionComponents = version.split("."); 2601 const majorVersion = serverData.asNumber(versionComponents[0]) || 0; 2602 const minorVersion = serverData.asNumber(versionComponents[1]) || 0; 2603 const patchVersion = serverData.asNumber(versionComponents[2]) || 0; 2604 return objectGraph.client.isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion); 2605} 2606/** 2607 * Check whether the active paired device's OS is the same or greater than a given version. 2608 */ 2609export function isActivePairedDeviceAtLeastVersion(objectGraph, version) { 2610 if (serverData.isNull(version) || version.length === 0) { 2611 return true; 2612 } 2613 return objectGraph.client.isPairedSystemVersionAtLeast(version); 2614} 2615/** 2616 * Check whether the active paired device's OS is below a given version. 2617 */ 2618export function isActivePairedWatchOSBelowVersion(objectGraph, version) { 2619 if (serverData.isNull(version) || version.length === 0) { 2620 return false; 2621 } 2622 return objectGraph.client.isActivePairedWatchSystemVersionBelow(version); 2623} 2624export function shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfStyle) { 2625 if (objectGraph.client.isTV) { 2626 switch (shelfStyle) { 2627 case "upsellBreakout": 2628 return true; 2629 default: 2630 return false; 2631 } 2632 } 2633 else { 2634 switch (shelfStyle) { 2635 case "smallLockup": 2636 case "mediumLockup": 2637 case "appTrailerLockup": 2638 case "screenshotsLockup": 2639 case "mixedMediaLockup": 2640 case "upsellBreakout": 2641 case "arcadeShowcase": 2642 return true; 2643 default: 2644 return false; 2645 } 2646 } 2647} 2648export function shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, displayStyle) { 2649 switch (displayStyle) { 2650 case "LockupSmall": 2651 case "LockupLarge": 2652 case "BreakoutLarge": 2653 case "Hero": 2654 case "EditorialLockupLarge": 2655 case "EditorialLockupLargeVariant": 2656 case "EditorialLockupMedium": 2657 case "EditorialLockupMediumVariant": 2658 case "StoryMedium": 2659 return true; 2660 default: 2661 return false; 2662 } 2663} 2664/** 2665 * The dynamic date string used by apps coming soon. 2666 */ 2667export function dynamicPreorderDateFromData(objectGraph, data, fallbackLabel) { 2668 const preorderOffer = offers.offerDataFromData(objectGraph, data); 2669 const isPreorder = serverData.asString(preorderOffer, "type") === "preorder"; 2670 if (isPreorder) { 2671 const releaseDateRaw = serverData.asString(preorderOffer, "expectedReleaseDate"); 2672 const dateDisplayFormat = contentAttributes.contentAttributeAsString(objectGraph, data, "expectedReleaseDateDisplayFormat"); 2673 if (serverData.isDefinedNonNullNonEmpty(dateDisplayFormat)) { 2674 if (serverData.isDefinedNonNullNonEmpty(releaseDateRaw)) { 2675 const releaseDate = dateUtil.parseDateOmittingTimeFromString(releaseDateRaw); 2676 const tokenFormatMap = { 2677 "@@expectedDateMY@@": objectGraph.loc.string("PreOrder.Date.MonthYear"), 2678 "@@expectedDateMDY@@": objectGraph.loc.string("PreOrder.Date.MonthDayYear"), 2679 }; 2680 for (const [serverToken, dateFormat] of Object.entries(tokenFormatMap)) { 2681 if (dateDisplayFormat.includes(serverToken)) { 2682 let formattedDate = objectGraph.loc.formatDateWithContext(dateFormat, releaseDate, "middleOfSentence"); 2683 if (objectGraph.client.isTV) { 2684 formattedDate = formattedDate.replace(/ /g, "\u00a0"); 2685 } 2686 return dateDisplayFormat.replace(serverToken, formattedDate); 2687 } 2688 } 2689 } 2690 return dateDisplayFormat; 2691 } 2692 } 2693 // There was no dynamic date to display 2694 return fallbackLabel; 2695} 2696/** 2697 * The primary content for an editorial item. 2698 * @param data The data from which to derive the primary content. 2699 */ 2700export function primaryContentForData(objectGraph, data) { 2701 const primaryContent = mediaRelationship.relationshipData(objectGraph, data, "primary-content"); 2702 if (serverData.isDefinedNonNullNonEmpty(primaryContent)) { 2703 return primaryContent; 2704 } 2705 // If an EI has canvasData, then in MAPI response its "primary-content" relationship will not include the 2706 // primary content meta data. Instead, the primary content data will be included in the "card-contents" relationship. 2707 if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isCanvasAvailable")) { 2708 return mediaRelationship.relationshipData(objectGraph, data, "card-contents"); 2709 } 2710 return null; 2711} 2712const grayColorHex = "#9BA9BD"; 2713export const grayColorCriteria = { 2714 colorHex: grayColorHex, 2715 maxSaturation: 4, 2716 maxBrightness: 9, 2717}; 2718/** 2719 * The list of tag background colors we can use to match an app icon background color against, based on saturation and brightness 2720 */ 2721const saturationBrightnessBasedTagColorBuckets = [ 2722 grayColorCriteria, // Gray 2723]; 2724/** 2725 * The list of tag background colors we can use to match an app icon background color against 2726 */ 2727const hueBasedTagColorBuckets = [ 2728 { colorHex: "#F7816F", minHue: 0, maxHue: 16 }, 2729 { colorHex: "#FF9034", minHue: 17, maxHue: 33 }, 2730 { colorHex: "#E3B059", minHue: 34, maxHue: 59 }, 2731 { colorHex: "#74BD66", minHue: 60, maxHue: 129 }, 2732 { colorHex: "#72C792", minHue: 130, maxHue: 169 }, 2733 { colorHex: "#61BFE2", minHue: 170, maxHue: 209 }, 2734 { colorHex: "#6EA3E9", minHue: 210, maxHue: 239 }, 2735 { colorHex: "#7D69FA", minHue: 240, maxHue: 259 }, 2736 { colorHex: "#B363F7", minHue: 260, maxHue: 289 }, 2737 { colorHex: "#EE7CBD", minHue: 290, maxHue: 360 }, // Pink 2738]; 2739/** 2740 * Find the matching tag color for an icon's background color 2741 * 2742 * @param iconBackgroundColor The bgColor from the icon artwork data 2743 * @returns The closest matching color from the HI provided set of tag background colors for this icon color 2744 */ 2745export function closestTagBackgroundColorForIcon(iconBackgroundColor) { 2746 var _a; 2747 return ((_a = color.findColorBucketForColor(iconBackgroundColor, saturationBrightnessBasedTagColorBuckets, hueBasedTagColorBuckets)) !== null && _a !== void 0 ? _a : color.fromHex(grayColorHex)); 2748} 2749/** 2750 * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object 2751 * @param data An EditorialArtwork JSON object from MAPI 2752 * @returns A JoeColorSet from parsing the input RGB values 2753 */ 2754export function joeColorHexSetFromData(data) { 2755 var _a, _b, _c, _d, _e; 2756 const textGradient = []; 2757 for (const gradientColorHex of serverData.asArrayOrEmpty(data, "textGradient")) { 2758 if (isSome(gradientColorHex) && serverData.isString(gradientColorHex)) { 2759 textGradient.push(gradientColorHex); 2760 } 2761 } 2762 return { 2763 bgColor: (_a = serverData.asString(data, "bgColor")) !== null && _a !== void 0 ? _a : undefined, 2764 textColor1: (_b = serverData.asString(data, "textColor1")) !== null && _b !== void 0 ? _b : undefined, 2765 textColor2: (_c = serverData.asString(data, "textColor2")) !== null && _c !== void 0 ? _c : undefined, 2766 textColor3: (_d = serverData.asString(data, "textColor3")) !== null && _d !== void 0 ? _d : undefined, 2767 textColor4: (_e = serverData.asString(data, "textColor4")) !== null && _e !== void 0 ? _e : undefined, 2768 textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined, 2769 }; 2770} 2771/** 2772 * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object 2773 * @param data An EditorialArtwork JSON object from MAPI 2774 * @returns A JoeColorSet from parsing the input RGB values 2775 */ 2776export function joeColorSetFromData(data) { 2777 var _a, _b, _c, _d, _e, _f; 2778 const joeColorHexSet = joeColorHexSetFromData(data); 2779 const textGradient = []; 2780 for (const gradientColorHex of (_a = joeColorHexSet.textGradient) !== null && _a !== void 0 ? _a : []) { 2781 const gradientColor = color.fromHex(gradientColorHex); 2782 if (isSome(gradientColor)) { 2783 textGradient.push(gradientColor); 2784 } 2785 } 2786 return { 2787 bgColor: (_b = color.fromHex(joeColorHexSet.bgColor)) !== null && _b !== void 0 ? _b : undefined, 2788 textColor1: (_c = color.fromHex(joeColorHexSet.textColor1)) !== null && _c !== void 0 ? _c : undefined, 2789 textColor2: (_d = color.fromHex(joeColorHexSet.textColor2)) !== null && _d !== void 0 ? _d : undefined, 2790 textColor3: (_e = color.fromHex(joeColorHexSet.textColor3)) !== null && _e !== void 0 ? _e : undefined, 2791 textColor4: (_f = color.fromHex(joeColorHexSet.textColor4)) !== null && _f !== void 0 ? _f : undefined, 2792 textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined, 2793 }; 2794} 2795/** 2796 * Attempt to find the first non-gray placeholder color 2797 * @param joeColorSet The joe color set for a given icon 2798 * @returns The color hext value to use for the joe color placeholder 2799 */ 2800export function bestJoeColorPlaceholderSelectionLogic(joeColorSet) { 2801 const joeColorKeys = [ 2802 "bgColor", 2803 "textColor1", 2804 "textColor2", 2805 "textColor3", 2806 "textColor4", 2807 ]; 2808 for (const joeColorKey of joeColorKeys) { 2809 const joeColorHex = joeColorSet[joeColorKey]; 2810 if (!serverData.isString(joeColorHex)) { 2811 continue; 2812 } 2813 const isGrayColor = color.doesColorMeetCriteria(color.fromHex(joeColorHex), grayColorCriteria); 2814 if (!isGrayColor) { 2815 return joeColorHex; 2816 } 2817 } 2818 return null; 2819} 2820//# sourceMappingURL=content.js.map