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