summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/content
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/content')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js105
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js71
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js33
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js328
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/content.js2820
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js91
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js485
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js77
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js322
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/content/sad.js93
10 files changed, 4425 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js b/node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js
new file mode 100644
index 0000000..91d95a6
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js
@@ -0,0 +1,105 @@
+import { isSome } from "@jet/environment";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as serverData from "../../foundation/json-parsing/server-data";
+/**
+ * Provides the localized name of the product's age rating, e.g. "12+".
+ * @param objectGraph The App Store object graph.
+ * @param data The Media API data for an app.
+ * @param useLegacyFallback Indicates whether the legacy key should be used if the modern one is missing.
+ * @returns The localized `string` of the product's age rating, or `undefined`.
+ */
+export function name(objectGraph, data, useLegacyFallback = false) {
+ const modernValue = mediaAttributes.attributeAsString(data, "ageRating.name");
+ if (isSome(modernValue)) {
+ return modernValue;
+ }
+ else if (useLegacyFallback) {
+ return mediaAttributes.attributeAsString(data, "contentRatingsBySystem.appsApple.name");
+ }
+ else {
+ return undefined;
+ }
+}
+/**
+ * Provides the product's age rating value, e.g. 300. This value is understood
+ * by systems such as Managed Configuration and Screen Time to enforce content
+ * restrictions.
+ * @param objectGraph The App Store object graph.
+ * @param data The Media API data for an app.
+ * @param useLegacyFallback Indicates whether the legacy key should be used if the modern one is missing.
+ * @returns The `number` value of the product's age rating, or `undefined`.
+ */
+export function value(objectGraph, data, useLegacyFallback = false) {
+ const modernValue = mediaAttributes.attributeAsNumber(data, "ageRating.value");
+ if (isSome(modernValue)) {
+ return modernValue;
+ }
+ else if (useLegacyFallback) {
+ return mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsApple.value");
+ }
+ else {
+ return undefined;
+ }
+}
+/**
+ * Provides the product's age rating description from Media API. This was
+ * historically generated by the client, so has no legacy fallback.
+ * @param objectGraph The App Store object graph.
+ * @param data The Media API data for an app.
+ * @returns The product's age rating description `string`, or `undefined`.
+ */
+export function description(objectGraph, data) {
+ return mediaAttributes.attributeAsString(data, "ageRating.description");
+}
+export function hasInAppControls(objectGraph, data) {
+ const contentLevels = mediaAttributes.attributeAsArrayOrEmpty(data, "ageRating.contentLevels");
+ for (const contentLevel of contentLevels) {
+ if (serverData.asString(contentLevel, "kind") === "IAC") {
+ return true;
+ }
+ }
+ return false;
+}
+/**
+ * Provides the product's developer age guidance URL from Media API. This is an
+ * optional URL the developer can provide in App Store Connect to give further
+ * details on their app's content controls. There is no legacy fallback for
+ * this value.
+ * @param objectGraph The App Store object graph.
+ * @param data The Media API data for an app.
+ * @returns The product's developer age guidance URL `string`, or `undefined`.
+ */
+export function developerAgeGuidanceURL(objectGraph, data) {
+ return mediaAttributes.attributeAsString(data, "ageRating.ageGuidanceUrl");
+}
+/**
+ * Returns the name of an image resource in the App Store bundle corresponding
+ * to the provided MAPI `ageRating` data.
+ * @param objectGraph The App Store object graph.
+ * @param data The Media API data for an app.
+ * @returns The `string` name of an image resource in the bundle, or `undefined`.
+ */
+export function pictogramResource(objectGraph, data) {
+ // Values pulled from https://quip-apple.com/0bq3AiLxhzaW
+ const pictogramResources = new Map([
+ // Brazil
+ ["br.100.official", "br.l.official"],
+ ["br.100", "br.l"],
+ ["br.210.official", "br.10.official"],
+ ["br.210", "br.10"],
+ ["br.300.official", "br.12.official"],
+ ["br.300", "br.12"],
+ ["br.314.official", "br.14.official"],
+ ["br.314", "br.14"],
+ ["br.416.official", "br.16.official"],
+ ["br.416", "br.16"],
+ ["br.618.official", "br.18.official"],
+ ["br.618", "br.18"],
+ ]);
+ const storefront = objectGraph.locale.activeStorefront;
+ const contentLevel = mediaAttributes.attributeAsString(data, "ageRating.value");
+ const isOfficial = mediaAttributes.attributeAsBooleanOrFalse(data, "ageRating.isOfficial");
+ const key = storefront + "." + contentLevel + (isOfficial ? ".official" : "");
+ return pictogramResources.get(key);
+}
+//# sourceMappingURL=age-ratings.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js
new file mode 100644
index 0000000..ebf7c71
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js
@@ -0,0 +1,71 @@
+import * as modelsBase from "../../../api/models/base";
+export function createArtworkVariantForClient(objectGraph, allowingTransparency = false, supportsWideGamut = false, useCase = 0 /* content.ArtworkUseCase.Default */) {
+ const format = artworkFormatForClient(objectGraph, allowingTransparency);
+ return createArtworkVariantForFormat(objectGraph, format, supportsWideGamut, useCase);
+}
+export function createArtworkVariantForFormat(objectGraph, format, supportsWideGamut = false, useCase = 0 /* content.ArtworkUseCase.Default */) {
+ const quality = qualityForClient(objectGraph, format, useCase);
+ return new modelsBase.ArtworkVariant(format, quality, supportsWideGamut);
+}
+export function createArtworkForResource(objectGraph, resource, width = 0, height = 0, backgroundColor = null, textColor = null, checksum = null) {
+ const artwork = new modelsBase.Artwork(resource, width, height, [
+ createArtworkVariantForClient(objectGraph, false, false, 0 /* content.ArtworkUseCase.Default */),
+ ]);
+ artwork.backgroundColor = backgroundColor;
+ artwork.textColor = textColor;
+ artwork.checksum = checksum;
+ return artwork;
+}
+/**
+ * Create a bundle artwork template for a resource.
+ */
+export function artworkTemplateForBundleImage(resource) {
+ if (preprocessor.GAMES_TARGET) {
+ return `bundleimage://${resource}?bundleid=com.apple.GameStoreKit`;
+ }
+ else {
+ return `resource://${resource}`;
+ }
+}
+/**
+ * Create Artwork for a system image.
+ */
+export function createArtworkForSystemImage(objectGraph, resource) {
+ return createArtworkForResource(objectGraph, `systemimage://${resource}`);
+}
+function artworkFormatForClient(objectGraph, allowsTransparency) {
+ // Determine whether HEIF is supported for the given target.
+ let supportsHEIF;
+ switch (objectGraph.host.clientIdentifier) {
+ case "com.apple.TVAppStore.AppStoreTopShelfExtension":
+ case "com.apple.Arcade.ArcadeTopShelfExtension":
+ case "com.apple.AppStore.Widgets":
+ // Disable HEIF for top shelf
+ supportsHEIF = false;
+ break;
+ default:
+ supportsHEIF = objectGraph.client.supportsHEIF;
+ break;
+ }
+ // HEIF is the image format, HEIC is the container
+ const defaultArtworkFormat = supportsHEIF ? "heic" : "jpeg";
+ const defaultArtworkFormatAllowingTransparency = supportsHEIF ? "heic" : "png";
+ return allowsTransparency ? defaultArtworkFormatAllowingTransparency : defaultArtworkFormat;
+}
+function qualityForClient(objectGraph, artworkFormat, useCase) {
+ switch (artworkFormat) {
+ case "heic": {
+ if (objectGraph.client.isTV && useCase === 21 /* content.ArtworkUseCase.Uber */) {
+ return 70;
+ }
+ else {
+ return 60;
+ }
+ }
+ // JPEG, LCR and PNG
+ default: {
+ return 70;
+ }
+ }
+}
+//# sourceMappingURL=artwork.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js
new file mode 100644
index 0000000..7607675
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js
@@ -0,0 +1,33 @@
+/**
+ * Created by keithpk on 1/25/17.
+ */
+import * as serverData from "../../../foundation/json-parsing/server-data";
+import * as artworkBuilder from "./artwork";
+export function closestArtworkMatchingSize(objectGraph, artworkArray, width, height) {
+ const desiredRatio = width / height;
+ let foundAspectRatio = 0;
+ let foundArtwork = null;
+ for (const artwork of artworkArray) {
+ const actualWidth = serverData.asNumber(artwork, "width");
+ const actualHeight = serverData.asNumber(artwork, "height");
+ const actualRatio = actualWidth / actualHeight;
+ if (actualRatio === desiredRatio ||
+ Math.abs(desiredRatio - actualRatio) <= Math.abs(desiredRatio - foundAspectRatio)) {
+ if (!foundArtwork ||
+ (actualWidth <= width && actualWidth > foundArtwork.width) ||
+ (foundArtwork.width > width && actualWidth < foundArtwork.width && actualWidth > foundArtwork.width)) {
+ foundArtwork = artwork;
+ foundAspectRatio = actualRatio;
+ }
+ }
+ }
+ if (foundArtwork) {
+ const artworkUrl = serverData.asString(foundArtwork, "url");
+ const foundWidth = serverData.asNumber(foundArtwork, "width");
+ const foundHeight = serverData.asNumber(foundArtwork, "height");
+ const foundChecksum = serverData.asString(foundArtwork, "checksum");
+ return artworkBuilder.createArtworkForResource(objectGraph, artworkUrl, foundWidth, foundHeight, null, null, foundChecksum);
+ }
+ return null;
+}
+//# sourceMappingURL=legacy-artwork.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js b/node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js
new file mode 100644
index 0000000..1cae53a
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js
@@ -0,0 +1,328 @@
+import { isNothing, isSome } from "@jet/environment/types/optional";
+import * as derivedData from "../../foundation/json-parsing/derived-data";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes";
+import { variantAttributeForKey } from "../product-page/product-page-variants";
+import * as contentDeviceFamily from "./device-family";
+/**
+ * Retrieve the specified attribute from the data, coercing it to a JSONData dictionary
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The path of the attribute.
+ * @param attributePlatform The specific platform attribute to get content for. Omit to infer from the data's structure.
+ * @param defaultValue The object to return if the path search fails.
+ * @returns The dictionary of data
+ */
+export function contentAttributeAsDictionary(objectGraph, data, attributePath, attributePlatform, defaultValue) {
+ if (!attributePlatform) {
+ attributePlatform = bestAttributePlatformFromData(objectGraph, data);
+ }
+ if (isNothing(attributePlatform)) {
+ return null;
+ }
+ let value = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, attributePath);
+ if (!value) {
+ value = mediaAttributes.attributeAsDictionary(data, attributePath, defaultValue);
+ }
+ return value;
+}
+/**
+ * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array.
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The path of the attribute.
+ * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @returns {any[]} The attribute value as an array.
+ */
+export function contentAttributeAsArrayOrEmpty(objectGraph, data, attributePath, attributePlatformOverride = undefined) {
+ const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : bestAttributePlatformFromData(objectGraph, data);
+ if (isNothing(attributePlatform)) {
+ return [];
+ }
+ let value = mediaPlatformAttributes.platformAttributeAsArrayOrEmpty(data, attributePlatform, attributePath);
+ if (serverData.isNullOrEmpty(value)) {
+ value = mediaAttributes.attributeAsArrayOrEmpty(data, attributePath);
+ }
+ return value;
+}
+/**
+ * Retrieve the specified attribute from the data as an array.
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The path of the attribute.
+ * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @returns {any[]} The attribute value as an array.
+ */
+export function contentAttributeAsArray(objectGraph, data, attributePath, attributePlatformOverride = undefined) {
+ const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : bestAttributePlatformFromData(objectGraph, data);
+ if (isNothing(attributePlatform)) {
+ return null;
+ }
+ let value = mediaPlatformAttributes.platformAttributeAsArray(data, attributePlatform, attributePath);
+ if (isNothing(value)) {
+ value = mediaAttributes.attributeAsArray(data, attributePath);
+ }
+ return value;
+}
+/**
+ * Retrieve the specified attribute from the data as a string.
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The object path for the attribute.
+ * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @param policy The validation policy to use when resolving this value.
+ * @returns {string} The attribute value as a string.
+ */
+export function contentAttributeAsString(objectGraph, data, attributePath, attributePlatformOverride = undefined, policy = "coercible") {
+ let value;
+ const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : bestAttributePlatformFromData(objectGraph, data);
+ if (isSome(attributePlatform)) {
+ value = mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, attributePath, policy);
+ }
+ if (!value) {
+ value = mediaAttributes.attributeAsString(data, attributePath, policy);
+ }
+ return value;
+}
+/**
+ * Retrieve the specified attribute from the data as a boolean.
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The path of the attribute.
+ * @param policy The validation policy to use when resolving this value.
+ * @returns {boolean} The attribute value as a boolean.
+ */
+export function contentAttributeAsBoolean(objectGraph, data, attributePath, attributePlatform, policy = "coercible") {
+ if (!attributePlatform) {
+ attributePlatform = bestAttributePlatformFromData(objectGraph, data);
+ }
+ if (isNothing(attributePlatform)) {
+ return null;
+ }
+ let value = mediaPlatformAttributes.platformAttributeAsBoolean(data, attributePlatform, attributePath, policy);
+ if (serverData.isNull(value)) {
+ value = mediaAttributes.attributeAsBoolean(data, attributePath, policy);
+ }
+ return value;
+}
+/**
+ * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist.
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The path of the attribute.
+ * @param attributePlatform The specific platform attribute to get content for. Omit to infer from the data's structure.
+ * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present..
+ */
+export function contentAttributeAsBooleanOrFalse(objectGraph, data, attributePath, attributePlatform) {
+ if (!attributePlatform) {
+ attributePlatform = bestAttributePlatformFromData(objectGraph, data);
+ }
+ if (isNothing(attributePlatform)) {
+ return false;
+ }
+ let value = mediaPlatformAttributes.platformAttributeAsBoolean(data, attributePlatform, attributePath);
+ if (serverData.isNull(value)) {
+ value = mediaAttributes.attributeAsBooleanOrFalse(data, attributePath);
+ }
+ return value;
+}
+/**
+ * Retrieve the specified attribute from the data as a number.
+ *
+ * @param data The data from which to retrieve the attribute.
+ * @param attributePath The path of the attribute.
+ * @param policy The validation policy to use when resolving this value.
+ * @returns {boolean} The attribute value as a number.
+ */
+export function contentAttributeAsNumber(objectGraph, data, attributePath, policy = "coercible") {
+ const attributePlatform = bestAttributePlatformFromData(objectGraph, data);
+ if (isNothing(attributePlatform)) {
+ return null;
+ }
+ let value = mediaPlatformAttributes.platformAttributeAsNumber(data, attributePlatform, attributePath, policy);
+ if (serverData.isNull(value)) {
+ value = mediaAttributes.attributeAsNumber(data, attributePath);
+ }
+ return value;
+}
+/**
+ * Computes the best attribute platform for a given piece of content
+ *
+ * @param {Data} data The media API data representing the content
+ * @returns {AttributePlatform}
+ */
+export function bestAttributePlatformFromData(objectGraph, data, clientIdentifierOverride) {
+ const baseCacheKey = "bestAttributePlatformFromData";
+ const cacheKey = isSome(clientIdentifierOverride) ? `${baseCacheKey}.${clientIdentifierOverride}` : baseCacheKey;
+ return derivedData.value(data, cacheKey, () => {
+ const isIOSOnly = contentDeviceFamily.dataOnlyHasDeviceFamilies(objectGraph, data, ["iphone", "ipad", "ipod"], true);
+ const isTvOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "tvos");
+ const isMacOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac");
+ const isWatchOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "watch");
+ const isVisionOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice");
+ // 1. The data is for a single platform only.
+ let dedicatedPlatform = null;
+ if (isTvOnly) {
+ dedicatedPlatform = "appletvos";
+ }
+ else if (isMacOnly) {
+ dedicatedPlatform = "osx";
+ }
+ else if (isIOSOnly) {
+ dedicatedPlatform = "ios";
+ }
+ else if (isWatchOnly) {
+ dedicatedPlatform = "watch";
+ }
+ else if (isVisionOnly) {
+ dedicatedPlatform = "xros";
+ }
+ if (!serverData.isNull(dedicatedPlatform)) {
+ return dedicatedPlatform;
+ }
+ // 2. Loop through our preferred ordering of platforms and use the first one that has platformAttributes present.
+ const alternatePlatforms = defaultAttributePlatformOrdering(objectGraph, clientIdentifierOverride);
+ for (const candidatePlatform of alternatePlatforms) {
+ if (mediaPlatformAttributes.hasPlatformAttribute(data, candidatePlatform)) {
+ return candidatePlatform;
+ }
+ }
+ // 3. Catch-All
+ return defaultAttributePlatform(objectGraph);
+ });
+}
+/**
+ * Computes the best attribute platform for a given Media API Marketplace
+ * response. Since Marketplace responses don't contain a top-level
+ * `deviceFamilies` property, this employs an alternative method from
+ * `bestAttributePlatformFromData()` to get the attribute platform.
+ *
+ * @param objectGraph The App Store object graph
+ * @param data The Media API Marketplace response to search for an attribute platform.
+ * @returns The most appropriate attribute platform available for the current client.
+ */
+export function bestAttributePlatformFromMarketplaceData(objectGraph, data) {
+ // 1. Iterate through the client's preferred platform ordering until we
+ // find the first one present in the response.
+ const preferredAttributePlatforms = defaultAttributePlatformOrdering(objectGraph);
+ for (const attributePlatform of preferredAttributePlatforms) {
+ const versionsAttributes = contentAttributeAsDictionary(objectGraph, data, "versionAttributes", attributePlatform);
+ if (serverData.isDefinedNonNullNonEmpty(versionsAttributes)) {
+ return attributePlatform;
+ }
+ }
+ // 2. Catch-All
+ return defaultAttributePlatform(objectGraph);
+}
+/**
+ * The default attribute platform for the current client
+ */
+export function defaultAttributePlatform(objectGraph) {
+ var _a;
+ if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.attributePlatform) {
+ return objectGraph.activeIntent.attributePlatform;
+ }
+ switch (objectGraph.client.deviceType) {
+ case "phone":
+ case "pad":
+ return "ios";
+ case "tv":
+ return "appletvos";
+ case "mac":
+ return "osx";
+ case "watch":
+ return "watch";
+ case "vision":
+ return "xros";
+ default:
+ return null;
+ }
+}
+/**
+ * The preferred ordering to use given our default platform.
+ */
+function defaultAttributePlatformOrdering(objectGraph, clientIdentifierOverride) {
+ const defaultPlatform = defaultAttributePlatform(objectGraph);
+ if (defaultPlatform === null) {
+ // If the `"web"` client is active and there is not an "active intent" to
+ // inform a default platform, fall back to a hard-coded ordering
+ if (objectGraph.client.isWeb) {
+ return ["ios", "osx", "xros", "watch", "appletvos"];
+ }
+ else {
+ return [];
+ }
+ }
+ switch (defaultPlatform) {
+ case "ios":
+ if (clientIdentifierOverride === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifierOverride === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ return ["xros", "ios", "appletvos", "osx"];
+ }
+ else {
+ return ["ios", "appletvos", "osx", "xros"];
+ }
+ case "osx":
+ return ["osx", "ios", "appletvos", "xros"];
+ case "appletvos":
+ return ["appletvos", "ios", "osx", "xros"];
+ case "watch":
+ // Per Hiren Kotadia on 2019-2-26, watch platform attributes will always be under ios.
+ // We're going to promote ios to the head of the search list to speed up Media API
+ // response parsing. We'll keep watch as #2 in the list so that if this changes in
+ // the future, it should mostly just work. -km
+ return ["ios", "watch", "osx", "xros"];
+ case "xros":
+ return ["xros", "ios", "appletvos", "osx"];
+ default:
+ return [defaultPlatform];
+ }
+}
+// region Variant Attributes
+/**
+ * Retrieve the attribute for a specific platform's variant attribute as a dictionary, from custom attributes or standard attributes.
+ * @param data Data to get attribute for.
+ * @param productVariantData Variant data to use when finding item.
+ * @param attributeKey The key to fetch in platform attributes. May be converted to custom attribute key.
+ * @param attributePlatform The platform to fetch attribute for. Defaults to current platform if unspecified.
+ */
+export function customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform) {
+ // Use `customAttributes.${customAttributeKey}` for platform if present
+ const customAttributeKey = mediaAttributes.attributeKeyAsCustomAttributeKey(attributeKey);
+ if (isNothing(customAttributeKey)) {
+ return null;
+ }
+ const customAttributes = contentAttributeAsDictionary(objectGraph, data, "customAttributes", attributePlatform);
+ const allowNondefaultTreatmentInNondefaultPage = mediaAttributes.attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttributeKey);
+ const customAttribute = variantAttributeForKey(objectGraph, customAttributes, productVariantData, customAttributeKey, allowNondefaultTreatmentInNondefaultPage);
+ if (serverData.isDefinedNonNullNonEmpty(customAttribute)) {
+ return serverData.asDictionary(customAttribute);
+ }
+ // Otherwise, use `${attributeKey}` for platform.
+ return contentAttributeAsDictionary(objectGraph, data, attributeKey, attributePlatform);
+}
+/**
+ * Retrieve the attribute for a specific platform's variant attribute as a dictionary, from custom attributes or standard attributes.
+ * @param data Data to get attribute for.
+ * @param productVariantData Variant data to use when finding item.
+ * @param attributeKey The key to fetch in platform attributes when custom attributes are not present.
+ * @param attributePlatform The platform to fetch attribute for. Defaults to current platform if unspecified.
+ */
+export function customAttributeAsString(objectGraph, data, productVariantData, attributeKey, attributePlatform) {
+ // Use `customAttributes.${customAttributeKey}` for platform if present
+ const customAttributeKey = mediaAttributes.attributeKeyAsCustomAttributeKey(attributeKey);
+ if (isNothing(customAttributeKey)) {
+ return null;
+ }
+ const customAttributes = contentAttributeAsDictionary(objectGraph, data, "customAttributes", attributePlatform);
+ const allowNondefaultTreatmentInNondefaultPage = mediaAttributes.attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttributeKey);
+ const customAttribute = variantAttributeForKey(objectGraph, customAttributes, productVariantData, customAttributeKey, allowNondefaultTreatmentInNondefaultPage);
+ if (serverData.isDefinedNonNullNonEmpty(customAttribute)) {
+ return serverData.asString(customAttribute);
+ }
+ // Otherwise, use `${attributeKey}` for platform.
+ return contentAttributeAsString(objectGraph, data, attributeKey);
+}
+// endregion
+//# sourceMappingURL=attributes.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/content.js b/node_modules/@jet-app/app-store/tmp/src/common/content/content.js
new file mode 100644
index 0000000..72b5ae1
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/content.js
@@ -0,0 +1,2820 @@
+import * as validation from "@jet/environment/json/validation";
+import { isNothing, isSome, unwrapOptional as unwrap } from "@jet/environment/types/optional";
+import * as models from "../../api/models";
+import * as modelsBase from "../../api/models/base";
+import * as modelsShelves from "../../api/models/shelves";
+import { ads } from "../../api/typings/constants";
+import * as derivedData from "../../foundation/json-parsing/derived-data";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as mediaDataFetching from "../../foundation/media/data-fetching";
+import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes";
+import * as mediaRelationship from "../../foundation/media/relationships";
+import * as mediaUrlBuilder from "../../foundation/media/url-builder";
+import { Parameters, Path, Protocol } from "../../foundation/network/url-constants";
+import * as color from "../../foundation/util/color-util";
+import * as dateUtil from "../../foundation/util/date-util";
+import { unreachable } from "../../foundation/util/errors";
+import { isDefinedNonNullNonEmpty } from "@apple-media-services/media-api";
+import { editorialCardFromData } from "../../foundation/media/associations";
+import * as client from "../../foundation/wrappers/client";
+import * as videoDefaults from "../constants/video-constants";
+import * as filtering from "../filtering";
+import * as lockups from "../lockups/lockups";
+import * as metricsHelpersImpressions from "../metrics/helpers/impressions";
+import * as metricsHelpersLocation from "../metrics/helpers/location";
+import * as metricsHelpersMedia from "../metrics/helpers/media";
+import * as offers from "../offers/offers";
+import * as productPageVariants from "../product-page/product-page-variants";
+import * as artwork from "./artwork/artwork";
+import * as contentAttributes from "./attributes";
+import * as contentDeviceFamily from "./device-family";
+import * as sad from "./sad";
+import { isFeatureEnabledForCurrentUser } from "../util/lottery";
+class RunnabilityInfo {
+ constructor() {
+ this.runsOnIntel = true;
+ this.runsOnAppleSilicon = true;
+ this.requiresRosetta = false;
+ }
+}
+/**
+ * Determines a reasonable artwork use case from a given shelf style
+ *
+ * @param shelfStyle The shelf style to consider
+ */
+export function artworkUseCaseFromShelfStyle(objectGraph, shelfStyle) {
+ switch (shelfStyle) {
+ case "inAppPurchaseLockup":
+ case "appShowcase":
+ case "smallLockup": {
+ return 1 /* ArtworkUseCase.LockupIconSmall */;
+ break;
+ }
+ case "mediumLockup": {
+ return 2 /* ArtworkUseCase.LockupIconMedium */;
+ break;
+ }
+ case "largeLockup": {
+ return 3 /* ArtworkUseCase.LockupIconLarge */;
+ break;
+ }
+ default: {
+ return 0 /* ArtworkUseCase.Default */;
+ }
+ }
+}
+/**
+ * Convert an API artwork object into an Artwork model object.
+ * @param artwork The artwork in API format.
+ * @returns An `Artwork` object.
+ */
+export function artworkFromApiArtwork(objectGraph, artworkData, options) {
+ return validation.context("artworkFromApiArtwork", () => {
+ var _a, _b, _c;
+ const allowingTransparency = serverData.isDefinedNonNull(options.allowingTransparency)
+ ? options.allowingTransparency
+ : false;
+ const useJoeColorDefault = objectGraph.client.isVision || objectGraph.client.isWeb;
+ const withJoeColorPlaceholder = serverData.isDefinedNonNull(options.withJoeColorPlaceholder)
+ ? options.withJoeColorPlaceholder
+ : useJoeColorDefault;
+ const artworkUrl = serverData.asString(artworkData, "url");
+ if (serverData.isNull(artworkUrl)) {
+ return null;
+ }
+ // Whether wide gamut is supported
+ const supportsWideGamut = serverData.asBooleanOrFalse(artworkData, "hasP3");
+ // Add base variant
+ const variants = [
+ artwork.createArtworkVariantForClient(objectGraph, allowingTransparency, supportsWideGamut, options.useCase),
+ ];
+ // Add layered image variant
+ const supportsLayeredImage = serverData.asBooleanOrFalse(artworkData, "supportsLayeredImage");
+ if (supportsLayeredImage && (objectGraph.client.isTV || objectGraph.client.isVision)) {
+ variants.push(artwork.createArtworkVariantForFormat(objectGraph, "lcr", supportsWideGamut, options.useCase));
+ }
+ // Artwork Placeholder Color
+ // If we indicate the image could be transparent then we don't want a placeholder background
+ let placeholderBackgroundColor = null;
+ if (allowingTransparency) {
+ placeholderBackgroundColor = color.named("clear");
+ }
+ else if (withJoeColorPlaceholder) {
+ const joeColorHexSet = joeColorHexSetFromData(artworkData);
+ const placeholderColorHex = (_b = (_a = options.joeColorPlaceholderSelectionLogic) === null || _a === void 0 ? void 0 : _a.call(options, joeColorHexSet)) !== null && _b !== void 0 ? _b : serverData.asString(artworkData, "bgColor");
+ const apiBackgroundColor = color.fromHex(placeholderColorHex);
+ if (!serverData.isNull(apiBackgroundColor)) {
+ placeholderBackgroundColor = apiBackgroundColor;
+ }
+ }
+ // If we don't want clear, joe color, or joe color fails to parse then fall back to default background
+ if (serverData.isNull(placeholderBackgroundColor) && !objectGraph.client.isVision) {
+ placeholderBackgroundColor = color.named("placeholderBackground");
+ }
+ const textColorKey = (_c = options.overrideTextColorKey) !== null && _c !== void 0 ? _c : "textColor1";
+ const apiTextColor = color.fromHex(serverData.asString(artworkData, textColorKey));
+ const artworkModel = new modelsBase.Artwork(artworkUrl, options.overrideWidth || serverData.asNumber(artworkData, "width"), options.overrideHeight || serverData.asNumber(artworkData, "height"), variants);
+ artworkModel.backgroundColor = placeholderBackgroundColor;
+ artworkModel.checksum = serverData.asString(artworkData, "checksum");
+ if (serverData.isDefinedNonNull(apiTextColor)) {
+ artworkModel.textColor = apiTextColor;
+ }
+ if (serverData.isDefinedNonNull(options.style)) {
+ artworkModel.style = options.style;
+ }
+ if (serverData.isDefinedNonNull(options.cropCode)) {
+ artworkModel.crop = options.cropCode;
+ }
+ if (serverData.isDefinedNonNull(options.contentMode)) {
+ artworkModel.contentMode = options.contentMode;
+ }
+ return artworkModel;
+ });
+}
+export function impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions) {
+ return validation.context("impressionableAppIconFromData", () => {
+ const rawArtwork = iconFromData(objectGraph, data, artworkOptions);
+ if (!serverData.isDefinedNonNull(rawArtwork)) {
+ return null;
+ }
+ const icon = new models.ImpressionableArtwork(rawArtwork);
+ const title = mediaAttributes.attributeAsString(data, "name");
+ const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, title, metricsOptions);
+ metricsHelpersImpressions.addImpressionFields(objectGraph, icon, metricsImpressionOptions);
+ return icon;
+ });
+}
+/**
+ * Batch method for `impressionableAppIconFromData`. Doesn't push location stack or increment location counter, matching other behavior with other icon grids.
+ * @param dataCollection Data container array with app data.
+ * @param metricsOptions Metrics blob containing information about page and location.
+ * @returns Array of `ImpressionableArtwork`
+ */
+export function impressionableAppIconsFromDataCollection(objectGraph, dataCollection, metricsOptions, artworkOptions) {
+ return validation.context("impressionableAppIconFromData", () => {
+ const icons = [];
+ if (serverData.isNullOrEmpty(metricsOptions.targetType)) {
+ metricsOptions.targetType = "artwork";
+ }
+ for (const data of dataCollection) {
+ const icon = impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions);
+ if (icon) {
+ icons.push(icon);
+ metricsHelpersLocation.nextPosition(metricsOptions.locationTracker);
+ }
+ }
+ return icons;
+ });
+}
+/**
+ * Defines possible use cases for SearchChartOrCategoryBrick.
+ */
+export var SearchChartOrCategoryBrickUseCase;
+(function (SearchChartOrCategoryBrickUseCase) {
+ SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["seeAllPage"] = 0] = "seeAllPage";
+ SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["categoryBreakout"] = 1] = "categoryBreakout";
+ SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["other"] = 2] = "other";
+})(SearchChartOrCategoryBrickUseCase || (SearchChartOrCategoryBrickUseCase = {}));
+/**
+ * Gets all possible artwork that this chart or category can show
+ * @param objectGraph
+ * @param data
+ * @param isForSeeAllPage Whether or not the chart or category is on the see-all page or not;
+ * this is because the see-all page should always have the `Density1` style
+ * @param style The style of the chart or category that will be rendered
+ * @returns All permutations of artowrk that the chart or category can show
+ */
+export function searchChartOrCategoryArtworkFromData(objectGraph, data, useCase, style) {
+ const artworkPath = "editorialArtwork.searchCategoryBrick";
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath);
+ if (serverData.isNullOrEmpty(artworkData)) {
+ return null;
+ }
+ let artworkStyle = style;
+ if (useCase === SearchChartOrCategoryBrickUseCase.seeAllPage) {
+ artworkStyle = models.GenericSearchPageShelfDisplayStyleDensity.Density1;
+ }
+ /// Crops = [LTR crop, RTL crop]
+ /// ContentModes = [ContentMode for LTR crop, ContentMode for RTL crop]
+ /// Note: These must be the same length
+ let crops = [];
+ let contentModes = [];
+ switch (artworkStyle) {
+ /// Tile
+ case models.GenericSearchPageShelfDisplayStyleDensity.Density1:
+ const width = useCase === SearchChartOrCategoryBrickUseCase.categoryBreakout ? "1191" : "2350";
+ artworkData["width"] = width;
+ artworkData["height"] = "670";
+ crops = ["SCB.ApSCBL01", "SCB.ApSCBL03"];
+ contentModes = [modelsBase.ArtworkContentMode.right, modelsBase.ArtworkContentMode.left];
+ break;
+ /// Pill
+ case models.GenericSearchPageShelfDisplayStyleDensity.Density2:
+ artworkData["width"] = "2482";
+ artworkData["height"] = "670";
+ crops = ["SCB.ApSCBS01", "SCB.ApSCBS02"];
+ contentModes = [modelsBase.ArtworkContentMode.left, modelsBase.ArtworkContentMode.right];
+ break;
+ /// Round
+ case models.GenericSearchPageShelfDisplayStyleDensity.Density3:
+ artworkData["width"] = "670";
+ artworkData["height"] = "670";
+ crops = ["cc"];
+ contentModes = [modelsBase.ArtworkContentMode.scaleAspectFit];
+ break;
+ default:
+ break;
+ }
+ return crops.map((crop, index) => {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode: crop,
+ contentMode: index < contentModes.length ? contentModes[index] : null,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ withJoeColorPlaceholder: true,
+ });
+ });
+}
+/**
+ * Create an icon artwork from the provided data.
+ * @param objectGraph The object graph.
+ * @param data The data object to pull icon data from.
+ * @param artworkOptions The options for creating the artwork.
+ * @param clientIdentifierOverride A client identifier override.
+ * @param productVariantData The product variant data to use to select the icon.
+ * @param attributePlatformOverride An override platform, from which to fetch the icon.
+ * @returns An `Artwork` object representing the icon.
+ */
+export function iconFromData(objectGraph, data, artworkOptions, clientIdentifierOverride, productVariantData, attributePlatformOverride = undefined) {
+ return validation.context("iconFromData", () => {
+ if (!data) {
+ validation.unexpectedNull("ignoredValue", "data");
+ return null;
+ }
+ const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : iconAttributePlatform(objectGraph, data, clientIdentifierOverride);
+ const usePrerenderedIconArtwork = shouldUsePrerenderedIconArtwork(objectGraph);
+ // The preferred client identifier to use when selecting the artwork.
+ // This client identifier here ensures that we always prefer pill artwork for messages and circular artwork for watch / vision.
+ // Unless there's an override specified where another artwork type needs to be used (for example, in developer pages).
+ const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier;
+ // Watch
+ const watchIcon = watchIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, usePrerenderedIconArtwork, attributePlatform);
+ if (isSome(watchIcon)) {
+ return watchIcon;
+ }
+ // Messages
+ const messagesIcon = messagesIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, attributePlatform);
+ if (isSome(messagesIcon)) {
+ return messagesIcon;
+ }
+ // In-App Purchases
+ const iapIcon = inAppPurchaseIconFromData(objectGraph, data, artworkOptions);
+ if (isSome(iapIcon)) {
+ return iapIcon;
+ }
+ // Bundles
+ const bundlesIcon = bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork);
+ if (isSome(bundlesIcon)) {
+ return bundlesIcon;
+ }
+ // Calculate variant data if one wasn't provided from caller.
+ if (serverData.isNull(productVariantData)) {
+ productVariantData = productPageVariants.productVariantDataForData(objectGraph, data);
+ }
+ const artworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "artwork", attributePlatform);
+ // tvOS
+ const tvIcon = tvIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform);
+ if (isSome(tvIcon)) {
+ return tvIcon;
+ }
+ // visionOS
+ const visionIcon = visionIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform);
+ if (isSome(visionIcon)) {
+ return visionIcon;
+ }
+ // macOS & iOS
+ return macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform);
+ });
+}
+/**
+ * Determines if a client is capable of showing pre-rendered icon artwork, and if the relevant
+ * feature / bag flags are enabled.
+ * @param objectGraph Current object graph
+ * @returns True if we should use prerendered icon artwork.
+ */
+export function shouldUsePrerenderedIconArtwork(objectGraph) {
+ const clientSupportsPrerenderedIconArtwork = objectGraph.client.isWatch || objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isWeb;
+ const isEnabledForUser = isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.iconArtworkRolloutRate);
+ return (isEnabledForUser &&
+ objectGraph.bag.enableIconArtwork &&
+ objectGraph.client.isIconArtworkCapable &&
+ clientSupportsPrerenderedIconArtwork);
+}
+function watchIconFromData(objectGraph, data, artworkOptions, clientIdentifier, usePrerenderedIconArtwork, attributePlatform) {
+ if (clientIdentifier !== client.watchIdentifier &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS") &&
+ !objectGraph.client.isWatch) {
+ return null;
+ }
+ // Attempt to use pre-rendered circular icon artwork first, if applicable
+ if (usePrerenderedIconArtwork) {
+ const iconArtworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularIconArtwork");
+ if (isSome(iconArtworkData)) {
+ return artworkFromApiArtwork(objectGraph, iconArtworkData, {
+ ...artworkOptions,
+ style: "roundPrerendered",
+ cropCode: "bb",
+ withJoeColorPlaceholder: true,
+ });
+ }
+ }
+ // Fallback to the legacy icon artwork
+ const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularArtwork");
+ if (isSome(artworkData)) {
+ const style = usePrerenderedIconArtwork ? "roundPrerendered" : "round";
+ const cropCode = usePrerenderedIconArtwork ? "ic" : undefined;
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: style,
+ cropCode: cropCode,
+ withJoeColorPlaceholder: true,
+ });
+ }
+ return null;
+}
+function messagesIconFromData(objectGraph, data, artworkOptions, clientIdentifier, attributePlatform) {
+ const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data);
+ const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data);
+ const shouldShowMessagesIcon = hasMessagesExtension && (clientIdentifier === client.messagesIdentifier || isHiddenFromSpringboard);
+ const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "ovalArtwork");
+ if (shouldShowMessagesIcon && serverData.isDefinedNonNull(artworkData)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "pill",
+ });
+ }
+ return null;
+}
+function inAppPurchaseIconFromData(objectGraph, data, artworkOptions) {
+ if (data.type !== "in-apps") {
+ return null;
+ }
+ const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork");
+ if (isSome(artworkData)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "iap",
+ });
+ }
+ return null;
+}
+function bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork) {
+ if (data.type !== "app-bundles") {
+ return null;
+ }
+ // Attempt to use pre-rendered icon artwork first, if applicable
+ if (usePrerenderedIconArtwork) {
+ const iconArtworkData = mediaAttributes.attributeAsDictionary(data, "iconArtwork");
+ if (isSome(iconArtworkData)) {
+ return artworkFromApiArtwork(objectGraph, iconArtworkData, {
+ ...artworkOptions,
+ style: "roundedRectPrerendered",
+ cropCode: "bb",
+ });
+ }
+ }
+ // Fallback to the legacy icon artwork
+ const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork");
+ if (isSome(artworkData)) {
+ const style = usePrerenderedIconArtwork ? "roundedRectPrerendered" : "roundedRect";
+ const cropCode = usePrerenderedIconArtwork ? "ia" : undefined;
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: style,
+ cropCode: cropCode,
+ allowingTransparency: true,
+ });
+ }
+ return null;
+}
+function tvIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) {
+ if (attributePlatform !== "appletvos" && clientIdentifier !== client.tvIdentifier) {
+ return null;
+ }
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "tvRect",
+ });
+}
+function visionIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) {
+ if (attributePlatform !== "xros" && clientIdentifier !== "VisionAppStore" /* ClientIdentifier.VisionAppStore */) {
+ return null;
+ }
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: "round",
+ });
+}
+function macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform) {
+ const isMac = attributePlatform === "osx";
+ const allowTransparency = isMac && !preprocessor.GAMES_TARGET;
+ // Attempt to use pre-rendered icon artwork first, if applicable
+ if (usePrerenderedIconArtwork) {
+ const iconArtworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "iconArtwork", attributePlatform);
+ if (isSome(iconArtworkData)) {
+ return artworkFromApiArtwork(objectGraph, iconArtworkData, {
+ ...artworkOptions,
+ style: "roundedRectPrerendered",
+ cropCode: "bb",
+ allowingTransparency: allowTransparency,
+ });
+ }
+ }
+ // Fallback to the standard icon artwork
+ let style;
+ let cropCode;
+ if (usePrerenderedIconArtwork) {
+ style = "roundedRectPrerendered";
+ cropCode = isMac ? "ib" : "ia";
+ }
+ else {
+ style = isMac ? "unadorned" : "roundedRect";
+ cropCode = "bb";
+ }
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ ...artworkOptions,
+ style: style,
+ cropCode: cropCode,
+ allowingTransparency: allowTransparency,
+ });
+}
+/**
+ * Determines the best attribute platform to use for the icon.
+ * @param objectGraph Current object graph
+ * @param data The product data
+ * @param clientIdentifierOverride The client identifier override to use, if any
+ * @returns
+ */
+export function iconAttributePlatform(objectGraph, data, clientIdentifierOverride) {
+ switch (clientIdentifierOverride) {
+ case client.watchIdentifier:
+ case client.messagesIdentifier: {
+ return "ios";
+ }
+ case client.tvIdentifier: {
+ return "appletvos";
+ }
+ case "VisionAppStore" /* ClientIdentifier.VisionAppStore */: {
+ return "xros";
+ }
+ default: {
+ return contentAttributes.bestAttributePlatformFromData(objectGraph, data, clientIdentifierOverride);
+ }
+ }
+}
+/**
+ * Determine the media platform, given the app platform and screenshot type.
+ * @param appPlatform The app platform specific to this media.
+ * @param type The response screenshot type, which is applicable for both screenshots and trailers, for this media.
+ * @param supplementaryAppPlatforms
+ * @returns {MediaPlatform} The configured media platform object.
+ * TODO: legacy_export
+ */
+export function mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type, supplementaryAppPlatforms) {
+ if (!appPlatform) {
+ return null;
+ }
+ const systemImageName = systemImageNameForAppPlatform(appPlatform);
+ const deviceCornerRadius = deviceCornerRadiusFactorForMediaType(objectGraph, type);
+ const deviceBorderThickness = deviceBorderThicknessForMediaType(objectGraph, type);
+ const outerDeviceCornerRadius = deviceOuterCornerRadiusFactorForMediaType(objectGraph, type);
+ return new modelsBase.MediaPlatform(appPlatform, type, systemImageName, supplementaryAppPlatforms, deviceCornerRadius, deviceBorderThickness, outerDeviceCornerRadius);
+}
+/**
+ * Configures the trailers object from the platform data.
+ * @param data The platform data to use.
+ * @param videoConfiguration config to use for the trailers
+ * @param metricsOptions The metrics options to use.
+ * @param adamId The adamId for the lockup.
+ * @param isAd Whether the trailers are for an ad lockup. Defaults to false.
+ * @param cropCode The crop code to use for the video preview.
+ * @returns {Trailers} The configured trailers object.
+ */
+export function trailersFromData(objectGraph, data, videoConfiguration, metricsOptions, adamId, isAd = false, cropCode) {
+ const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoConfiguration, null, null, isAd, cropCode);
+ if (!platformVideos) {
+ return null;
+ }
+ const videoPreviews = platformVideos.videos;
+ const trailerVideos = [];
+ if (videoPreviews && videoPreviews.length > 0) {
+ for (const trailerVideo of videoPreviews) {
+ metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, trailerVideo, {
+ ...metricsOptions,
+ id: adamId,
+ });
+ trailerVideos.push(trailerVideo);
+ }
+ }
+ let trailers = null;
+ if (trailerVideos.length > 0) {
+ trailers = new modelsShelves.Trailers();
+ trailers.videos = trailerVideos;
+ trailers.mediaPlatform = platformVideos.mediaPlatform;
+ }
+ return trailers;
+}
+/**
+ * A convenience class for encapsulating a `Video` that is tied to a specific `MediaPlatform`.
+ */
+class PlatformVideos {
+ constructor(videos, mediaPlatform) {
+ this.videos = videos;
+ this.mediaPlatform = mediaPlatform;
+ }
+}
+/**
+ * Finds the best platform video previews to use for the given parameters.
+ * @param data The data from which to derive the platform videos.
+ * @param videoConfiguration A video configuration to use for the videos
+ * @param includedAppPlatforms If provided, restricts the resulting platform videos to only these platforms
+ * @param productVariantData
+ * @param isAd Whether the video preview data is for an ad. Defaults to false.
+ * @param cropCode The crop code to use for the preview artwork.
+ * @returns The best available platform videos.
+ */
+export function platformVideoPreviewFromData(objectGraph, data, videoConfiguration, includedAppPlatforms = null, productVariantData = null, isAd = false, cropCode) {
+ return validation.context("platformVideoPreviewFromData", () => {
+ if (serverData.isNull(productVariantData)) {
+ productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // create variant data if not provided.
+ }
+ const videoPreviewsByTypeData = videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd);
+ const videoPreviewsByType = {};
+ if (!videoPreviewsByTypeData) {
+ return null;
+ }
+ let sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, objectGraph.host.clientIdentifier, objectGraph.client.deviceType);
+ if (serverData.isDefinedNonNull(includedAppPlatforms)) {
+ // If we have a restricted set of included app platforms, use those platforms
+ // to build our sortedAppPlatforms array in the proper sort order
+ const includedSortedAppPlatforms = [];
+ for (const appPlatform of sortedAppPlatforms) {
+ if (includedAppPlatforms.includes(appPlatform)) {
+ includedSortedAppPlatforms.push(appPlatform);
+ }
+ }
+ sortedAppPlatforms = includedSortedAppPlatforms;
+ }
+ if (sortedAppPlatforms.length === 0) {
+ return null;
+ }
+ for (const appPlatform of sortedAppPlatforms) {
+ const types = mediaTypesForAppPlatform(objectGraph, appPlatform, objectGraph.client.screenSize);
+ for (const type of Object.keys(videoPreviewsByTypeData)) {
+ const videosDataForType = serverData.asArrayOrEmpty(videoPreviewsByTypeData, type);
+ const videosForType = [];
+ for (const video of videosDataForType) {
+ const previewFrame = serverData.asDictionary(video, "previewFrame");
+ if (!previewFrame) {
+ validation.unexpectedNull("ignoredValue", "object", `videoPreviewsByType.${type}.previewFrame`);
+ continue;
+ }
+ const videoUrl = serverData.asString(video, "video");
+ if (!videoUrl) {
+ validation.unexpectedNull("ignoredValue", "string", `videoPreviewsByType.${type}.video`);
+ continue;
+ }
+ const preview = artwork.createArtworkForResource(objectGraph, serverData.asString(previewFrame, "url"), serverData.asNumber(previewFrame, "width"), serverData.asNumber(previewFrame, "height"), null, null, serverData.asString(previewFrame, "checksum"));
+ if (serverData.isDefinedNonNull(cropCode)) {
+ preview.crop = cropCode;
+ }
+ videosForType.push(new modelsBase.Video(videoUrl, preview, videoConfiguration));
+ }
+ videoPreviewsByType[type] = videosForType;
+ }
+ for (const type of types) {
+ if (videoPreviewsByType[type]) {
+ return new PlatformVideos(videoPreviewsByType[type], mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type));
+ }
+ }
+ }
+ return null;
+ });
+}
+/**
+ * Configures the videos from some platform data.
+ * @param data The store platform data.
+ * @returns A list of `Video` objects.
+ */
+export function videoPreviewsFromData(objectGraph, data) {
+ return validation.context("videoPreviewsFromApiPlatformData", () => {
+ const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoDefaults.defaultVideoConfiguration(objectGraph));
+ if (platformVideos) {
+ return platformVideos.videos;
+ }
+ else {
+ return [];
+ }
+ });
+}
+/**
+ * Determines the `AppPlatform` to use, in order to determine appropriate `MediaType` for media.
+ * @param {AppPlatform} appPlatform The underlying `AppPlatform`.
+ * @returns {AppPlatform} The `AppPlatform` that we map to in order to select the appropriate `MediaType`.
+ */
+function selectionAppPlatformFromAppPlatform(objectGraph, appPlatform) {
+ if (appPlatform === "messages") {
+ switch (objectGraph.client.deviceType) {
+ case "pad": {
+ return "pad";
+ }
+ default: {
+ return "phone";
+ }
+ }
+ }
+ return appPlatform;
+}
+/**
+ * Provide the caller with an ordered array of screenshots for a given context. The first object can be used in search
+ * lockups, and the array will only contain screenshots for the supported app platforms.
+ *
+ * @param data The api product data (containing supported app platforms and screenshots)
+ * @param useCase
+ * @param includedAppPlatforms Optionally, a list of app platforms to confine the screenshots to.
+ * @param clientIdentifierOverride
+ * @param productVariantData
+ * @param isAd Whether the screenshots are being gathered for an ad lockup. Defaults to false.
+ * @returns An ordered array of screenshots for display on a product page
+ * */
+export function screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms = null, clientIdentifierOverride, productVariantData, isAd = false, cropCode) {
+ return validation.context("screenshotsFromData", () => {
+ const screenshots = [];
+ if (serverData.isNull(productVariantData)) {
+ productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // resolve if not resolved by caller.
+ }
+ let sortedAppPlatforms = includedAppPlatforms;
+ if (!sortedAppPlatforms || sortedAppPlatforms.length === 0) {
+ const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier;
+ let preferredDeviceType = objectGraph.client.deviceType;
+ if (preferredClientIdentifier === client.watchIdentifier) {
+ preferredDeviceType = "watch";
+ }
+ if (clientIdentifierOverride === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifierOverride === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ preferredDeviceType = "vision";
+ }
+ sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, preferredClientIdentifier, preferredDeviceType);
+ }
+ for (const appPlatform of sortedAppPlatforms) {
+ const supplementaryAppPlatforms = [];
+ let screenshotData;
+ if (appPlatform === "messages") {
+ screenshotData = messagesScreenshotsFromData(objectGraph, data, "ios");
+ if (supportsFunCameraFromData(objectGraph, data, "ios")) {
+ supplementaryAppPlatforms.push("faceTime");
+ }
+ }
+ else if (appPlatform === "tv" && !objectGraph.host.isTV) {
+ // For tvOS screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "appletvos");
+ }
+ else if (appPlatform === "vision" && !objectGraph.host.isVision) {
+ // For visionOS screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "xros");
+ }
+ else if (appPlatform === "mac" && !objectGraph.host.isMac) {
+ // For Mac screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "osx");
+ }
+ else if ((appPlatform === "phone" || appPlatform === "pad" || appPlatform === "watch") &&
+ !objectGraph.host.isiOS &&
+ !objectGraph.host.isWatch) {
+ // For iPhone / iPad / watch screenshots displayed on other platforms.
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "ios");
+ }
+ else {
+ screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd);
+ }
+ if (!screenshotData) {
+ continue;
+ }
+ const bestScreenshots = bestScreenshotData(objectGraph, screenshotData, appPlatform, useCase, supplementaryAppPlatforms, cropCode);
+ if (bestScreenshots) {
+ screenshots.push(bestScreenshots);
+ }
+ }
+ return screenshots;
+ });
+}
+/**
+ * Creates an array of product media from the given screenshots. If videos are desired
+ * to be inserted in the same media row, this must be done elsewhere.
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param screenshots The screenshots objects with which to configure the media.
+ * @return A list of product media objects.
+ */
+function productMediaFromScreenshots(objectGraph, data, screenshots) {
+ const allMedia = [];
+ if (screenshots && screenshots.length > 0) {
+ const allPlatforms = screenshots.map((platformScreenshots) => {
+ return platformScreenshots.mediaPlatform;
+ });
+ for (const screenshotsForPlatform of screenshots) {
+ // Create media items from all the screenshots.
+ const screenshotMediaItems = [];
+ for (const screenshotArtwork of screenshotsForPlatform.artwork) {
+ const screenshotItem = new modelsShelves.ProductMediaItem();
+ screenshotItem.screenshot = screenshotArtwork;
+ screenshotMediaItems.push(screenshotItem);
+ }
+ const platform = screenshotsForPlatform.mediaPlatform;
+ const productMedia = new modelsShelves.ProductMedia(screenshotMediaItems, platform, allPlatforms, descriptionOfMediaPlatform(objectGraph, platform), descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms), placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms));
+ allMedia.push(productMedia);
+ }
+ }
+ return allMedia;
+}
+/**
+ * Build a set of of `ProductMedia` from apps resource
+ * @param data Apps resource data
+ * @param useCase Artwork use case
+ * @param includedAppPlatforms What platforms are included.
+ * @param productVariantData A variant to use. This can be populated as an optimization to avoid re-resolving the same variant data, e.g. in a product page.
+ * @param clientIdentifierOverride
+ */
+export function productMediaFromData(objectGraph, data, useCase, includedAppPlatforms = null, productVariantData = null, clientIdentifierOverride) {
+ const screenshots = screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms, clientIdentifierOverride, productVariantData);
+ return productMediaFromScreenshots(objectGraph, data, screenshots);
+}
+/**
+ * Finds the best screenshot data from a response to use for the given parameters.
+ * @param data The data from which to derive the screenshots.
+ * @param appPlatform The app platform to which the screenshots belong.
+ * @param supplementaryAppPlatforms
+ * @returns The best available screenshots.
+ */
+function bestScreenshotData(objectGraph, data, appPlatform, useCase, supplementaryAppPlatforms, cropCode) {
+ const selectionPlatform = selectionAppPlatformFromAppPlatform(objectGraph, appPlatform);
+ const screenshotTypes = mediaTypesForAppPlatform(objectGraph, selectionPlatform, objectGraph.client.screenSize);
+ let bestScreenshot = null;
+ let bestScreenshotType;
+ for (let i = 0; i < screenshotTypes.length && !serverData.isDefinedNonNullNonEmpty(bestScreenshot); i++) {
+ bestScreenshot = serverData.asArrayOrEmpty(data, screenshotTypes[i]);
+ bestScreenshotType = screenshotTypes[i];
+ }
+ if (serverData.isDefinedNonNullNonEmpty(bestScreenshot)) {
+ const artworks = bestScreenshot.map(function (screenshotArtwork) {
+ return artworkFromApiArtwork(objectGraph, screenshotArtwork, {
+ useCase: useCase,
+ cropCode: cropCode,
+ });
+ });
+ const platform = mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, bestScreenshotType, supplementaryAppPlatforms);
+ const screenshots = new modelsBase.Screenshots(artworks, platform);
+ return screenshots;
+ }
+ return null;
+}
+/**
+ * Returns a list of sorted app platforms for displaying screenshots. This contains the sorting logic for screenshots.
+ *
+ * @param data Server data for the app
+ * @param clientIdentifier Identifier of the current client.
+ * @param deviceType Type of the current device.
+ * @returns A sorted list of AppPlatform values to use when displaying
+ * */
+export function sortedAppPlatformsFromData(objectGraph, data, clientIdentifier, deviceType) {
+ return derivedData.value(data, `sortedAppPlatformsFromData.${clientIdentifier}.${deviceType}`, () => {
+ var _a;
+ const supportedAppPlatforms = supportedAppPlatformsFromData(objectGraph, data);
+ const excludedAppPlatforms = [];
+ let sortedAppPlatforms = [];
+ const addAppPlatformIfPossible = function (appPlatform, excludePlatform) {
+ if (sortedAppPlatforms.indexOf(appPlatform) !== -1) {
+ return;
+ }
+ if (excludedAppPlatforms.indexOf(appPlatform) !== -1) {
+ return;
+ }
+ if (supportedAppPlatforms.indexOf(appPlatform) !== -1) {
+ sortedAppPlatforms.push(appPlatform);
+ if (excludePlatform) {
+ excludedAppPlatforms.push(excludePlatform);
+ }
+ }
+ };
+ // If there is an `AppPlatform` associated with the active `Intent`, give
+ // that first priority
+ if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform) {
+ addAppPlatformIfPossible(objectGraph.activeIntent.appPlatform);
+ }
+ if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ addAppPlatformIfPossible("vision");
+ }
+ // Next, priority is given to the client
+ switch (clientIdentifier) {
+ case client.watchIdentifier: {
+ addAppPlatformIfPossible("watch");
+ break;
+ }
+ case client.messagesIdentifier: {
+ addAppPlatformIfPossible("messages");
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ // Next the current device type
+ switch (deviceType) {
+ case "phone": {
+ addAppPlatformIfPossible("phone");
+ break;
+ }
+ case "pad": {
+ addAppPlatformIfPossible("pad");
+ break;
+ }
+ case "tv": {
+ addAppPlatformIfPossible("tv");
+ break;
+ }
+ case "watch": {
+ addAppPlatformIfPossible("watch");
+ break;
+ }
+ case "mac": {
+ addAppPlatformIfPossible("mac");
+ break;
+ }
+ case "vision": {
+ addAppPlatformIfPossible("vision");
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ // For Apple Silicon and visionOS, prefer iPad platform over iPhone
+ if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ ||
+ clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) {
+ addAppPlatformIfPossible("pad");
+ addAppPlatformIfPossible("phone");
+ }
+ if (objectGraph.appleSilicon.isSupportEnabled || objectGraph.client.isVision) {
+ addAppPlatformIfPossible("pad");
+ addAppPlatformIfPossible("phone");
+ }
+ else {
+ addAppPlatformIfPossible("phone");
+ addAppPlatformIfPossible("pad");
+ }
+ addAppPlatformIfPossible("mac");
+ addAppPlatformIfPossible("vision");
+ addAppPlatformIfPossible("tv");
+ addAppPlatformIfPossible("watch");
+ addAppPlatformIfPossible("messages");
+ sortedAppPlatforms = sortedAppPlatforms.filter(function (appPlatform) {
+ return excludedAppPlatforms.indexOf(appPlatform) === -1;
+ });
+ return sortedAppPlatforms;
+ });
+}
+/**
+ * For a given server data, this will return the Game Center features that the app supports
+ *
+ * @param data Server data for the app
+ * @returns An array of supported Game Center features
+ * */
+export function supportedGameCenterFeaturesFromData(data) {
+ var _a;
+ if (isNothing(data)) {
+ return undefined;
+ }
+ return ((_a = derivedData.value(data, "supportedGameCenterFeaturesFromData", () => {
+ const features = [];
+ const supportedGameCenterFeatures = serverData.asArrayOrEmpty(data, "attributes.supportedGameCenterFeatures");
+ if (supportedGameCenterFeatures.includes("achievements")) {
+ features.push("achievements");
+ }
+ if (supportedGameCenterFeatures.includes("challenges")) {
+ features.push("challenges");
+ }
+ if (supportedGameCenterFeatures.includes("leaderboards")) {
+ features.push("leaderboards");
+ }
+ if (supportedGameCenterFeatures.includes("multiplayer-activities")) {
+ features.push("multiplayer-activities");
+ }
+ return features;
+ })) !== null && _a !== void 0 ? _a : undefined);
+}
+/**
+ * For a given server data, returns whether the game is eligible for the Games App
+ * This will default to true since we generally expect apps we view in the Games app to be games.
+ * It will be unusual that this is evaluated to `false`.
+ *
+ * @param data Server data for the app
+ * @returns A boolean indicating whether game is eligible for display
+ * */
+export function isEligibleForGamesApp(data) {
+ var _a;
+ if (isNothing(data)) {
+ return true;
+ }
+ return (_a = serverData.asBoolean(data, "attributes.isEligibleForGamesApp")) !== null && _a !== void 0 ? _a : true;
+}
+/**
+ * For a given server data, this will return the platforms that the app supports
+ *
+ * @param data Server data for the app
+ * @returns An array of supported AppPlatforms
+ * */
+export function supportedAppPlatformsFromData(objectGraph, data) {
+ if (!data) {
+ return null;
+ }
+ return derivedData.value(data, "supportedAppPlatformsFromData", () => {
+ const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data, "ios");
+ const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data);
+ const isAppleWatchSupported = isAppleWatchSupportedFromData(objectGraph, data);
+ const serverDeviceFamilies = mediaAttributes.attributeAsArrayOrEmpty(data, "deviceFamilies");
+ const appPlatforms = [];
+ for (const serverDeviceFamily of serverDeviceFamilies) {
+ switch (serverDeviceFamily) {
+ case "iphone":
+ if (!isHiddenFromSpringboard) {
+ appPlatforms.push("phone");
+ }
+ break;
+ case "ipad":
+ if (!isHiddenFromSpringboard) {
+ appPlatforms.push("pad");
+ }
+ break;
+ case "tvos":
+ appPlatforms.push("tv");
+ break;
+ case "watch":
+ appPlatforms.push("watch");
+ break;
+ case "realityDevice":
+ appPlatforms.push("vision");
+ break;
+ default:
+ break;
+ }
+ }
+ if (hasMessagesExtension) {
+ appPlatforms.push("messages");
+ }
+ if (isAppleWatchSupported) {
+ appPlatforms.push("watch");
+ }
+ if (contentDeviceFamily.dataHasDeviceFamily(objectGraph, data, "mac")) {
+ appPlatforms.push("mac");
+ }
+ return appPlatforms;
+ });
+}
+/**
+ * Returns a localized, user-friendly description of all media platforms. This may be a comma delimited
+ * list of the platforms (including supplementary platforms), or it may be 'Only for ___', depending
+ * on the context.
+ *
+ * The localization keys used by this function are defined natively, and are updated using
+ * `tools/platform-media-localizations.py`. If the key doesn't exist, then the script needs to
+ * be updated to add the new combination/order of platforms.
+ *
+ * For failed attempts to localize the string, this function will fallback to a default order that is
+ * guaranteed to exist.
+ *
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param allPlatforms The list of platforms to describe.
+ * @returns The friendly description of all platforms.
+ */
+export function descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms) {
+ if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) {
+ const platform = allPlatforms[0];
+ const platformKey = platform.appPlatform.toUpperCase();
+ return objectGraph.loc.string(`ONLY_FOR_${platformKey}_APP`);
+ }
+ // Flatten all platform partial keys, including their supplementary platforms
+ let keys = allPlatforms.reduce((partialResult, platform) => partialResult.concat(platformLocalizationKeys(platform)), []);
+ try {
+ // Attempt to localize the constructed key
+ return objectGraph.loc.tryString(`PLATFORMS_${keys.join("_")}`);
+ }
+ catch (error) {
+ // If the key does not exist, a best attempt fallback string will be provided.
+ const fallbackOrder = ["PHONE", "PAD", "MAC", "VISION", "TV", "WATCH", "MESSAGES", "FACETIME"];
+ keys = fallbackOrder.filter((key) => keys.includes(key));
+ return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`);
+ }
+}
+/**
+ * Determines where to place the all platforms description, which is visible when the product media is collapsed, or there is only one platform.
+ * This is only used by iOS, visionOS & macOS. For tvOS & watchOS, we always put the media description at the bottom.
+ *
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param allPlatforms The list of platforms to describe.
+ * @returns Where to place the all platforms description.
+ */
+export function placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms) {
+ if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) {
+ return "top";
+ }
+ else {
+ return "bottom";
+ }
+}
+/**
+ * Determines whether we want to use the 'Only for ___' text to describe `allPlatforms`.
+ *
+ * @param objectGraph The object graph.
+ * @param data Apps resource data.
+ * @param allPlatforms The list of platforms to describe.
+ * @returns Whether we want to use 'Only for ___' text to describe `allPlatforms`.
+ */
+function shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms) {
+ if (allPlatforms.length === 1) {
+ const platform = allPlatforms[0];
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ const isRunnableOnCurrentDevice = supportsPlatform(runnableAppPlatforms, platform.appPlatform);
+ const noSupplementaryPlatforms = platform.supplementaryAppPlatforms.length === 0;
+ const isForDifferentDevice = platform.appPlatform !== objectGraph.client.deviceType;
+ if (noSupplementaryPlatforms && isForDifferentDevice && !isRunnableOnCurrentDevice) {
+ return true;
+ }
+ }
+ return false;
+}
+/**
+ * Returns a localized description of a media platform, including any supplementary platforms.
+ * e.g. 'Mac' or 'iMessage, FaceTime'.
+ *
+ * @param objectGraph Object graph, used for localizing the string.
+ * @param allPlatforms The platform to describe.
+ * @returns The friendly description of the platform.
+ */
+export function descriptionOfMediaPlatform(objectGraph, platform) {
+ const keys = platformLocalizationKeys(platform);
+ return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`);
+}
+/**
+ * Returns an array of partial loc keys that represent a media platform.
+ * This consists the media's app platform + any supplementary platforms.
+ * e.g. ["MAC"] or ["MESSAGES", "FACETIME"]
+ *
+ * @param platform The media platform.
+ * @returns The list of partial loc key that represent the media platform.
+ */
+function platformLocalizationKeys(platform) {
+ const appPlatformKey = platform.appPlatform.toUpperCase();
+ const supplementaryPlatformKeys = platform.supplementaryAppPlatforms.map((supplementaryPlatform) => supplementaryPlatform.toUpperCase());
+ return [appPlatformKey].concat(supplementaryPlatformKeys);
+}
+/**
+ * Determines if a given app has a compatible iOS binary for this client.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries
+ * @returns {boolean} True when the app and device are halva.
+ */
+export function supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary) {
+ let isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible");
+ // Override for News in Moltres on Mac
+ if (preprocessor.GAMES_TARGET && data.id === "1066498020" && objectGraph.client.deviceType === "mac") {
+ isIOSBinaryMacOSCompatible = true;
+ }
+ return doesClientSupportMacOSCompatibleIOSBinary && isIOSBinaryMacOSCompatible;
+}
+/**
+ * Determines if a given app has a compatible iOS binary for the current client.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @returns {boolean} True when the app and device are visionOS.
+ */
+export function supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data) {
+ return (objectGraph.client.isVision &&
+ mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible"));
+}
+/**
+ * Determines if a given app has a compatible iOS binary for arbitrary clients.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @returns {boolean} True when the app can run on visionOS.
+ */
+export function supportsVisionOSCompatibleIOSBinaryOnAnyClient(data) {
+ return mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible");
+}
+/**
+ * Determines app binary traits.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @returns {string[]} The app binary traits.
+ */
+export function appBinaryTraitsFromData(objectGraph, data) {
+ if (!objectGraph.client.isiOS) {
+ return undefined;
+ }
+ let appBinaryTraits;
+ if (objectGraph.isAvailable(ads) &&
+ ["debug", "internal"].includes(objectGraph.client.buildType) &&
+ isSome(objectGraph.ads.fetchAppBinaryTraitsOverride)) {
+ // use client override for debugging internal builds
+ appBinaryTraits = objectGraph.ads.fetchAppBinaryTraitsOverride();
+ }
+ if (isNothing(appBinaryTraits)) {
+ // parse from server response
+ appBinaryTraits = mediaPlatformAttributes.platformAttributeAsArrayOrEmpty(data, "ios", "appBinaryTraits");
+ }
+ return appBinaryTraits;
+}
+/**
+ * Determines whether the product has external browser engine.
+ * @param objectGraph Current object graph
+ * @param data The product data
+ * @returns True if the product has external browser engine
+ */
+export function hasExternalBrowserForData(objectGraph, data) {
+ var _a;
+ const appBinaryTraits = appBinaryTraitsFromData(objectGraph, data);
+ const externalBrowserTraits = new Set(["uses-non-webkit-browser-engine", "is-custom-browser-engine-app"]);
+ return (_a = appBinaryTraits === null || appBinaryTraits === void 0 ? void 0 : appBinaryTraits.some((trait) => externalBrowserTraits.has(trait))) !== null && _a !== void 0 ? _a : false;
+}
+/**
+ * Determines minimum os version
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @param {boolean} isClientHalva Whether the client is halva.
+ * @returns {string} The minimum OS version.
+ */
+export function minimumOSVersionFromData(objectGraph, data, isClientHalva) {
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (supportsMacOSCompatibleIOSBinary) {
+ const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumMacOSVersion");
+ if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) {
+ return minimumOSVersion;
+ }
+ }
+ else if (supportsVisionOSCompatibleIOSBinary) {
+ const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumXROSVersion");
+ if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) {
+ return minimumOSVersion;
+ }
+ }
+ const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data);
+ return mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "minimumOSVersion");
+}
+/**
+ * Determines required capabilities for device.
+ *
+ * @param {mediaDataStructure.Data} data The product data to use.
+ * @param {boolean} isClientHalva Whether the client is halva.
+ * @returns {string} The device capabilities to use.
+ */
+export function requiredCapabilitiesFromData(objectGraph, data, isClientHalva) {
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (supportsMacOSCompatibleIOSBinary) {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "macRequiredCapabilities");
+ }
+ else if (supportsVisionOSCompatibleIOSBinary) {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilitiesForRealityDevice");
+ }
+ else {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilities");
+ }
+}
+/**
+ * Returns the app platforms you can buy for on the given device.
+ *
+ * @param objectGraph The current object graph
+ * @param data The data for the app in question
+ * @param device The device type to check
+ * @param supportsMacOSCompatibleIOSBinary Whether device and app supports macOS compatible iOS binary
+ * @param supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @returns An array of supported app platforms
+ */
+function buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ let systemApps;
+ switch (device) {
+ case "phone":
+ systemApps = sad.systemApps(objectGraph);
+ if (isSome(data) && systemApps.isSystemAppFromData(data)) {
+ return ["phone", "watch", "messages"];
+ }
+ else {
+ return ["phone", "watch", "messages", "tv", "vision"];
+ }
+ case "pad":
+ systemApps = sad.systemApps(objectGraph);
+ if (isSome(data) && systemApps.isSystemAppFromData(data)) {
+ return ["phone", "pad", "messages"];
+ }
+ else {
+ return ["phone", "pad", "messages", "tv", "vision"];
+ }
+ case "tv":
+ return ["tv"];
+ case "watch":
+ return ["watch"];
+ case "mac":
+ if (supportsMacOSCompatibleIOSBinary) {
+ return ["mac", "phone", "pad"];
+ }
+ else {
+ return ["mac"];
+ }
+ case "vision":
+ if (supportsVisionOSCompatibleIOSBinary) {
+ return ["vision", "phone", "pad"];
+ }
+ else {
+ return ["vision"];
+ }
+ default:
+ return [];
+ }
+}
+/**
+ * Returns the app platforms you can preorder on for the given device.
+ *
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @returns {models.AppPlatform[]} An array of supported app platforms
+ */
+function preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ switch (device) {
+ case "phone":
+ return ["phone", "watch", "messages"];
+ case "pad":
+ return ["phone", "pad", "messages"];
+ case "tv":
+ return ["tv"];
+ case "watch":
+ return ["watch"];
+ case "mac":
+ if (supportsMacOSCompatibleIOSBinary) {
+ return ["mac", "phone", "pad"];
+ }
+ else {
+ return ["mac"];
+ }
+ case "vision":
+ if (supportsVisionOSCompatibleIOSBinary) {
+ return ["vision", "phone", "pad"];
+ }
+ else {
+ return ["vision"];
+ }
+ default:
+ return [];
+ }
+}
+/**
+ * Returns the app platforms you can run on the given device.
+ *
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @returns {models.AppPlatform[]} An array of supported app platforms
+ */
+export function runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ switch (device) {
+ case "phone":
+ return ["phone", "messages"];
+ case "pad":
+ return ["phone", "pad", "messages"];
+ case "tv":
+ return ["tv"];
+ case "watch":
+ return ["watch"];
+ case "mac":
+ if (supportsMacOSCompatibleIOSBinary) {
+ return ["mac", "phone", "pad"];
+ }
+ else {
+ return ["mac"];
+ }
+ case "vision":
+ if (supportsVisionOSCompatibleIOSBinary) {
+ return ["vision", "phone", "pad"];
+ }
+ else {
+ return ["vision"];
+ }
+ default:
+ return [];
+ }
+}
+/**
+ * Determines if a given piece of content supports the provided app platform
+ *
+ * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content
+ * @param {AppPlatform} platform The platform to check
+ * @returns {boolean} True if the platform is supported, false if not
+ */
+export function supportsPlatform(appPlatforms, platform) {
+ return appPlatforms.indexOf(platform) !== -1;
+}
+/**
+ * Determines if a given piece of content is buyable on the provided device.
+ *
+ * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary.
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary
+ * @param {boolean} isMacOSAppBuyableOnDevice Whether a macOS app is buyable on this device (this enables additional criteria for Apple Silicon).
+ * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not
+ */
+export function buyableOnDevice(objectGraph, data, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppBuyableOnDevice = true) {
+ const platforms = buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ // Do any of the platforms supported by the device match any of the content's app platforms?
+ if (!platforms.some((platform) => supportsPlatform(appPlatforms, platform))) {
+ return false;
+ }
+ if (objectGraph.client.isMac && platforms.includes("mac")) {
+ return isMacOSAppBuyableOnDevice;
+ }
+ return true;
+}
+/**
+ * Determines macOS runnability info for apps and bundles on macOS.
+ */
+function macOSRunnabilityInfoFromData(objectGraph, data) {
+ var _a;
+ const runnabilityInfo = new RunnabilityInfo();
+ // Return most permissible runnability for non-macOS platforms.
+ if (objectGraph.client.deviceType !== "mac") {
+ return runnabilityInfo;
+ }
+ // Use media API attributes for non-bundles.
+ if (data.type !== "app-bundles") {
+ runnabilityInfo.runsOnIntel =
+ (_a = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "runsOnIntel", contentAttributes.defaultAttributePlatform(objectGraph))) !== null && _a !== void 0 ? _a : true;
+ runnabilityInfo.runsOnAppleSilicon = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "runsOnAppleSilicon", contentAttributes.defaultAttributePlatform(objectGraph));
+ runnabilityInfo.requiresRosetta = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresRosetta", contentAttributes.defaultAttributePlatform(objectGraph));
+ return runnabilityInfo;
+ }
+ const bundleAppsData = mediaRelationship.relationshipCollection(data, "apps");
+ // Return most permissible runnability when there no children available
+ if (bundleAppsData.length === 0) {
+ return runnabilityInfo;
+ }
+ // Synthesize runnability info from bundle apps
+ for (const appData of bundleAppsData) {
+ if (serverData.isNull(appData.attributes)) {
+ continue;
+ }
+ const appRunnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, appData);
+ runnabilityInfo.runsOnIntel = runnabilityInfo.runsOnIntel && appRunnabilityInfo.runsOnIntel;
+ runnabilityInfo.runsOnAppleSilicon =
+ runnabilityInfo.runsOnAppleSilicon && appRunnabilityInfo.runsOnAppleSilicon;
+ runnabilityInfo.requiresRosetta = runnabilityInfo.requiresRosetta || appRunnabilityInfo.requiresRosetta;
+ }
+ return runnabilityInfo;
+}
+/**
+ * Determines if a given macOS app is buyable on this device.
+ *
+ */
+export function isMacOSAppBuyableAndRunnableFromData(objectGraph, data, isAppleSiliconSupportEnabled, isRosettaAvailable) {
+ const runnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, data);
+ if (isAppleSiliconSupportEnabled) {
+ return (runnabilityInfo.runsOnAppleSilicon &&
+ (!runnabilityInfo.requiresRosetta || (runnabilityInfo.requiresRosetta && isRosettaAvailable)));
+ }
+ else {
+ return runnabilityInfo.runsOnIntel;
+ }
+}
+/**
+ * Determines if a given piece of content is preorderable on the provided device.
+ *
+ * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content
+ * @param {DeviceType} device The device type to check
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary.
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary.
+ * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not
+ */
+export function preorderableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) {
+ const platforms = preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ // Do any of the platforms supported by the device match any of the content's app platforms?
+ return platforms.some((platform) => supportsPlatform(appPlatforms, platform));
+}
+/**
+ * Determines if any of a given array of app platforms can be run on the provided device.
+ *
+ * @param appPlatforms The app platforms supported by a piece of content.
+ * @param device The device type to check.
+ * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary.
+ * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary.
+ * @returns `true` if any of the app platforms can be run on the given device; `false` otherwise.
+ */
+export function runnableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppRunnableOnDevice = true) {
+ const runnablePlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ // Do any of the platforms supported by the device match any of the content's app platforms?
+ if (!runnablePlatforms.some((platform) => supportsPlatform(appPlatforms, platform))) {
+ return false;
+ }
+ if (objectGraph.client.isMac && appPlatforms.includes("mac")) {
+ return isMacOSAppRunnableOnDevice;
+ }
+ return true;
+}
+/**
+ * Determines if a given piece of content is runnable on the provided device.
+ *
+ * @param data The product data to use.
+ * @param device
+ * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries
+ * @returns {boolean} True if the product can be run on the provided device, false if not
+ */
+export function runnableOnDeviceWithData(objectGraph, data, device, doesClientSupportMacOSCompatibleIOSBinary) {
+ // (1) Required capabilities mismatch
+ if (!lockups.deviceHasCapabilitiesFromData(objectGraph, data)) {
+ return false;
+ }
+ // (2) 32-bit only, unsupported deletable system app, doesn't meet minimum OS requirements, or doesn't support current platform
+ // Note that Filter.UnsupportedPlatform only checks if the product is buyable, not runnable
+ const filter = 2 /* filtering.Filter.ThirtyTwoBit */ |
+ 4 /* filtering.Filter.UnsupportedSystemDeletableApps */ |
+ 512 /* filtering.Filter.MinimumOSRequirement */ |
+ 128 /* filtering.Filter.UnsupportedPlatform */ |
+ 8192 /* filtering.Filter.MacOSRosetta */;
+ if (filtering.shouldFilter(objectGraph, data, filter)) {
+ return false;
+ }
+ // (3) Finally, check if any of the product platforms are supported on this device
+ const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary);
+ const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary);
+ const productAppPlatforms = supportedAppPlatformsFromData(objectGraph, data);
+ return runnableAppPlatforms.some((platform) => supportsPlatform(productAppPlatforms, platform));
+}
+/**
+ * Determines which screenshot keys (MediaType) we need to use to pull the appropriate screenshots
+ * from the server data.
+ *
+ * @param appPlatform The app platform requested
+ * @param screenSize The size of the screen being used to display the screenshots
+ * @returns An array of ScreenshotType strings that can be used on the server data
+ * */
+export function mediaTypesForAppPlatform(objectGraph, appPlatform, screenSize) {
+ switch (appPlatform) {
+ case "mac": {
+ return ["mac"];
+ }
+ case "watch": {
+ if (screenSize.isEqualTo(screenSizeWatchUltra) || screenSize.isEqualTo(screenSizeN230)) {
+ // 2022 is the preferred dropwell for Ultra devices
+ return ["appleWatch_2022", "appleWatch_2024", "appleWatch_2021", "appleWatch_2018", "appleWatch"];
+ }
+ else {
+ return ["appleWatch_2024", "appleWatch_2022", "appleWatch_2021", "appleWatch_2018", "appleWatch"];
+ }
+ }
+ case "tv": {
+ return ["appleTV"];
+ }
+ case "vision": {
+ return ["appleVisionPro"];
+ }
+ case "pad": {
+ const types = [];
+ if ((screenSize.isEqualTo(screenSizeIPadPro2018) ||
+ screenSize.isEqualTo(screenSizeIPadPro2018Landscape) ||
+ screenSize.isEqualTo(screenSizeJ720) ||
+ screenSize.isEqualTo(screenSizeJ720Landscape)) &&
+ objectGraph.client.screenCornerRadius > 0.0) {
+ types.push("ipadPro_2018");
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPadPro)) {
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ types.push("ipad_11");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPad11) ||
+ screenSize.isEqualTo(screenSizeIPad11Landscape) ||
+ screenSize.isEqualTo(screenSizeIPadJ310) ||
+ screenSize.isEqualTo(screenSizeIPadJ310Landscape) ||
+ screenSize.isEqualTo(screenSizeJ717) ||
+ screenSize.isEqualTo(screenSizeJ717Landscape)) {
+ types.push("ipad_11");
+ types.push("ipadPro_2018");
+ types.push("ipadPro");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPad105)) {
+ types.push("ipad_10_5");
+ types.push("ipad");
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPadAir2020)) {
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ types.push("ipad_10_5");
+ types.push("ipad");
+ }
+ else if (screenSize.isEqualTo(screenSizeIPad102)) {
+ types.push("ipad");
+ types.push("ipad_10_5");
+ types.push("ipad_11");
+ types.push("ipadPro");
+ types.push("ipadPro_2018");
+ }
+ else {
+ // Regardless of screen size match, we should add on 'some' iPad.
+ types.push("ipadPro_2018");
+ types.push("ipad_11");
+ types.push("ipad");
+ types.push("ipad_10_5");
+ types.push("ipadPro");
+ }
+ return types;
+ }
+ case "phone": {
+ /** Phone Best Match Policy **
+
+ The best match is given by |B| + |L| + |S|, where:
+ B: Exact type match
+ L: All types larger than the exact type, in increasing order
+ S: All types smaller than the exact type, in decreasing order
+
+ Example:
+ Types for iphone6 == [iphone6, iphone6+, iphone_5_8, iphone5, iphone]
+ Types for iphone5 == [iphone5, iphone6, iphone6+, iphone_5_8, iphone]
+
+ ** */
+ // Grab the exact match.
+ let perfectMatch;
+ if (screenSize.isEqualTo(screenSizeIphone65) || screenSize.isEqualTo(screenSizeIPhone134)) {
+ perfectMatch = "iphone_6_5";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone58) ||
+ screenSize.isEqualTo(screenSizeIPhone131) ||
+ screenSize.isEqualTo(screenSizeIPhone132)) {
+ perfectMatch = "iphone_5_8";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhoneOriginal)) {
+ perfectMatch = "iphone";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone5)) {
+ perfectMatch = "iphone5";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone6)) {
+ perfectMatch = "iphone6";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone6Plus)) {
+ perfectMatch = "iphone6+";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone61) || screenSize.isEqualTo(screenSizeD93)) {
+ perfectMatch = "iphone_d73";
+ }
+ else if (screenSize.isEqualTo(screenSizeIPhone67) ||
+ screenSize.isEqualTo(screenSizeD94) ||
+ screenSize.isEqualTo(screenSizeD23)) {
+ perfectMatch = "iphone_d74";
+ }
+ else {
+ perfectMatch = "iphone_5_8";
+ }
+ // Append remaining types to our exact match.
+ const perfectMatchIndex = decreasingPhoneTypes.indexOf(perfectMatch);
+ const largerTypes = decreasingPhoneTypes.slice(0, perfectMatchIndex);
+ largerTypes.reverse();
+ const smallerTypes = decreasingPhoneTypes.slice(perfectMatchIndex + 1);
+ const perfectMatchArray = [perfectMatch];
+ return perfectMatchArray.concat(largerTypes, smallerTypes);
+ }
+ default: {
+ return [];
+ }
+ }
+}
+export function combinedFileSizeFromData(objectGraph, data) {
+ var _a;
+ if (serverData.isNull(data)) {
+ return null;
+ }
+ // This background asset information is for the work done in SydneyB
+ const backgroundAssetsInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfo");
+ // This background asset information is for the work done in SydneyE
+ const backgroundAssetsInfoWithOptional = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfoWithOptional");
+ const isIOSBinaryCompatibleWithMac = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, true);
+ const isMacOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac");
+ const isWebViewingMac = objectGraph.client.isWeb && ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) === "mac";
+ if ((objectGraph.client.isMac || isWebViewingMac || isMacOnly) && !isIOSBinaryCompatibleWithMac) {
+ const macFileSize = objectGraph.bag.enableProductPageInstallSize
+ ? macInstallSizeInBytesFromData(objectGraph, data)
+ : offers.macFileSizeInBytesFromData(objectGraph, data);
+ if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) {
+ const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(macFileSize, null, null, maxEssentialInstallSizeInBytes);
+ }
+ else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) {
+ const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes");
+ const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(macFileSize, maxDownloadSizeInBytes, maxInstallSizeInBytes, null);
+ }
+ return new modelsBase.CombinedFileSize(macFileSize, null, null, null);
+ }
+ else {
+ /* File Size: Our policy is to rely on thinned variant, device model, and universal (in that order). */
+ const fileSizeByDevice = mediaAttributes.attributeAsDictionary(data, "fileSizeByDevice");
+ if (fileSizeByDevice) {
+ /* thinnedApplicationVariantIdentifier can contain two device names. The preferred device, and a compatible device. */
+ let fileSizeKeys = [];
+ if (objectGraph.client.thinnedApplicationVariantIdentifier) {
+ fileSizeKeys = objectGraph.client.thinnedApplicationVariantIdentifier.split(" ");
+ }
+ fileSizeKeys = fileSizeKeys.concat([objectGraph.host.deviceModel, "universal"]);
+ for (const key of fileSizeKeys) {
+ const fileSizeValue = serverData.asNumber(fileSizeByDevice[key]);
+ if (fileSizeValue) {
+ if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) {
+ const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(fileSizeValue, null, null, maxEssentialInstallSizeInBytes);
+ }
+ else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) {
+ const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes");
+ const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes");
+ return new modelsBase.CombinedFileSize(fileSizeValue, maxDownloadSizeInBytes, maxInstallSizeInBytes, null);
+ }
+ else {
+ return new modelsBase.CombinedFileSize(fileSizeValue, null, null, null);
+ }
+ }
+ }
+ }
+ }
+ return null;
+}
+/**
+ * Extract the file size and unit from a CombinedFileSize object.
+ * @param objectGraph Current object graph
+ * @param combinedFileSize The combined file size object
+ * @returns A FileSizeAndUnit object
+ */
+export function fileSizeAndUnitFromCombinedFileSize(objectGraph, combinedFileSize) {
+ let totalFileSize;
+ if (isSome(combinedFileSize.maxEssentialInstallSizeInBytes)) {
+ totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxEssentialInstallSizeInBytes;
+ }
+ else if (isSome(combinedFileSize.maxInstallSizeInBytes)) {
+ totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxInstallSizeInBytes;
+ }
+ else {
+ totalFileSize = combinedFileSize.fileSizeByDevice;
+ }
+ if (totalFileSize <= 0) {
+ return null;
+ }
+ // We split using all whitespace characters because in some locs a non-breaking space is used.
+ const parts = objectGraph.loc.fileSize(totalFileSize).trim().split(/\s+/);
+ if (parts.length !== 2) {
+ return null;
+ }
+ return {
+ size: parts[0],
+ unit: parts[1],
+ };
+}
+/**
+ * Extracts the install size for a macOS app.
+ * @param objectGraph Current object graph
+ * @param data Product page data
+ * @returns The install size for the Mac binary, in bytes
+ */
+function macInstallSizeInBytesFromData(objectGraph, data) {
+ const deviceData = mediaPlatformAttributes.platformAttributeAsDictionary(data, "osx", "installSizeByDeviceInBytes");
+ if (isNothing(deviceData)) {
+ return null;
+ }
+ // macOS does not support app thinning, so there is only ever one macOS device in this list. Unfortunately
+ // there is no known API that gives us this device name, so we resort to hard-coding for now.
+ const installSizeInBytes = deviceData["Mac"];
+ if (isNothing(installSizeInBytes)) {
+ return null;
+ }
+ return serverData.asNumber(installSizeInBytes);
+}
+/**
+ * Determines the primary langauge locale, from a given list of locales.
+ * @param objectGraph Current object graph
+ * @param locales The list of locales
+ * @returns A single LanguageLocale object, or null
+ */
+export function primaryLanguageLocaleFromLocales(objectGraph, locales) {
+ const languageCount = locales.length;
+ if (languageCount <= 0) {
+ return null;
+ }
+ return {
+ tag: serverData.asString(serverData.traverse(locales, "0.tag")).split("-")[0].toUpperCase(),
+ name: serverData.asString(serverData.traverse(locales, "0.name")),
+ };
+}
+/**
+ * Determines the uber artwork for the product, if there is any.
+ * @param {Data} The data for the product.
+ * @returns {models.Artwork} The artwork for the uber, or `null` if there is none.
+ * null.
+ */
+export function productUberFromData(objectGraph, data, options) {
+ let uberArtworkData;
+ let uberArtworkPath = null;
+ let fallbackUberArtworkPath = null;
+ let cropCode = null;
+ let fallbackCropCode = null;
+ switch (objectGraph.client.deviceType) {
+ case "mac":
+ if (options.supportsArcade) {
+ uberArtworkPath = "editorialArtwork.splashFullScreen";
+ cropCode = "sr";
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.centeredFullscreenBackground";
+ cropCode = "ep";
+ }
+ break;
+ case "tv":
+ if (options.presentedInTopShelf) {
+ uberArtworkPath = "editorialArtwork.topShelf";
+ cropCode = "sr";
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.splashFullScreen";
+ cropCode = "ta";
+ fallbackUberArtworkPath = "editorialArtwork.fullscreenBackground";
+ fallbackCropCode = "sr";
+ }
+ break;
+ case "vision":
+ uberArtworkPath = "editorialArtwork.productUberStatic16x9";
+ cropCode = "sr";
+ break;
+ default:
+ if (options.supportsArcade) {
+ if (options.prefersCompactVariant || objectGraph.client.isPhone) {
+ uberArtworkPath = "editorialArtwork.splashTall";
+ cropCode = "oc";
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.splashFullScreen";
+ cropCode = "oh";
+ }
+ }
+ else {
+ uberArtworkPath = "editorialArtwork.bannerUber";
+ cropCode = "sr";
+ }
+ break;
+ }
+ uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, uberArtworkPath);
+ // If we don't have the desired artwork, we sometimes attempt to use other artwork as a fallback.
+ if (fallbackUberArtworkPath !== null && serverData.isNullOrEmpty(uberArtworkData)) {
+ uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, fallbackUberArtworkPath);
+ // Use the fallback crop if it's available.
+ if (fallbackCropCode !== null) {
+ cropCode = fallbackCropCode;
+ }
+ }
+ if (serverData.isDefinedNonNull(uberArtworkData) && serverData.isDefinedNonNull(cropCode)) {
+ return artworkFromApiArtwork(objectGraph, uberArtworkData, {
+ cropCode,
+ useCase: 21 /* ArtworkUseCase.Uber */,
+ withJoeColorPlaceholder: true,
+ overrideHeight: null,
+ overrideWidth: null,
+ });
+ }
+ return null;
+}
+/**
+ * Determines the logo artwork for the product, if there is any.
+ * @param {Data} The data for the product.
+ * @returns {models.Artwork} The artwork for the uber, or `null` if there is none.
+ * null.
+ */
+export function productLogoArtworkFromData(objectGraph, data) {
+ let artworkPath = null;
+ let cropCode = null;
+ switch (objectGraph.client.deviceType) {
+ case "tv":
+ artworkPath = "editorialArtwork.contentLogoTrimmed";
+ cropCode = "bb";
+ break;
+ default:
+ return null;
+ }
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath);
+ if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ withJoeColorPlaceholder: true,
+ });
+ }
+ return null;
+}
+/**
+ * Determines the editorial video for the product, if there is any.
+ * @returns {models.Video} The editorial video for the product, or `null` if there is none.
+ * null.
+ * @param data
+ * @param useCase
+ * @param preferredFlavorsOverride
+ */
+export function productEditorialVideoFromData(objectGraph, data, useCase, preferredFlavorsOverride, videoPreviewOverride) {
+ let preferredFlavors = [];
+ if (serverData.isDefinedNonNullNonEmpty(preferredFlavorsOverride)) {
+ preferredFlavors = preferredFlavorsOverride;
+ }
+ else {
+ switch (objectGraph.client.deviceType) {
+ case "mac":
+ case "tv":
+ preferredFlavors = ["splashVideo16x9"];
+ break;
+ case "pad":
+ preferredFlavors = ["splashVideo4x3"];
+ break;
+ case "vision":
+ preferredFlavors = ["productUberMotion16x9"];
+ break;
+ default:
+ preferredFlavors = ["splashVideo3x4"];
+ }
+ }
+ let uberEditorialVideoData = null;
+ let videoPreviewData = null;
+ for (const videoFlavor of preferredFlavors) {
+ uberEditorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ videoFlavor,
+ ]);
+ videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ videoFlavor,
+ "previewFrame",
+ ]);
+ if (serverData.isDefinedNonNullNonEmpty(uberEditorialVideoData)) {
+ break;
+ }
+ }
+ // Video Preview based on data, or externally provided override if any.
+ const videoPreview = videoPreviewOverride !== null && videoPreviewOverride !== void 0 ? videoPreviewOverride : artworkFromApiArtwork(objectGraph, videoPreviewData, {
+ useCase: useCase,
+ withJoeColorPlaceholder: true,
+ cropCode: "sr",
+ });
+ if (serverData.isDefinedNonNull(uberEditorialVideoData)) {
+ const videoUrl = serverData.asString(uberEditorialVideoData, "video");
+ if (serverData.isNull(videoUrl)) {
+ return null;
+ }
+ let playbackControls;
+ let autoplayPlaybackControls;
+ if (objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV) {
+ playbackControls = videoDefaults.standardControls(objectGraph);
+ autoplayPlaybackControls = {
+ muteUnmute: true,
+ };
+ }
+ else {
+ playbackControls = {};
+ autoplayPlaybackControls = {};
+ }
+ const configuration = {
+ allowsAutoPlay: true,
+ looping: true,
+ canPlayFullScreen: false,
+ playbackControls: playbackControls,
+ autoPlayPlaybackControls: autoplayPlaybackControls,
+ };
+ return new models.Video(videoUrl, videoPreview, configuration);
+ }
+ return null;
+}
+/**
+ * Determines the video for the poster lockup, if there is any.
+ * @param {Data} The data for the lockup.
+ * @param {useCase} The use case for this artwork.
+ * @returns {models.Video} The video for the poster lockup, or `null` if there is none.
+ * null.
+ */
+export function posterEditorialVideoFromData(objectGraph, data, useCase) {
+ const editorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ "posterCardVideo16x9",
+ ]);
+ const videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [
+ "editorialVideo",
+ "posterCardVideo16x9",
+ "previewFrame",
+ ]);
+ const cropCode = "sr";
+ const videoPreview = artworkFromApiArtwork(objectGraph, videoPreviewData, {
+ useCase: useCase,
+ withJoeColorPlaceholder: true,
+ cropCode: cropCode,
+ });
+ if (serverData.isDefinedNonNull(editorialVideoData)) {
+ const videoUrl = serverData.asString(editorialVideoData, "video");
+ if (serverData.isNull(videoUrl)) {
+ return null;
+ }
+ const configuration = {
+ allowsAutoPlay: true,
+ looping: true,
+ canPlayFullScreen: false,
+ playbackControls: videoDefaults.noControls(objectGraph),
+ autoPlayPlaybackControls: videoDefaults.noControls(objectGraph),
+ };
+ return new models.Video(videoUrl, videoPreview, configuration);
+ }
+ return null;
+}
+/**
+ * Determines the artwork for the poster lockup, if there is any.
+ * @param {Data} The data for the lockup.
+ * @returns {models.Artwork} The artwork for the poster lockup, or `null` if there is none.
+ * null.
+ */
+export function posterArtworkFromData(objectGraph, data) {
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.postCard");
+ const cropCode = "sr";
+ if (serverData.isDefinedNonNull(artworkData)) {
+ return artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ withJoeColorPlaceholder: true,
+ });
+ }
+ return null;
+}
+/**
+ * Determines the artwork for the epic heading on a poster lockup, if there is any.
+ * @param {Data} The data for the product.
+ * @returns {models.Artwork} The artwork for the epic heading, or `null` if there is none.
+ * null.
+ */
+export function posterEpicHeadingArtworkFromData(objectGraph, data) {
+ const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.epicHeading");
+ const cropCode = "bb";
+ if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) {
+ const epicHeadingArtwork = artworkFromApiArtwork(objectGraph, artworkData, {
+ cropCode,
+ useCase: 0 /* ArtworkUseCase.Default */,
+ });
+ if (objectGraph.client.isVision) {
+ epicHeadingArtwork.backgroundColor = color.named("clear");
+ }
+ return epicHeadingArtwork;
+ }
+ return null;
+}
+/**
+ * Fetch the most-landscape media from data. Hoisted from Arcade See All.
+ * Used for:
+ * - Arcade See All Media Lockups
+ * - Continue Playing Lockups
+ */
+export function editorialSplashVideoFromData(objectGraph, data, videoPreviewOverride) {
+ let preferredEditorialVideoFlavors = null;
+ switch (objectGraph.client.deviceType) {
+ case "mac":
+ case "tv":
+ case "phone":
+ case "vision":
+ preferredEditorialVideoFlavors = ["splashVideo16x9", "splashVideo4x3", "splashVideo3x4"];
+ break;
+ default:
+ preferredEditorialVideoFlavors = ["splashVideo4x3", "splashVideo16x9", "splashVideo3x4"];
+ }
+ return productEditorialVideoFromData(objectGraph, data, 21 /* ArtworkUseCase.Uber */, preferredEditorialVideoFlavors, videoPreviewOverride);
+}
+/**
+ * Determines the URL to use for the developer page.
+ * @param {Data} developerData The data for the "developer" relationship.
+ * @returns {string} The string form of the URL for the developer page, or `null` if the developer data is undefined or
+ * null.
+ */
+export function developerUrlFromDeveloperData(objectGraph, developerData) {
+ if (!serverData.isDefinedNonNull(developerData)) {
+ return null;
+ }
+ if (objectGraph.client.isWeb) {
+ return mediaAttributes.attributeAsString(developerData, "url");
+ }
+ return `${Protocol.internal}:/${Path.developer}/${Path.href}?${Parameters.href}=${developerData.href}`;
+}
+/**
+ * Determines the URL to use for the Charts page.
+ * @param {Data} data The data for the product.
+ * @returns {string} The string form of the URL for the charts page, or `null` if the data is undefined or
+ * null.
+ */
+export function chartUrlFromData(objectGraph, genre, chart) {
+ const request = new mediaDataFetching.Request(objectGraph)
+ .forType("charts")
+ .addingQuery("types", "apps")
+ .addingQuery("chart", chart)
+ .addingQuery("genre", genre)
+ .includingMacOSCompatibleIOSAppsWhenSupported(true);
+ return mediaUrlBuilder.buildURLFromRequest(objectGraph, request).toString();
+}
+/**
+ * Returns the key into the chart-position badge data for the given client name.
+ * @param clientIdentifier Identifier of the current client.
+ * @returns {string} The relevant key in the chart-position badge JSON data.
+ */
+export function badgeChartKeyForClientIdentifier(objectGraph, clientIdentifier) {
+ switch (clientIdentifier) {
+ case client.appStoreIdentifier:
+ case client.productPageExtensionIdentifier:
+ return "appStore";
+ case client.watchIdentifier:
+ return "watch";
+ case client.messagesIdentifier:
+ return "messages";
+ case client.tvIdentifier:
+ return "appletv";
+ default:
+ return null;
+ }
+}
+/**
+ * Internal function returning the name and asset name representing the
+ * storefront content rating for the provided rank.
+ * @param objectGraph The App Store object graph.
+ * @param rank A content rating rank from CX.
+ * @returns A tuple containing the name and asset name representing the rank,
+ * or `undefined` if rank is unknown/invalid.
+ */
+function storefrontContentRatingInfoForRank(objectGraph, rank) {
+ switch (rank) {
+ // Brazil Self-Rated
+ case 6:
+ return ["L", "br.l"];
+ case 7:
+ return ["10", "br.10"];
+ case 8:
+ return ["12", "br.12"];
+ case 9:
+ return ["14", "br.14"];
+ case 10:
+ return ["16", "br.16"];
+ case 11:
+ return ["18", "br.18"];
+ // Brazil Official
+ case 12:
+ return ["AL", "br.l.official"];
+ case 13:
+ return ["A10", "br.10.official"];
+ case 14:
+ return ["A12", "br.12.official"];
+ case 15:
+ return ["A14", "br.14.official"];
+ case 16:
+ return ["A16", "br.16.official"];
+ case 17:
+ return ["A18", "br.18.official"];
+ // Korea
+ case 20:
+ return ["All", "kr.all"];
+ case 21:
+ return ["12", "kr.12"];
+ case 22:
+ return ["15", "kr.15"];
+ // Australia
+ case 31:
+ return ["15+", "AgeRating-AU-15"];
+ case 32:
+ return ["R 18+", "AgeRating-AU-18"];
+ // France
+ case 47:
+ return ["18+", "AgeRating-FR-18"];
+ default:
+ return undefined;
+ }
+}
+/// Returns a localized title for the given app platform.
+export function appPlatformTitle(objectGraph, appPlatform) {
+ switch (appPlatform) {
+ case "phone":
+ return objectGraph.loc.string("AppPlatform.Phone");
+ case "pad":
+ return objectGraph.loc.string("AppPlatform.Pad");
+ case "vision":
+ return objectGraph.loc.string("AppPlatform.Vision");
+ case "tv":
+ return objectGraph.loc.string("AppPlatform.TV");
+ case "watch":
+ return objectGraph.loc.string("AppPlatform.Watch");
+ case "messages":
+ return objectGraph.loc.string("AppPlatform.Messages");
+ case "mac":
+ return objectGraph.loc.string("AppPlatform.Mac");
+ default:
+ return "";
+ }
+}
+/**
+ * Provides the name of the asset representing the storefront content rating
+ * for the provided `rank`.
+ * @param objectGraph The App Store object graph.
+ * @param rank A content rating rank from CX.
+ * @returns The asset name representing the `rank`, corresponding to a file on
+ * device, or `undefined` if rank is unknown/invalid.
+ */
+export function storefrontContentRatingResourceForRank(objectGraph, rank) {
+ var _a;
+ return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[1];
+}
+/**
+ * Provides a textual representation of the storefront content rating for the
+ * provided `rank`, e.g. "18+". This should match the main text displayed in
+ * the content rating pictogram from `storefrontContentRatingResourceForRank`.
+ * @param objectGraph The App Store object graph.
+ * @param rank A content rating rank from CX.
+ * @returns The textual version of the storefront content rating representing
+ * the `rank`, or `undefined` if rank is unknown/invalid.
+ */
+export function storefrontContentRatingNameForRank(objectGraph, rank) {
+ var _a;
+ return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[0];
+}
+export function promotionalTextFromData(objectGraph, data, productVariantData) {
+ return contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "promotionalText");
+}
+export function hasMessagesExtensionFromData(objectGraph, data, attributePlatform) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasMessagesExtension", attributePlatform);
+}
+export function supportsFunCameraFromData(objectGraph, data, attributePlatform) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsFunCamera", attributePlatform);
+}
+export function isHiddenFromSpringboardFromData(objectGraph, data) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isHiddenFromSpringboard");
+}
+function isAppleWatchSupportedFromData(objectGraph, data) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isAppleWatchSupported");
+}
+function messagesScreenshotsFromData(objectGraph, data, attributePlatform) {
+ return contentAttributes.contentAttributeAsDictionary(objectGraph, data, "messagesScreenshots", attributePlatform);
+}
+function screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) {
+ const attributeKey = isAd ? "customScreenshotsByTypeForAd" : "screenshotsByType";
+ return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform);
+}
+function videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) {
+ const attributeKey = isAd ? "customVideoPreviewsByTypeForAd" : "videoPreviewsByType";
+ return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform);
+}
+/**
+ * Whether Arcade is supported, based on the provided data.
+ * @param objectGraph The App Store object graph.
+ * @param data The data blob to check for Arcade support.
+ * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @returns A boolean indicating if Arcade is supported.
+ */
+export function isArcadeSupported(objectGraph, data, attributePlatformOverride = undefined) {
+ return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade", attributePlatformOverride);
+}
+/**
+ * Try to get notes for some piece of content, giving preference to the enrichedEditorialnotes, falling back to editorialNotes,
+ * then finally to itunesNotes. For some data the notes are stored in the attributes not the platformAttributes.
+ * @param {Data} data
+ * @param {string} key
+ * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations
+ * * @param attributePlatformOverride An override platform, from which to fetch the attribute.
+ * @returns {string}
+ */
+export function notesFromData(objectGraph, data, key, enableEditorialCardOverrides = false, attributePlatformOverride = undefined) {
+ var _a, _b;
+ if (isNothing(data)) {
+ return null;
+ }
+ let note;
+ if (enableEditorialCardOverrides) {
+ const editorialCard = editorialCardFromData(data);
+ if (mediaAttributes.hasAttributes(editorialCard)) {
+ note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key], attributePlatformOverride);
+ }
+ }
+ note =
+ (_b = (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key], attributePlatformOverride)) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key], attributePlatformOverride)) !== null && _b !== void 0 ? _b : contentAttributes.contentAttributeAsString(objectGraph, data, ["itunesNotes", key], attributePlatformOverride);
+ return note;
+}
+/**
+ * Try and get notes for some piece of content editorialNotes
+ * @param {Data} data
+ * @param {string} key
+ * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations
+ * @returns {string}
+ */
+export function editorialNotesFromData(objectGraph, data, key, enableEditorialCardOverrides = false) {
+ var _a;
+ let note;
+ if (enableEditorialCardOverrides) {
+ const editorialCard = editorialCardFromData(data);
+ if (mediaAttributes.hasAttributes(editorialCard)) {
+ note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key]);
+ }
+ }
+ note =
+ (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key])) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key]);
+ return note;
+}
+/**
+ * Determines whether the provided data is for a macOS installer.
+ * @param data The data against which to check for a macOS installer.
+ */
+export function isMacOSInstaller(objectGraph, data) {
+ return derivedData.value(data, "isMacOSInstaller", () => {
+ const isMac = objectGraph.client.isMac;
+ if (!isMac) {
+ return false;
+ }
+ const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId");
+ if (!serverData.isDefinedNonNull(bundleId)) {
+ return false;
+ }
+ return bundleId.startsWith("com.apple.InstallAssistant");
+ });
+}
+/**
+ * Check whether an app is unsupported by the current companion configuration.
+ * @param data The data representing an app listing.
+ */
+export function isUnsupportedByCurrentCompanion(objectGraph, data) {
+ const deletableApps = sad.systemApps(objectGraph);
+ if (objectGraph.host.isWatch) {
+ // AppConduit will handle determining if SAD apps are supported
+ if (deletableApps.isUnsupportedDeletableSystemAppFromData(data)) {
+ return true;
+ }
+ else if (objectGraph.client.isTinkerWatch) {
+ return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") && !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ return false;
+ }
+ else {
+ if (isUnsupportedDeletableSystemAppFromData(objectGraph, data, objectGraph.client.isTinkerWatch)) {
+ return true;
+ }
+ else if (objectGraph.client.isTinkerWatch) {
+ if (deletableApps.isSystemAppFromData(data)) {
+ // We don't consider whether an app is marked as standalone with companion
+ // when running in standalone mode. We always want SAD apps to be installable.
+ return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isDeliveredInIOSAppForWatchOS") &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ else {
+ return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ }
+ else {
+ // We only allow standalone system apps to be installed when the watch
+ // is not running in standalone mode. This simplifies things for other teams.
+ return (objectGraph.client.isWatch &&
+ deletableApps.isSystemAppFromData(data) &&
+ !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"));
+ }
+ }
+}
+/**
+ * @deprecated Use sad.isUnsupportedDeletableSystemAppFromData instead.
+ * Check whether a SAD app is unsupported by the current companion configuration.
+ *
+ * @param data The data representing an app listing.
+ * @param isTinkerWatch Whether the current device is a tinker watch
+ */
+export function isUnsupportedDeletableSystemAppFromData(objectGraph, data, isTinkerWatch) {
+ if (isTinkerWatch && sad.systemApps(objectGraph).isSystemAppFromData(data)) {
+ const watchBundleId = mediaAttributes.attributeAsString(data, "watchBundleId");
+ if (serverData.isDefinedNonNullNonEmpty(watchBundleId)) {
+ switch (watchBundleId) {
+ // rdar://63111354 (On Tinker device, able to attempt to download non-Tinker 1st and 3rd party app)
+ // These apps should prevented from installing on a Tinker device
+ case "com.apple.mobilemail.watchkitapp":
+ case "com.apple.news.watchkitapp":
+ case "com.apple.iBooks.watchkitapp":
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+ return false;
+}
+/**
+ * Device Sizes
+ *
+ * Please do not use these constants for anything but screenshots.
+ * Our code should not depend on absolute screen sizes for anything
+ * not related to selecting the correct screenshots to display. -km
+ */
+/// The screen size of iPhone 6.5" devices.
+export const screenSizeIphone65 = new modelsBase.Size(414.0, 896.0);
+/// The screen size of iPhone 5.8" devices.
+export const screenSizeIPhone58 = new modelsBase.Size(375.0, 812.0);
+/// The screen size of iPhone 6+ like devices.
+export const screenSizeIPhone6Plus = new modelsBase.Size(414.0, 736.0);
+/// The screen size of iPhone 6 like devices.
+export const screenSizeIPhone6 = new modelsBase.Size(375.0, 667.0);
+/// The screen size of iPhone 5 like devices.
+export const screenSizeIPhone5 = new modelsBase.Size(320.0, 568.0);
+/// The screen size of original iPhone like devices.
+export const screenSizeIPhoneOriginal = new modelsBase.Size(320.0, 480.0);
+/// The screen size of iPad and iPad mini devices.
+export const screenSizeIPad = new modelsBase.Size(768.0, 1024.0);
+/// The screen size of 7th and 8th gen 10.2" iPads.
+export const screenSizeIPad102 = new modelsBase.Size(810.0, 1080.0);
+/// The screen size of iPad pro 10.5" devices.
+export const screenSizeIPad105 = new modelsBase.Size(834.0, 1112.0);
+/// The screen size of iPad pro 11" devices.
+export const screenSizeIPad11 = new modelsBase.Size(834.0, 1194.0);
+/// The screen size of iPad Pro 11" devices in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeIPad11Landscape = new modelsBase.Size(1194.0, 834.0);
+/// The screen size of iPad pro 12.9" devices.
+export const screenSizeIPadPro = new modelsBase.Size(1024.0, 1366.0);
+/// The screen size of iPad pro 12.9" devices, with rounded corners.
+export const screenSizeIPadPro2018 = new modelsBase.Size(1024.0, 1366.0);
+/// The screen size of iPad pro 12.9" devices, with rounded corners, in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeIPadPro2018Landscape = new modelsBase.Size(1366.0, 1024.0);
+// The screen size of the J310 iPad device.
+export const screenSizeIPadJ310 = new modelsBase.Size(744.0, 1133.0);
+// The screen size of the J310 iPad device, in landscape orientation.
+// rdar: //83176176 (J310: kMGQMainScreenCanvasSizes reports width as largest dimension, contrary to all other iPads and UIKit)
+export const screenSizeIPadJ310Landscape = new modelsBase.Size(1133.0, 744.0);
+/// The screen size of J720/J721 devices.
+export const screenSizeJ720 = new modelsBase.Size(1032.0, 1376.0);
+/// The screen size of J720/J721, in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeJ720Landscape = new modelsBase.Size(1376.0, 1032.0);
+/// The screen size of J717/J718 devices.
+export const screenSizeJ717 = new modelsBase.Size(834.0, 1210.0);
+/// The screen size of J717/J718, in landscape orientation.
+/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation.
+export const screenSizeJ717Landscape = new modelsBase.Size(1210.0, 834.0);
+/// The screen size of the 42mm Apple Watch devices.
+export const screenSizeWatch = new modelsBase.Size(312.0, 390.0);
+/// The screen size of large 2018 Apple Watch devices.
+export const screenSizeWatch2018 = new modelsBase.Size(368.0, 448.0);
+// The screen size of the large 2021 Apple Watch devices.
+export const screenSizeWatch2021 = new modelsBase.Size(396.0, 484.0);
+// The screen size for the 2022 Apple Watch devices.
+export const screenSizeWatch2022 = new modelsBase.Size(410.0, 502.0);
+// The screen size for the 2024 Apple Watch devices.
+export const screenSizeWatch2024 = new modelsBase.Size(416.0, 496.0);
+// The screen size for the Apple Watch Ultra / Ultra 2 devices.
+export const screenSizeWatchUltra = new modelsBase.Size(410.0, 502.0);
+/// The screen size for iPad device.
+export const screenSizeIPadAir2020 = new modelsBase.Size(820.0, 1180.0);
+/// The screen size for iPhone devices.
+export const screenSizeIPhone131 = new modelsBase.Size(360.0, 780.0);
+export const screenSizeIPhone132 = new modelsBase.Size(390.0, 844.0);
+export const screenSizeIPhone134 = new modelsBase.Size(428.0, 926.0);
+// The screen size for a 6.1" D73-style device.
+export const screenSizeIPhone61 = new modelsBase.Size(393.0, 852.0);
+// The screen size for a 6.7" D74-style device.
+export const screenSizeIPhone67 = new modelsBase.Size(430.0, 932.0);
+/// The screen size for a D93 device.
+export const screenSizeD93 = new modelsBase.Size(402.0, 874.0);
+/// The screen size for a D94 device.
+export const screenSizeD94 = new modelsBase.Size(440.0, 956.0);
+/// The screen size for a D23 device.
+export const screenSizeD23 = new modelsBase.Size(420.0, 912.0);
+/// The screen size for a N230 device.
+export const screenSizeN230 = new modelsBase.Size(422.0, 514.0);
+/// All phone types, in order of decreasing size.
+const decreasingPhoneTypes = [
+ "iphone_d74",
+ "iphone_6_5",
+ "iphone_d73",
+ "iphone_5_8",
+ "iphone6+",
+ "iphone6",
+ "iphone5",
+ "iphone",
+];
+// region Device Corner Radius
+/**
+ * The reason we need to hardcode these is because we may want to display screenshots with rounding for a device that is
+ * not the one the user is currently browsing the store with. For example, imagine that the user is browsing the store
+ * with an iPhone 8 but ends up looking at an app that only has D22 screenshots. They should see the D22 screenshots
+ * according to the D22 corner rounding, and using the current client's `screenCornerRadius` would not give us the
+ * proper value.
+ */
+/// The device corner radius of iPad pro 12.9" devices from 2018.
+const deviceCornerRadiusIpadPro2018 = 18.0;
+/// The device corner radius of iPad pro 11" devices.
+const deviceCornerRadiusIpad11 = 18.0;
+/// The device corner radius of iPhone 6.5" devices.
+const deviceCornerRadiusIphone65 = 41.5;
+/// The device corner radius of iPhone 5.8" devices.
+const deviceCornerRadiusIphone58 = 39.0;
+/// The device corner radius of iPhone 6.1" devices.
+const deviceCornerRadiusIphone61 = 55.0;
+/// The device corner radius of iPhone 6.7" devices.
+const deviceCornerRadiusIphone67 = 55.0;
+/// The device corner radius of large 2018 Apple Watch devices.
+const deviceCornerRadiusWatch2018 = 34.0;
+/// The outer device corner radius of large Apple Watch devices.
+const outerDeviceCornerRadiusWatch = 30.0;
+/// The device border thickness for Apple Watch.
+const deviceBorderThicknessWatch = 13.0;
+/// The device border thickness for 2018 Apple Watch.
+const deviceBorderThicknessWatch2018 = 11.0;
+/// The device corner radius for 2021 Apple Watch.
+const deviceCornerRadiusWatch2021 = 55;
+/// The device border thickness for 2021 Apple Watch.
+const deviceBorderThicknessWatch2021 = 5.5;
+/// The device corner radius for 2022/2024 Apple Watch.
+const deviceCornerRadiusWatch2022 = 108;
+/// The outer device corner radius for 2022/2024 Apple Watch.
+const deviceOuterCornerRadiusWatch2022 = 112.5;
+/// The device border thickness for 2022/2024 Apple Watch.
+const deviceBorderThicknessWatch2022 = 4.5;
+export function currentAppPlatform(objectGraph) {
+ var _a;
+ switch (objectGraph.client.deviceType) {
+ case "web":
+ return unwrap((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform);
+ default:
+ return objectGraph.client.deviceType;
+ }
+}
+/// The SF Symbol name that represents the media type.
+export function systemImageNameForAppPlatform(appPlatform) {
+ switch (appPlatform) {
+ case "phone":
+ return "iphone";
+ case "pad":
+ return "ipad";
+ case "tv":
+ return "tv";
+ case "watch":
+ return "applewatch";
+ case "mac":
+ return "macbook";
+ case "messages":
+ return "message";
+ case "vision":
+ return "visionpro";
+ default:
+ unreachable(appPlatform);
+ }
+}
+/**
+ * Returns the factor by which to multiply an artwork's portrait-equivalent width, in order to compute the artwork's
+ * device-rounded corner radius. This is useful because we want to display screenshots for device-rounded screenshots
+ * with a corner radius that is scaled to that of the device screen.
+ *
+ * r = w * r'
+ * r' = R / W
+ *
+ * Where:
+ * r: scaled radius
+ * r': The return value of this function.
+ * R: device corner radius
+ * w: width at which the screenshot will be displayed
+ * W: device width
+ *
+ * We need to have this value here in the JS because we may need to display device-rounded screenshots on a device that
+ * does not have a device corner radius; there is no native API for querying the corner radius of various devices and,
+ * even if there were, we want to avoid specific screen-size checks in the native code.
+ * @param type The screenshot type vended by the server.
+ * @returns {number} The device corner radius factor, or null if the device does not have a corner radius.
+ */
+function deviceCornerRadiusFactorForMediaType(objectGraph, type) {
+ // Let's only bridge over and access client's properties if we need to.
+ switch (type) {
+ case "ipadPro_2018":
+ return deviceCornerRadiusIpadPro2018 / screenSizeIPadPro2018.width;
+ case "ipad_11":
+ return deviceCornerRadiusIpad11 / screenSizeIPad11.width;
+ case "iphone_6_5":
+ return deviceCornerRadiusIphone65 / screenSizeIphone65.width;
+ case "iphone_5_8":
+ return deviceCornerRadiusIphone58 / screenSizeIPhone58.width;
+ case "iphone_d73":
+ return deviceCornerRadiusIphone61 / screenSizeIPhone61.width;
+ case "iphone_d74":
+ return deviceCornerRadiusIphone67 / screenSizeIPhone67.width;
+ case "appleWatch_2018":
+ return deviceCornerRadiusWatch2018 / screenSizeWatch2018.width;
+ case "appleWatch_2021":
+ return deviceCornerRadiusWatch2021 / screenSizeWatch2021.width;
+ case "appleWatch_2022":
+ return deviceCornerRadiusWatch2022 / screenSizeWatch2022.width;
+ case "appleWatch_2024":
+ return deviceCornerRadiusWatch2022 / screenSizeWatch2024.width;
+ default:
+ return null;
+ }
+}
+function deviceOuterCornerRadiusFactorForMediaType(objectGraph, type) {
+ switch (type) {
+ case "appleWatch":
+ return outerDeviceCornerRadiusWatch / screenSizeWatch.width;
+ case "appleWatch_2022":
+ return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2022.width;
+ case "appleWatch_2024":
+ return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2024.width;
+ default:
+ return deviceCornerRadiusFactorForMediaType(objectGraph, type);
+ }
+}
+function deviceBorderThicknessForMediaType(objectGraph, type) {
+ switch (type) {
+ case "appleWatch":
+ return deviceBorderThicknessWatch / screenSizeWatch.width;
+ case "appleWatch_2018":
+ return deviceBorderThicknessWatch2018 / screenSizeWatch2018.width;
+ case "appleWatch_2021":
+ return deviceBorderThicknessWatch2021 / screenSizeWatch2021.width;
+ case "appleWatch_2022":
+ return deviceBorderThicknessWatch2022 / screenSizeWatch2022.width;
+ case "appleWatch_2024":
+ return deviceBorderThicknessWatch2022 / screenSizeWatch2024.width;
+ default:
+ return null;
+ }
+}
+// endregion
+/**
+ Returns a boolean indicating if the client's operating
+ system is the same or later than the specified version.
+
+ @param version The full version number to check against
+ @returns true if the operating system is the same or newer than the specified version; false otherwise.
+ */
+export function isOSAtLeastVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return true;
+ }
+ const versionComponents = version.split(".");
+ const majorVersion = serverData.asNumber(versionComponents[0]) || 0;
+ const minorVersion = serverData.asNumber(versionComponents[1]) || 0;
+ const patchVersion = serverData.asNumber(versionComponents[2]) || 0;
+ return objectGraph.host.isOSAtLeast(majorVersion, minorVersion, patchVersion);
+}
+/**
+ Returns a boolean indicating if the system version of the active, paired watch (if any) is
+ at least the provided version number.
+
+ @param version The full version number to check against
+ @returns true if an active, paired watch exists, and its operating system version is the same or newer than the specified version; false otherwise.
+ */
+export function isActivePairedWatchOSAtLeastVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return true;
+ }
+ const versionComponents = version.split(".");
+ const majorVersion = serverData.asNumber(versionComponents[0]) || 0;
+ const minorVersion = serverData.asNumber(versionComponents[1]) || 0;
+ const patchVersion = serverData.asNumber(versionComponents[2]) || 0;
+ return objectGraph.client.isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion);
+}
+/**
+ * Check whether the active paired device's OS is the same or greater than a given version.
+ */
+export function isActivePairedDeviceAtLeastVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return true;
+ }
+ return objectGraph.client.isPairedSystemVersionAtLeast(version);
+}
+/**
+ * Check whether the active paired device's OS is below a given version.
+ */
+export function isActivePairedWatchOSBelowVersion(objectGraph, version) {
+ if (serverData.isNull(version) || version.length === 0) {
+ return false;
+ }
+ return objectGraph.client.isActivePairedWatchSystemVersionBelow(version);
+}
+export function shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfStyle) {
+ if (objectGraph.client.isTV) {
+ switch (shelfStyle) {
+ case "upsellBreakout":
+ return true;
+ default:
+ return false;
+ }
+ }
+ else {
+ switch (shelfStyle) {
+ case "smallLockup":
+ case "mediumLockup":
+ case "appTrailerLockup":
+ case "screenshotsLockup":
+ case "mixedMediaLockup":
+ case "upsellBreakout":
+ case "arcadeShowcase":
+ return true;
+ default:
+ return false;
+ }
+ }
+}
+export function shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, displayStyle) {
+ switch (displayStyle) {
+ case "LockupSmall":
+ case "LockupLarge":
+ case "BreakoutLarge":
+ case "Hero":
+ case "EditorialLockupLarge":
+ case "EditorialLockupLargeVariant":
+ case "EditorialLockupMedium":
+ case "EditorialLockupMediumVariant":
+ case "StoryMedium":
+ return true;
+ default:
+ return false;
+ }
+}
+/**
+ * The dynamic date string used by apps coming soon.
+ */
+export function dynamicPreorderDateFromData(objectGraph, data, fallbackLabel) {
+ const preorderOffer = offers.offerDataFromData(objectGraph, data);
+ const isPreorder = serverData.asString(preorderOffer, "type") === "preorder";
+ if (isPreorder) {
+ const releaseDateRaw = serverData.asString(preorderOffer, "expectedReleaseDate");
+ const dateDisplayFormat = contentAttributes.contentAttributeAsString(objectGraph, data, "expectedReleaseDateDisplayFormat");
+ if (serverData.isDefinedNonNullNonEmpty(dateDisplayFormat)) {
+ if (serverData.isDefinedNonNullNonEmpty(releaseDateRaw)) {
+ const releaseDate = dateUtil.parseDateOmittingTimeFromString(releaseDateRaw);
+ const tokenFormatMap = {
+ "@@expectedDateMY@@": objectGraph.loc.string("PreOrder.Date.MonthYear"),
+ "@@expectedDateMDY@@": objectGraph.loc.string("PreOrder.Date.MonthDayYear"),
+ };
+ for (const [serverToken, dateFormat] of Object.entries(tokenFormatMap)) {
+ if (dateDisplayFormat.includes(serverToken)) {
+ let formattedDate = objectGraph.loc.formatDateWithContext(dateFormat, releaseDate, "middleOfSentence");
+ if (objectGraph.client.isTV) {
+ formattedDate = formattedDate.replace(/ /g, "\u00a0");
+ }
+ return dateDisplayFormat.replace(serverToken, formattedDate);
+ }
+ }
+ }
+ return dateDisplayFormat;
+ }
+ }
+ // There was no dynamic date to display
+ return fallbackLabel;
+}
+/**
+ * The primary content for an editorial item.
+ * @param data The data from which to derive the primary content.
+ */
+export function primaryContentForData(objectGraph, data) {
+ const primaryContent = mediaRelationship.relationshipData(objectGraph, data, "primary-content");
+ if (serverData.isDefinedNonNullNonEmpty(primaryContent)) {
+ return primaryContent;
+ }
+ // If an EI has canvasData, then in MAPI response its "primary-content" relationship will not include the
+ // primary content meta data. Instead, the primary content data will be included in the "card-contents" relationship.
+ if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isCanvasAvailable")) {
+ return mediaRelationship.relationshipData(objectGraph, data, "card-contents");
+ }
+ return null;
+}
+const grayColorHex = "#9BA9BD";
+export const grayColorCriteria = {
+ colorHex: grayColorHex,
+ maxSaturation: 4,
+ maxBrightness: 9,
+};
+/**
+ * The list of tag background colors we can use to match an app icon background color against, based on saturation and brightness
+ */
+const saturationBrightnessBasedTagColorBuckets = [
+ grayColorCriteria, // Gray
+];
+/**
+ * The list of tag background colors we can use to match an app icon background color against
+ */
+const hueBasedTagColorBuckets = [
+ { colorHex: "#F7816F", minHue: 0, maxHue: 16 },
+ { colorHex: "#FF9034", minHue: 17, maxHue: 33 },
+ { colorHex: "#E3B059", minHue: 34, maxHue: 59 },
+ { colorHex: "#74BD66", minHue: 60, maxHue: 129 },
+ { colorHex: "#72C792", minHue: 130, maxHue: 169 },
+ { colorHex: "#61BFE2", minHue: 170, maxHue: 209 },
+ { colorHex: "#6EA3E9", minHue: 210, maxHue: 239 },
+ { colorHex: "#7D69FA", minHue: 240, maxHue: 259 },
+ { colorHex: "#B363F7", minHue: 260, maxHue: 289 },
+ { colorHex: "#EE7CBD", minHue: 290, maxHue: 360 }, // Pink
+];
+/**
+ * Find the matching tag color for an icon's background color
+ *
+ * @param iconBackgroundColor The bgColor from the icon artwork data
+ * @returns The closest matching color from the HI provided set of tag background colors for this icon color
+ */
+export function closestTagBackgroundColorForIcon(iconBackgroundColor) {
+ var _a;
+ return ((_a = color.findColorBucketForColor(iconBackgroundColor, saturationBrightnessBasedTagColorBuckets, hueBasedTagColorBuckets)) !== null && _a !== void 0 ? _a : color.fromHex(grayColorHex));
+}
+/**
+ * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object
+ * @param data An EditorialArtwork JSON object from MAPI
+ * @returns A JoeColorSet from parsing the input RGB values
+ */
+export function joeColorHexSetFromData(data) {
+ var _a, _b, _c, _d, _e;
+ const textGradient = [];
+ for (const gradientColorHex of serverData.asArrayOrEmpty(data, "textGradient")) {
+ if (isSome(gradientColorHex) && serverData.isString(gradientColorHex)) {
+ textGradient.push(gradientColorHex);
+ }
+ }
+ return {
+ bgColor: (_a = serverData.asString(data, "bgColor")) !== null && _a !== void 0 ? _a : undefined,
+ textColor1: (_b = serverData.asString(data, "textColor1")) !== null && _b !== void 0 ? _b : undefined,
+ textColor2: (_c = serverData.asString(data, "textColor2")) !== null && _c !== void 0 ? _c : undefined,
+ textColor3: (_d = serverData.asString(data, "textColor3")) !== null && _d !== void 0 ? _d : undefined,
+ textColor4: (_e = serverData.asString(data, "textColor4")) !== null && _e !== void 0 ? _e : undefined,
+ textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined,
+ };
+}
+/**
+ * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object
+ * @param data An EditorialArtwork JSON object from MAPI
+ * @returns A JoeColorSet from parsing the input RGB values
+ */
+export function joeColorSetFromData(data) {
+ var _a, _b, _c, _d, _e, _f;
+ const joeColorHexSet = joeColorHexSetFromData(data);
+ const textGradient = [];
+ for (const gradientColorHex of (_a = joeColorHexSet.textGradient) !== null && _a !== void 0 ? _a : []) {
+ const gradientColor = color.fromHex(gradientColorHex);
+ if (isSome(gradientColor)) {
+ textGradient.push(gradientColor);
+ }
+ }
+ return {
+ bgColor: (_b = color.fromHex(joeColorHexSet.bgColor)) !== null && _b !== void 0 ? _b : undefined,
+ textColor1: (_c = color.fromHex(joeColorHexSet.textColor1)) !== null && _c !== void 0 ? _c : undefined,
+ textColor2: (_d = color.fromHex(joeColorHexSet.textColor2)) !== null && _d !== void 0 ? _d : undefined,
+ textColor3: (_e = color.fromHex(joeColorHexSet.textColor3)) !== null && _e !== void 0 ? _e : undefined,
+ textColor4: (_f = color.fromHex(joeColorHexSet.textColor4)) !== null && _f !== void 0 ? _f : undefined,
+ textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined,
+ };
+}
+/**
+ * Attempt to find the first non-gray placeholder color
+ * @param joeColorSet The joe color set for a given icon
+ * @returns The color hext value to use for the joe color placeholder
+ */
+export function bestJoeColorPlaceholderSelectionLogic(joeColorSet) {
+ const joeColorKeys = [
+ "bgColor",
+ "textColor1",
+ "textColor2",
+ "textColor3",
+ "textColor4",
+ ];
+ for (const joeColorKey of joeColorKeys) {
+ const joeColorHex = joeColorSet[joeColorKey];
+ if (!serverData.isString(joeColorHex)) {
+ continue;
+ }
+ const isGrayColor = color.doesColorMeetCriteria(color.fromHex(joeColorHex), grayColorCriteria);
+ if (!isGrayColor) {
+ return joeColorHex;
+ }
+ }
+ return null;
+}
+//# sourceMappingURL=content.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js b/node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js
new file mode 100644
index 0000000..8c8c925
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js
@@ -0,0 +1,91 @@
+import { isSome } from "@jet/environment/types/optional";
+import * as derivedData from "../../foundation/json-parsing/derived-data";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as mediaRelationship from "../../foundation/media/relationships";
+export function dataHasDeviceFamily(objectGraph, data, deviceFamily, recurse = false) {
+ return derivedData.value(data, `dataHasDeviceFamily.${deviceFamily}`, () => {
+ if (!data || !deviceFamily) {
+ return false;
+ }
+ const deviceFamilies = deviceFamiliesFromData(objectGraph, data, recurse);
+ return deviceFamilies.indexOf(deviceFamily) !== -1;
+ });
+}
+export function dataOnlyHasDeviceFamily(objectGraph, data, deviceFamily, recurse = false) {
+ var _a;
+ return (((_a = dataHasDeviceFamily(objectGraph, data, deviceFamily, recurse)) !== null && _a !== void 0 ? _a : false) &&
+ deviceFamiliesFromData(objectGraph, data, recurse).length === 1);
+}
+export function dataHasAnyDeviceFamilies(objectGraph, data, deviceFamilies, recurse = false) {
+ const dataDeviceFamilies = new Set(deviceFamiliesFromData(objectGraph, data, recurse));
+ for (const deviceFamily of deviceFamilies) {
+ if (dataDeviceFamilies.has(deviceFamily)) {
+ return true;
+ }
+ }
+ return false;
+}
+export function dataOnlyHasDeviceFamilies(objectGraph, data, deviceFamilies, recurse = false) {
+ const dataDeviceFamilies = new Set(deviceFamiliesFromData(objectGraph, data, recurse));
+ // Ensure length matches
+ if (deviceFamilies.length !== dataDeviceFamilies.size) {
+ return false;
+ }
+ // Ensure content matches
+ return deviceFamilies.every((deviceFamily) => dataDeviceFamilies.has(deviceFamily));
+}
+export function deviceFamiliesFromData(objectGraph, data, recurse = false) {
+ const deviceFamilies = mediaAttributes.attributeAsArrayOrEmpty(data, "deviceFamilies");
+ // If there isn't a device families attribute...
+ if (deviceFamilies.length === 0 && recurse) {
+ // Use its related apps (if it has them)
+ const apps = mediaRelationship.relationshipCollection(data, "apps");
+ if (isSome(apps)) {
+ for (const app of apps) {
+ // Get the device families from the first app that has one (if any)
+ const appDeviceFamilies = deviceFamiliesFromData(objectGraph, app, recurse);
+ if (serverData.isDefinedNonNullNonEmpty(appDeviceFamilies)) {
+ return appDeviceFamilies;
+ }
+ }
+ }
+ }
+ return deviceFamilies;
+}
+/**
+ * Returns the DeviceFamily for a given device type and model
+ * @param {base.DeviceType} deviceType The device type to check
+ * @param {objectGraph.host.deviceModel} deviceModel The device model to check, currently used to identify iPods
+ * @returns {DeviceFamily} DeviceFamily associated with the input parameters
+ */
+export function deviceFamilyFromDeviceType(objectGraph, deviceType, deviceModel) {
+ let deviceFamily = null;
+ switch (deviceType) {
+ case "phone":
+ deviceFamily = "iphone";
+ break;
+ case "pad":
+ deviceFamily = "ipad";
+ break;
+ case "mac":
+ deviceFamily = "mac";
+ break;
+ case "tv":
+ deviceFamily = "tvos";
+ break;
+ case "watch":
+ deviceFamily = "watch";
+ break;
+ case "vision":
+ deviceFamily = "realityDevice";
+ break;
+ default:
+ break;
+ }
+ if (serverData.isDefinedNonNull(deviceModel) && deviceModel === "ipod") {
+ deviceFamily = "ipod";
+ }
+ return deviceFamily;
+}
+//# sourceMappingURL=device-family.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js b/node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js
new file mode 100644
index 0000000..1cc3e7d
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js
@@ -0,0 +1,485 @@
+import * as validation from "@jet/environment/json/validation";
+import * as models from "../../api/models";
+import * as base from "../../api/models/base";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as mediaRelationship from "../../foundation/media/relationships";
+import * as http from "../../foundation/network/http";
+import * as urls from "../../foundation/network/urls";
+import * as externalDeepLink from "../linking/external-deep-link";
+import * as lockups from "../lockups/lockups";
+import * as metricsUtil from "../metrics/helpers/util";
+import * as offers from "../offers/offers";
+import * as reviews from "../product-page/reviews";
+import * as sharing from "../sharing";
+import * as contentArtwork from "./artwork/artwork";
+import * as content from "./content";
+import * as contentDeviceFamily from "./device-family";
+import { isSome } from "@jet/environment";
+import { Request } from "../../foundation/media/data-fetching";
+import { buildURLFromRequest } from "../../foundation/media/url-builder";
+/**
+ * Create and return the flow preview actions configuration for a product.
+ * @param objectGraph
+ * @param data The response data to read from.
+ * @param includeOfferAction Whether to include an offer action, where possible
+ * @param clientIdentifierOverride The suggested client identifier for the given product
+ * @param clickAction The flow action for viewing the product
+ * @param metricsOptions The metrics options to use for reporting metrics actions.
+ * @param metricsClickOptions TThe metrics click options for the product
+ * @param externalDeepLinkUrl
+ * @returns A configuration object for flow preview actions that can be taken on the product
+ */
+export function flowPreviewActionsConfigurationForProductFromData(objectGraph, data, includeOfferAction, clientIdentifierOverride, clickAction, metricsOptions, metricsClickOptions, externalDeepLinkUrl, lockupSubtitle, lockupTitle) {
+ return validation.context("flowPreviewActionsConfigurationForProductFromData", () => {
+ // Flow preview is only supported on iOS
+ if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") {
+ return null;
+ }
+ const productData = productDataFromData(objectGraph, data);
+ if (!serverData.isDefinedNonNullNonEmpty(productData)) {
+ return null;
+ }
+ const actions = [];
+ // Offer
+ let offerActionIndex = null;
+ let offerDisplayProperties = null;
+ const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(productData, "isPreorder");
+ if (includeOfferAction) {
+ const isArcade = content.isArcadeSupported(objectGraph, productData);
+ const offerType = lockups.offerTypeForMediaType(objectGraph, productData.type, isArcade);
+ const offerAction = offerActionFromData(objectGraph, productData, isPreorder, isArcade, offerType, clientIdentifierOverride, metricsClickOptions);
+ offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, productData, isPreorder, false, null, null, null, null, "flowPreview");
+ const wrappedOfferAction = wrappedOfferActionFromData(objectGraph, productData, offerAction, isPreorder, clientIdentifierOverride, metricsClickOptions, metricsOptions, externalDeepLinkUrl);
+ if (serverData.isDefinedNonNull(wrappedOfferAction) &&
+ serverData.isDefinedNonNull(offerDisplayProperties)) {
+ offerActionIndex = actions.length;
+ wrappedOfferAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://arrow.down.circle");
+ actions.push(wrappedOfferAction);
+ }
+ }
+ // Share
+ const shareAction = shareActionFromData(objectGraph, productData, metricsOptions);
+ if (serverData.isDefinedNonNull(shareAction)) {
+ actions.push(shareAction);
+ }
+ // Reviews
+ const shouldSuppressReviews = reviews.shouldSuppressReviews(objectGraph, productData);
+ const shouldShowRatingsAndReviews = !isPreorder && !shouldSuppressReviews;
+ if (shouldShowRatingsAndReviews) {
+ // See ratings & reviews
+ if (serverData.isDefinedNonNull(clickAction) &&
+ clickAction instanceof models.FlowAction &&
+ (clickAction.pageData instanceof models.ProductPage ||
+ clickAction.pageData instanceof models.ShelfBasedProductPage)) {
+ const seeRatingsAndReviewsAction = seeRatingsAndReviewsActionFromData(objectGraph, productData, clickAction);
+ if (serverData.isDefinedNonNull(seeRatingsAndReviewsAction)) {
+ actions.push(seeRatingsAndReviewsAction);
+ }
+ }
+ // Write a review
+ const isTvOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "tvos");
+ if (!isTvOnlyApp) {
+ const writeReviewAction = writeReviewActionFromData(objectGraph, productData, lockupSubtitle, lockupTitle);
+ if (serverData.isDefinedNonNull(writeReviewAction)) {
+ actions.push(writeReviewAction);
+ }
+ }
+ }
+ return new models.FlowPreviewActionsConfiguration(actions, offerDisplayProperties, offerActionIndex);
+ });
+}
+/**
+ * Create and return the flow preview configuration for a review object
+ * @param data The response data to read from.
+ * @param deviceId The UUID for the user's device.
+ * @param adamId The adam id of the product associated with the review
+ * @returns A configuration object for flow preview actions that can be taken on the review
+ */
+export function flowPreviewActionsConfigurationForReviewFromData(objectGraph, data, deviceId, adamId, reviewText) {
+ var _a;
+ if (serverData.isNullOrEmpty(data) ||
+ (objectGraph.client.deviceType !== "phone" &&
+ objectGraph.client.deviceType !== "pad" &&
+ objectGraph.client.deviceType !== "mac")) {
+ return null;
+ }
+ const actions = [
+ voteActionFromData(objectGraph, data, deviceId, true),
+ voteActionFromData(objectGraph, data, deviceId, false),
+ ];
+ // Workaround for missing report concern url in bag
+ if (((_a = objectGraph.bag.reportConcernUrl) === null || _a === void 0 ? void 0 : _a.length) > 0) {
+ actions.push(reportConcernActionFromData(objectGraph, data, deviceId));
+ }
+ if ((reviewText === null || reviewText === void 0 ? void 0 : reviewText.length) > 0) {
+ actions.push(copyReviewAction(objectGraph, reviewText));
+ }
+ return new models.FlowPreviewActionsConfiguration(actions);
+}
+/**
+ * Create and return the flow preview configuration for a review summary object
+ * @param data The response data to read from.
+ * @param adamId The adam ID of the app
+ * @param appName The name of the app
+ * @param reviewSummaryId The ID of the review summary
+ * @param reviewSummaryText The text of the review summary
+ * @param deviceId The UUID for the customer's device.
+ * @returns A configuration object for flow preview actions that can be taken on the review summary
+ */
+export function flowPreviewActionsConfigurationForReviewSummaryFromData(objectGraph, data, adamId, appName, reviewSummaryId, reviewSummaryText, deviceId) {
+ if (!objectGraph.client.isiOS) {
+ return null;
+ }
+ const actions = [];
+ const reportConcernAction = reviewSummaryReportConcernActionFromData(objectGraph, data, reviewSummaryId, deviceId);
+ if (isSome(reportConcernAction)) {
+ actions.push(reportConcernAction);
+ }
+ const learnMoreAction = reviews.reviewSummaryLearnMoreAction(objectGraph);
+ if (isSome(learnMoreAction)) {
+ actions.push(learnMoreAction);
+ }
+ const fileRadarAction = fileReviewSummaryRadarAction(objectGraph, adamId, appName, reviewSummaryId, reviewSummaryText);
+ if (isSome(fileRadarAction)) {
+ actions.push(fileRadarAction);
+ }
+ return new models.FlowPreviewActionsConfiguration(actions);
+}
+/**
+ * Creates a report a concern action for a given review
+ * @param data The response data to read from.
+ * @param deviceId The UUID for the user's device.
+ * @returns A report concern action
+ */
+export function reviewSummaryReportConcernActionFromData(objectGraph, data, reviewSummaryId, deviceId) {
+ return validation.context("reviewSummaryReportConcernActionFromApiRow", () => {
+ const isEnabled = serverData.asBoolean(data, "enabled");
+ if (!isEnabled) {
+ return null;
+ }
+ const sendAction = createReviewSummaryReportConcernHtmlTemplateAction(objectGraph, reviewSummaryId);
+ const concerns = serverData.asArrayOrEmpty(data, "concerns");
+ const reviewSummaryConcerns = concerns.map((concernData) => {
+ let name;
+ let uppercaseName;
+ const concernKind = serverData.asString(concernData, "kind");
+ switch (concernKind) {
+ case "OFFENSIVE":
+ case "OFFENSIVE_OR_HARMFUL":
+ name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Offensive.Name");
+ uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Offensive.UppercaseName");
+ break;
+ case "MISREPRESENTING_THE_APP":
+ case "MISREPRESENT":
+ name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Misrepresent.Name");
+ uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Misrepresent.UppercaseName");
+ break;
+ case "SOMETHING_ELSE":
+ name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.SomethingElse.Name");
+ uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.SomethingElse.UppercaseName");
+ break;
+ default:
+ break;
+ }
+ return new base.ReportConcernReason(concernKind, name, uppercaseName);
+ });
+ if (reviewSummaryConcerns.length === 0) {
+ return null;
+ }
+ const title = objectGraph.loc.string("ACTION_REVIEW_REPORT");
+ const explanation = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Explanation");
+ const action = new models.ReviewSummaryReportConcernAction(reviewSummaryConcerns, title, explanation, sendAction);
+ action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle");
+ return action;
+ });
+}
+function createReviewSummaryReportConcernHtmlTemplateAction(objectGraph, reviewSummaryId) {
+ const request = new Request(objectGraph);
+ request.includeAppBinaryTraitsAttribute = false;
+ request.resourceType = "concerns";
+ const url = buildURLFromRequest(objectGraph, request);
+ const sendAction = new models.HttpTemplateAction(url.toString());
+ sendAction.method = "POST";
+ sendAction.disableCache = true;
+ sendAction.needsMediaToken = true;
+ sendAction.headers = { "Content-Type": "application/json" };
+ sendAction.bodyDictionary = {
+ report: { contentId: reviewSummaryId, contentKind: "review-summaries", concerns: null },
+ };
+ const successToast = new models.AlertAction("toast");
+ successToast.title = objectGraph.loc.string("TOAST_CONCERN_REPORTED_TITLE");
+ successToast.message = objectGraph.loc.string("TOAST_CONCERN_REPORTED_DESCRIPTION");
+ successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle");
+ sendAction.successAction = successToast;
+ const failureAlert = new models.AlertAction("default");
+ failureAlert.title = objectGraph.loc.string("Alert.GenericError.Title");
+ failureAlert.message = objectGraph.loc.string("Alert.GenericError.Message");
+ failureAlert.isCancelable = true;
+ sendAction.failureAction = failureAlert;
+ return sendAction;
+}
+/**
+ * Walks through the given data to extract the data blob for a product
+ * @param data The response data to read from.
+ * @returns Response data for a product
+ */
+function productDataFromData(objectGraph, data) {
+ return validation.context(`productDataFromData: ${data.type}`, () => {
+ switch (data.type) {
+ case "apps":
+ case "app-bundles": {
+ return data;
+ }
+ case "editorial-items": {
+ const cardContents = mediaRelationship.relationshipCollection(data, "card-contents");
+ if (serverData.isDefinedNonNullNonEmpty(cardContents) && cardContents.length === 1) {
+ const cardContentsData = cardContents[0];
+ return productDataFromData(objectGraph, cardContentsData);
+ }
+ break;
+ }
+ case "editorial-elements": {
+ const contents = mediaRelationship.relationshipCollection(data, "contents");
+ if (serverData.isDefinedNonNullNonEmpty(contents) && contents.length === 1) {
+ const contentData = contents[0];
+ return productDataFromData(objectGraph, contentData);
+ }
+ break;
+ }
+ default: {
+ return null;
+ }
+ }
+ return null;
+ });
+}
+/**
+ * Create an offer action for the provided product data
+ * @param data The response data to read from.
+ * @param isPreorder Whether the product is a pre-order
+ * @param isArcade Whether the product is an Arcade product
+ * @param offerType The type of offer for the product
+ * @param clientIdentifierOverride The suggested client identifier for the given product
+ * @param metricsClickOptions TThe metrics click options for the product
+ * @returns An offer action
+ */
+function offerActionFromData(objectGraph, data, isPreorder, isArcade, offerType, clientIdentifierOverride, metricsClickOptions) {
+ if (serverData.isNull(data) || data.type !== "apps") {
+ return null;
+ }
+ const offerData = offers.offerDataFromData(objectGraph, data);
+ const appIcon = content.iconFromData(objectGraph, data, null, clientIdentifierOverride);
+ const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, appIcon, clientIdentifierOverride);
+ const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, false, metricsPlatformDisplayStyle, metricsClickOptions, "flowPreview");
+ return offerAction;
+}
+/**
+ * Wraps an offer action if required
+ * @param data The response data to read from.
+ * @param offerAction The provided offer action to wrap
+ * @param clientIdentifierOverride The suggested client identifier for the given product
+ * @param metricsClickOptions TThe metrics click options for the product
+ * @param metricsOptions The metrics options to use for reporting metrics actions.
+ * @param externalDeepLink The promotional deep link url to use on the product's offer.
+ * @returns A wrapped offer action
+ */
+function wrappedOfferActionFromData(objectGraph, data, offerAction, isPreorder, clientIdentifierOverride, metricsClickOptions, metricsOptions, externalDeepLinkUrl) {
+ if (serverData.isNull(offerAction)) {
+ return null;
+ }
+ let wrappedOfferAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, metricsClickOptions, "flowPreview", clientIdentifierOverride);
+ if ((externalDeepLinkUrl === null || externalDeepLinkUrl === void 0 ? void 0 : externalDeepLinkUrl.length) > 0) {
+ // Configure cross link as well as deep link action.
+ wrappedOfferAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, wrappedOfferAction, offerAction.adamId, null, externalDeepLinkUrl, false, metricsClickOptions);
+ }
+ return wrappedOfferAction;
+}
+/**
+ * Creates and returns an action for sharing a product
+ * @param data The response data to read from.
+ * @param metricsOptions The metrics options to use for reporting metrics actions.
+ * @returns A share action or null
+ */
+function shareActionFromData(objectGraph, data, metricsOptions) {
+ const shareAction = sharing.shareProductActionFromData(objectGraph, data, metricsOptions.pageInformation, metricsOptions.locationTracker);
+ if (serverData.isDefinedNonNull(shareAction)) {
+ shareAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_SHARE");
+ shareAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.arrow.up");
+ return shareAction;
+ }
+ return shareAction;
+}
+/**
+ * Creates and returns an action for navigating to a product's ratings & reviews
+ * @param data The response data to read from.
+ * @param clickAction The flow action for viewing the product
+ * @returns An action or null
+ */
+function seeRatingsAndReviewsActionFromData(objectGraph, data, clickAction) {
+ const seeRatingsAndReviewsAction = reviews.seeRatingsAndReviewsActionFromClickAction(objectGraph, data.id, clickAction);
+ if (serverData.isDefinedNonNull(seeRatingsAndReviewsAction)) {
+ seeRatingsAndReviewsAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_SEE_RATINGS_AND_REVIEWS");
+ seeRatingsAndReviewsAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://star");
+ seeRatingsAndReviewsAction.animationBehavior = "never";
+ }
+ return seeRatingsAndReviewsAction;
+}
+/**
+ * Creates and returns an action for writing a product review
+ * @param data The response data to read from.
+ * @returns An action or null
+ */
+function writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle) {
+ const writeReviewAction = reviews.writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle);
+ if (serverData.isDefinedNonNull(writeReviewAction)) {
+ writeReviewAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_WRITE_REVIEW");
+ writeReviewAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.pencil");
+ }
+ return writeReviewAction;
+}
+/**
+ * Create and returns an action for voting whether a review was helpful or not
+ * @param data The response data to read from.
+ * @param deviceId The UUID for the user's device.
+ * @param helpful Whether or not the vote is indicating the review was helpful, or not helpful.
+ * @returns A new vote action.
+ */
+export function voteActionFromData(objectGraph, data, deviceId, helpful) {
+ const baseVoteUrl = objectGraph.bag.voteUrl;
+ const reviewId = serverData.asString(data, "id", "coercible");
+ const voteUrl = new urls.URL(baseVoteUrl).param("userReviewId", reviewId);
+ if (objectGraph.client.isVision) {
+ // We only attach this parameter for visionOS, as the other platforms have legacy handling in place
+ // that we do not want to interrupt.
+ voteUrl.param("version", "2");
+ }
+ const action = new models.HttpAction(voteUrl.build());
+ const successToast = new models.AlertAction("toast");
+ if (helpful) {
+ action.title = objectGraph.loc.string("ACTION_REVIEW_HELPFUL");
+ successToast.title = objectGraph.loc.string("TOAST_HELPFUL_TITLE");
+ successToast.message = objectGraph.loc.string("TOAST_HELPFUL_DESCRIPTION");
+ action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://hand.thumbsup");
+ successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, objectGraph.client.isVision ? "systemimage://hand.thumbsup.fill" : "systemimage://hand.thumbsup");
+ }
+ else {
+ action.title = objectGraph.loc.string("ACTION_REVIEW_NOT_HELPFUL");
+ successToast.title = objectGraph.loc.string("TOAST_NOT_HELPFUL_TITLE");
+ successToast.message = objectGraph.loc.string("TOAST_NOT_HELPFUL_DESCRIPTION");
+ action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://hand.thumbsdown");
+ successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, objectGraph.client.isVision ? "systemimage://hand.thumbsdown.fill" : "systemimage://hand.thumbsdown");
+ }
+ action.method = "POST";
+ action.isStoreRequest = true;
+ action.disableCache = true;
+ action.headers = { "Content-Type": http.FormBuilder.contentType };
+ action.body = new http.FormBuilder()
+ .param("vote", helpful ? "1" : "0")
+ .param("guid", deviceId)
+ .build();
+ action.successAction = successToast;
+ return action;
+}
+/**
+ * Creates a report a concern action for a given review
+ * @param data The response data to read from.
+ * @param deviceId The UUID for the user's device.
+ * @returns A report concern action
+ */
+export function reportConcernActionFromData(objectGraph, data, deviceId) {
+ return validation.context("reportConcernActionFromApiRow", () => {
+ const reviewId = serverData.asString(data, "id", "coercible");
+ const reportConcernUrl = objectGraph.bag.reportConcernUrl;
+ const sendAction = new models.HttpTemplateAction(reportConcernUrl);
+ sendAction.method = "POST";
+ sendAction.isStoreRequest = true;
+ sendAction.disableCache = true;
+ sendAction.needsAuthentication = true;
+ sendAction.headers = { "Content-Type": http.FormBuilder.contentType };
+ sendAction.body = new http.FormBuilder().param("userReviewId", reviewId).param("guid", deviceId).build();
+ const reasonParameter = new models.HttpTemplateParameter("selectedReason", "formBody", "decimalPad");
+ const explanationParameter = new models.HttpTemplateParameter("explanation", "formBody", "text");
+ sendAction.parameters = [reasonParameter, explanationParameter];
+ if (!objectGraph.client.isVision) {
+ const successToast = new models.AlertAction("toast");
+ successToast.title = objectGraph.loc.string("TOAST_CONCERN_REPORTED_TITLE");
+ successToast.message = objectGraph.loc.string("TOAST_CONCERN_REPORTED_DESCRIPTION");
+ successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle");
+ sendAction.successAction = successToast;
+ }
+ const failureAlert = new models.AlertAction("default");
+ failureAlert.title = objectGraph.loc.string("Alert.GenericError.Title");
+ failureAlert.message = objectGraph.loc.string("Alert.GenericError.Message");
+ failureAlert.isCancelable = true;
+ sendAction.failureAction = failureAlert;
+ let concernReasons = objectGraph.bag.reportConcernReasons;
+ if (serverData.isNullOrEmpty(concernReasons)) {
+ // Not available until 18G
+ concernReasons = [
+ {
+ reasonId: "1",
+ name: "It contains offensive material",
+ upperCaseName: "IT CONTAINS OFFENSIVE MATERIAL",
+ },
+ {
+ reasonId: "8",
+ name: "It's off-topic",
+ upperCaseName: "IT\u2019S OFF-TOPIC",
+ },
+ {
+ reasonId: "111003",
+ name: "It looks like spam",
+ upperCaseName: "IT LOOKS LIKE SPAM",
+ },
+ {
+ reasonId: "7",
+ name: "Something else",
+ upperCaseName: "SOMETHING ELSE",
+ },
+ ];
+ }
+ const reasons = concernReasons.map((reasonData) => {
+ return new base.ReportConcernReason(serverData.asString(reasonData, "reasonId"), serverData.asString(reasonData, "name"), serverData.asString(reasonData, "upperCaseName"));
+ });
+ const action = new models.ReportConcernAction(reasons);
+ action.title = objectGraph.loc.string("ACTION_REVIEW_REPORT");
+ action.explanation = objectGraph.bag.reportConcernExplanation;
+ if (serverData.isNullOrEmpty(action.explanation)) {
+ // Not available until 18G
+ action.explanation = "Tell us a little more (Optional)";
+ }
+ action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle");
+ action.sendAction = sendAction;
+ return action;
+ });
+}
+export function copyReviewAction(objectGraph, reviewText) {
+ const copyTextAction = new models.CopyTextAction(reviewText);
+ copyTextAction.title = objectGraph.loc.string("ACTION_REVIEW_COPY");
+ copyTextAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://doc.on.doc");
+ return copyTextAction;
+}
+/**
+ * Creates an action for filing a radar against a Review Summary
+ * @param objectGraph Current object graph
+ * @param adamId The ID of the app
+ * @param appName The name of the app
+ * @param reviewSummaryId The ID of the review summary
+ * @param reviewSummaryText The text of the review summary
+ * @returns An action that will deep link to Radar
+ */
+function fileReviewSummaryRadarAction(objectGraph, adamId, appName, reviewSummaryId, reviewSummaryText) {
+ if (!["debug", "internal"].includes(objectGraph.client.buildType)) {
+ return null;
+ }
+ const componentId = "999915"; // ASE App Store | Recommendations
+ const title = `Review Summary Feedback: ${appName}`;
+ const description = `App ID: ${adamId}\nApp name: ${appName}\nReview summary ID: ${reviewSummaryId}\nReview summary: ${reviewSummaryText}\n\nFeedback: `;
+ const url = `tap-to-radar://new/problem?componentid=${componentId}&title=${title}&description=${description}`;
+ const action = new models.ExternalUrlAction(url, true);
+ action.title = objectGraph.loc.string("Action.ProvideFeedback");
+ action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://ant.circle");
+ return action;
+}
+//# sourceMappingURL=flow-preview.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js
new file mode 100644
index 0000000..7071edd
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js
@@ -0,0 +1,77 @@
+import { isFeatureEnabledForCurrentUser } from "../../common/util/lottery";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as platformAttributes from "../../foundation/media/platform-attributes";
+import * as contentAttributes from "./attributes";
+export function isGameControllerSupported(objectGraph, data) {
+ switch (controllerRequirementFromData(objectGraph, data)) {
+ case "CONTROLLER_OPTIONAL":
+ case "CONTROLLER_REQUIRED":
+ case "SIRI_REMOTE_REQUIRED":
+ case "SIRI_REMOTE_OR_CONTROLLER_REQUIRED":
+ case "CONTROLLER_RECOMMENDED":
+ return true;
+ default:
+ return false;
+ }
+}
+export function isGameControllerRecommended(objectGraph, data) {
+ return (objectGraph.client.isiOS &&
+ isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.gameControllerRecommendedRolloutRate) &&
+ controllerRequirementFromData(objectGraph, data) === "CONTROLLER_RECOMMENDED");
+}
+export function isGameControllerRequired(objectGraph, data) {
+ switch (controllerRequirementFromData(objectGraph, data)) {
+ case "CONTROLLER_REQUIRED":
+ case "SIRI_REMOTE_REQUIRED":
+ case "SIRI_REMOTE_OR_CONTROLLER_REQUIRED":
+ return true;
+ default:
+ return false;
+ }
+}
+export function isSpatialControllerRequired(objectGraph, data) {
+ return spatialControllerRequirementFromData(objectGraph, data) === "REQUIRED";
+}
+export function isSpatialControllerSupported(objectGraph, data) {
+ return (spatialControllerRequirementFromData(objectGraph, data) === "SUPPORTED" ||
+ spatialControllerRequirementFromData(objectGraph, data) === "REQUIRED");
+}
+export function controllerRequirementFromData(objectGraph, data) {
+ // If the data does not contain the current device's OS, we need to use the best alternative that it is compatible with.
+ const platformAttributeForClient = contentAttributes.defaultAttributePlatform(objectGraph);
+ if (platformAttributeForClient === null) {
+ return "NO_BADGE";
+ }
+ const requiresBinaryCompatibilityMode = !platformAttributes.hasPlatformAttribute(data, platformAttributeForClient);
+ if (requiresBinaryCompatibilityMode) {
+ const compatibilityControllerRequirementData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "compatibilityControllerRequirement");
+ if (compatibilityControllerRequirementData === null ||
+ serverData.isNullOrEmpty(compatibilityControllerRequirementData)) {
+ return "NO_BADGE";
+ }
+ const compatibilityControllerRequirement = compatibilityControllerRequirementData[platformAttributeForClient];
+ if (serverData.isNullOrEmpty(compatibilityControllerRequirement)) {
+ return "NO_BADGE";
+ }
+ return compatibilityControllerRequirement;
+ }
+ const remoteControllerRequirement = contentAttributes.contentAttributeAsString(objectGraph, data, "remoteControllerRequirement");
+ if (serverData.isDefinedNonNullNonEmpty(remoteControllerRequirement)) {
+ return remoteControllerRequirement;
+ }
+ return "NO_BADGE";
+}
+export function spatialControllerRequirementFromData(objectGraph, data) {
+ const platformAttributeForClient = contentAttributes.defaultAttributePlatform(objectGraph);
+ if (!objectGraph.client.isVision ||
+ platformAttributeForClient === null ||
+ !objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) {
+ return "NOT_SUPPORTED";
+ }
+ const spatialRequirement = platformAttributes.platformAttributeAsString(data, platformAttributeForClient, "spatialControllerRequirement");
+ if (serverData.isDefinedNonNullNonEmpty(spatialRequirement)) {
+ return spatialRequirement;
+ }
+ return "NOT_SUPPORTED";
+}
+//# sourceMappingURL=game-controller.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js b/node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js
new file mode 100644
index 0000000..7c449c8
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js
@@ -0,0 +1,322 @@
+import * as validation from "@jet/environment/json/validation";
+import * as models from "../../api/models";
+import { isFeatureEnabledForCurrentUser } from "../../common/util/lottery";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as color from "../../foundation/util/color-util";
+import { createArtworkForResource } from "./artwork/artwork";
+import * as contentAttributes from "./attributes";
+import * as gameController from "./game-controller";
+import { isSome } from "@jet/environment";
+import { supportedGameCenterFeaturesFromData } from "./content";
+import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent";
+import { getLocale } from "../locale";
+import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils";
+import { openGamesUIAction } from "../arcade/arcade-common";
+import { getPlatform } from "../preview-platform";
+/**
+ * Creates a list of product capabilities for a given product.
+ *
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @param isFreeProduct Whether the offer is for a free product
+ * @returns An array of product capabilities
+ */
+export function productCapabilitiesFromData(objectGraph, productData, isFreeProduct) {
+ return validation.context("capabilitiesFromData", () => {
+ return [
+ gameCenterCapabilityFromData(objectGraph, productData),
+ siriCapabilityFromData(objectGraph, productData),
+ sharePlayCapabilityFromData(objectGraph, productData),
+ walletCapabilityFromData(objectGraph, productData),
+ controllersCapabilityFromData(objectGraph, productData),
+ familySharingCapabilityFromData(objectGraph, productData, isFreeProduct),
+ spatialControlsCapabilityFromData(objectGraph, productData),
+ safariExtensionCapabilityFromData(objectGraph, productData),
+ ].filter((capability) => capability);
+ });
+}
+/**
+ * Creates the Game Center capability.
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @param gameInfoSummary The game info summary for the product
+ * @returns A product capability, or null
+ */
+export function gameCenterCapabilityFromData(objectGraph, productData) {
+ const isGameCenterEnabled = isSome(productData) &&
+ contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isGameCenterEnabled");
+ if (!isGameCenterEnabled) {
+ return null;
+ }
+ if (objectGraph.bag.gameCenterExtendSupportedFeatures) {
+ const supportedGameCenterFeatures = supportedGameCenterFeaturesFromData(productData);
+ const supportsLeaderboards = supportedGameCenterFeatures === null || supportedGameCenterFeatures === void 0 ? void 0 : supportedGameCenterFeatures.includes("leaderboards");
+ const supportsAchievements = supportedGameCenterFeatures === null || supportedGameCenterFeatures === void 0 ? void 0 : supportedGameCenterFeatures.includes("achievements");
+ let captionText;
+ if (supportsLeaderboards && supportsAchievements) {
+ captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_ALL_FEATURES");
+ }
+ else if (supportsLeaderboards) {
+ captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_ONLY_LEADERBOARDS");
+ }
+ else if (supportsAchievements) {
+ captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_ONLY_ACHIEVEMENTS");
+ }
+ else {
+ captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_NO_FEATURES");
+ }
+ const title = objectGraph.loc.string("CAPABILITY_GAME_CENTER_TITLE");
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityGameCenter", 46, 45);
+ if (objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e")) {
+ let linkAction;
+ if (isSome(productData) && objectGraph.props.enabled("gameProductIdOpenGamesUIAction")) {
+ linkAction = openGamesUIAction(objectGraph, {
+ gamePage: { productID: productData.id },
+ });
+ }
+ else {
+ linkAction = openGamesUIAction(objectGraph);
+ }
+ const linkActionTitle = objectGraph.loc.string("ProductPage.Capability.GameCenter.ActionTitle");
+ captionText = `${captionText}\n${linkActionTitle}`;
+ const styledText = new models.StyledText(captionText);
+ const linkedSubstrings = {};
+ linkedSubstrings[linkActionTitle] = linkAction;
+ const linkableCaption = new models.LinkableText(styledText, linkedSubstrings);
+ const captionTrailingArtwork = createArtworkForResource(objectGraph, "systemimage://arrow.up.forward.square.fill", 16, 16);
+ return new models.ProductCapability("gameCenter", title, linkableCaption, captionTrailingArtwork, undefined, artwork);
+ }
+ else {
+ const styledText = new models.StyledText(captionText);
+ const linkableCaption = new models.LinkableText(styledText);
+ return new models.ProductCapability("gameCenter", title, linkableCaption, null, null, artwork);
+ }
+ }
+ else {
+ const title = objectGraph.loc.string("CAPABILITY_GAME_CENTER_TITLE");
+ const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION"));
+ const caption = new models.LinkableText(styledText);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityGameCenter", 46, 45);
+ return new models.ProductCapability("gameCenter", title, caption, undefined, null, artwork);
+ }
+}
+/**
+ * Creates the Siri capability.
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @returns A product capability, or null
+ */
+function siriCapabilityFromData(objectGraph, productData) {
+ if (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isSiriSupported")) {
+ return null;
+ }
+ const title = objectGraph.loc.string("CAPABILITY_SIRI_TITLE");
+ const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_SIRI_EXPLANATION"));
+ const caption = new models.LinkableText(styledText);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilitySiri", 46, 45);
+ return new models.ProductCapability("siri", title, caption, undefined, null, artwork);
+}
+/**
+ * Creates the Wallet capability.
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @returns A product capability, or null
+ */
+function walletCapabilityFromData(objectGraph, productData) {
+ if (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "supportsPassbook")) {
+ return null;
+ }
+ const title = objectGraph.loc.string("CAPABILITY_WALLET_TITLE");
+ const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_WALLET_EXPLANATION"));
+ const caption = new models.LinkableText(styledText);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityWallet", 46, 45);
+ return new models.ProductCapability("wallet", title, caption, undefined, null, artwork);
+}
+/**
+ * Creates the Controllers capability.
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @returns A product capability, or null
+ */
+function controllersCapabilityFromData(objectGraph, productData) {
+ if (!gameController.isGameControllerSupported(objectGraph, productData)) {
+ return null;
+ }
+ const title = objectGraph.loc.string("CAPABILITY_MFI_CONTROLLERS_TITLE"); // Game Controllers
+ // Create the Learn More link
+ let linkAction = null;
+ const storyId = objectGraph.bag.gameControllerLearnMoreEditorialItemId;
+ if (isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.gameControllerRecommendedRolloutRate) &&
+ (objectGraph.client.isiOS || objectGraph.client.isVision || objectGraph.client.isWeb) &&
+ isSome(storyId) &&
+ (storyId === null || storyId === void 0 ? void 0 : storyId.length) > 0) {
+ const routableArticlePageIntent = makeRoutableArticlePageIntent({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: storyId,
+ });
+ linkAction = new models.FlowAction("article");
+ linkAction.title = objectGraph.loc.string("ProductPage.Capability.GameController.ActionTitle"); // Learn More
+ linkAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent);
+ if (objectGraph.client.isWeb) {
+ linkAction.destination = routableArticlePageIntent;
+ }
+ }
+ let captionText = linkAction
+ ? objectGraph.loc.string("ProductPage.Capability.GameController.Explanation.v2.WithNewlineActionTemplate")
+ : objectGraph.loc.string("ProductPage.Capability.GameController.Explanation.v2");
+ const linkedSubstrings = {};
+ if (linkAction === null || linkAction === void 0 ? void 0 : linkAction.title) {
+ captionText = captionText.replace("{learnMoreLink}", linkAction.title); // Learn More
+ linkedSubstrings[linkAction.title] = linkAction;
+ }
+ const styledText = new models.StyledText(captionText);
+ const linkableCaption = new models.LinkableText(styledText, linkedSubstrings);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityController", 46, 45);
+ return new models.ProductCapability("controllers", title, linkableCaption, undefined, null, artwork);
+}
+/**
+ * Creates the Spatial Controls capability.
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @returns A product capability, or null
+ */
+function spatialControlsCapabilityFromData(objectGraph, productData) {
+ if (!objectGraph.client.isVision || !gameController.isSpatialControllerSupported(objectGraph, productData)) {
+ return null;
+ }
+ const title = objectGraph.loc.string("ProductPage.Badge.SpatialController.Heading");
+ // Create the Learn More link
+ let linkAction = null;
+ const storyId = objectGraph.bag.spatialControlsLearnMoreEditorialItemId;
+ if (isSome(storyId) && (storyId === null || storyId === void 0 ? void 0 : storyId.length) > 0) {
+ const routableArticlePageIntent = makeRoutableArticlePageIntent({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: storyId,
+ });
+ linkAction = new models.FlowAction("article");
+ linkAction.title = objectGraph.loc.string("Action.LearnMore");
+ linkAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent);
+ if (objectGraph.client.isWeb) {
+ linkAction.destination = routableArticlePageIntent;
+ }
+ }
+ let captionText = linkAction !== null
+ ? objectGraph.loc.string("ProductPage.Capability.SpatialController.Explanation.WithActionTemplate")
+ : objectGraph.loc.string("ProductPage.Capability.SpatialController.Explanation");
+ const linkedSubstrings = {};
+ if (isSome(linkAction === null || linkAction === void 0 ? void 0 : linkAction.title)) {
+ captionText = captionText.replace("{learnMoreLink}", linkAction.title);
+ linkedSubstrings[linkAction.title] = linkAction;
+ }
+ const styledText = new models.StyledText(captionText);
+ const linkableCaption = new models.LinkableText(styledText, linkedSubstrings);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilitySpatialControllers");
+ return new models.ProductCapability("spatialControllers", title, linkableCaption, undefined, null, artwork);
+}
+/**
+ * Creates a family sharing capability for the given product, if any. This is driven off whether family sharing
+ * is enabled, whether there are any IAPs, and whther any of those IAPs are shareable.
+ *
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @param isFreeProduct Whether the offer is for a free product
+ * @returns A product capability, or null
+ */
+function familySharingCapabilityFromData(objectGraph, productData, isFreeProduct) {
+ // Check if family sharing is enabled, or if this is a SAD app
+ const familyShareEnabledDateString = mediaAttributes.attributeAsString(productData, "familyShareEnabledDate");
+ if (!familyShareEnabledDateString ||
+ mediaAttributes.attributeAsBooleanOrFalse(productData, "isFirstPartyHideableApp")) {
+ return null;
+ }
+ // Check family sharing was enabled in the past
+ const familyShareEnabledDate = new Date(familyShareEnabledDateString);
+ const now = new Date();
+ if (!familyShareEnabledDate || familyShareEnabledDate > now) {
+ return null;
+ }
+ const hasInAppPurchases = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "hasInAppPurchases");
+ const hasFamilyShareableInAppPurchases = hasInAppPurchases &&
+ contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "hasFamilyShareableInAppPurchases");
+ // Create the Learn More link
+ let linkAction = null;
+ const storyId = objectGraph.bag.familySubscriptionsLearnMoreEditorialItemId;
+ const platformSupportsLink = objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isVision || objectGraph.client.isWeb;
+ if (isSome(storyId) && (storyId === null || storyId === void 0 ? void 0 : storyId.length) > 0 && platformSupportsLink && hasFamilyShareableInAppPurchases) {
+ const routableArticlePageIntent = makeRoutableArticlePageIntent({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: storyId,
+ });
+ linkAction = new models.FlowAction("article");
+ linkAction.title = objectGraph.loc.string("CAPABILITY_FAMILY_SHARING_ACTION_TITLE");
+ linkAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent);
+ if (objectGraph.client.isWeb) {
+ linkAction.destination = routableArticlePageIntent;
+ }
+ }
+ // Generate the caption based on whether the product has IAPs, whether it has shareable IAPs, and whether it is free
+ const title = objectGraph.loc.string("CAPABILITY_FAMILY_SHARING_TITLE");
+ let captionKey;
+ if (hasFamilyShareableInAppPurchases) {
+ captionKey = linkAction
+ ? "CAPABILITY_FAMILY_SHARING_SOME_SHAREABLE_IAPS_EXPLANATION_WITH_ACTION_TEMPLATE"
+ : "CAPABILITY_FAMILY_SHARING_SOME_SHAREABLE_IAPS_EXPLANATION";
+ }
+ else if (!hasInAppPurchases && !isFreeProduct) {
+ captionKey = linkAction
+ ? "CAPABILITY_FAMILY_SHARING_PAID_APP_NO_IAPS_EXPLANATION_WITH_ACTION_TEMPLATE"
+ : "CAPABILITY_FAMILY_SHARING_PAID_APP_NO_IAPS_EXPLANATION";
+ }
+ if (!captionKey) {
+ return null;
+ }
+ let captionText = objectGraph.loc.string(captionKey);
+ const linkedSubstrings = {};
+ if (linkAction === null || linkAction === void 0 ? void 0 : linkAction.title) {
+ captionText = captionText.replace("{learnMoreLink}", linkAction.title);
+ linkedSubstrings[linkAction.title] = linkAction;
+ }
+ const styledText = new models.StyledText(captionText);
+ const linkableCaption = new models.LinkableText(styledText, linkedSubstrings);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityFamilySharing", 46, 45);
+ return new models.ProductCapability("familySharing", title, linkableCaption, undefined, linkAction, artwork);
+}
+/**
+ * Creates the Safari Extension capability.
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @returns A product capability, or null
+ */
+function safariExtensionCapabilityFromData(objectGraph, productData) {
+ const productHasExtension = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "hasSafariExtension");
+ const platformSupports = objectGraph.client.isMac || objectGraph.client.isiOS;
+ if (!productHasExtension || !platformSupports) {
+ return null;
+ }
+ const title = objectGraph.loc.string("CAPABILITY_SAFARI_EXTENSION_TITLE");
+ const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_SAFARI_EXTENSION_EXPLANATION"));
+ const caption = new models.LinkableText(styledText);
+ const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilitySafariExtension", 129, 129);
+ return new models.ProductCapability("safariExtensions", title, caption, undefined, null, artwork);
+}
+/**
+ * Creates the SharePlay capability
+ * @param objectGraph The object graph
+ * @param productData The data for the product
+ * @returns A product capability, or null
+ */
+function sharePlayCapabilityFromData(objectGraph, productData) {
+ if (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "supportsSharePlay")) {
+ return null;
+ }
+ const title = objectGraph.loc.string("CAPABILITY_SHAREPLAY_TITLE");
+ const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_SHAREPLAY_EXPLANATION"));
+ const caption = new models.LinkableText(styledText);
+ const artwork = createArtworkForResource(objectGraph, "systemimage://shareplay");
+ const artworkTintColor = color.named("systemGreen");
+ return new models.ProductCapability("sharePlay", title, caption, undefined, null, artwork, artworkTintColor);
+}
+//# sourceMappingURL=product-capabilities.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/sad.js b/node_modules/@jet-app/app-store/tmp/src/common/content/sad.js
new file mode 100644
index 0000000..20fde05
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/content/sad.js
@@ -0,0 +1,93 @@
+import { isSome } from "@jet/environment/types/optional";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as contentAttributes from "./attributes";
+let implementation = null;
+/**
+ * Creates an implementation of the `SystemApps` interface
+ * which is appropriate for the current platform.
+ */
+export function systemApps(objectGraph) {
+ if (implementation !== null) {
+ return implementation;
+ }
+ // Because watchOS is not a proper platform from the back end's perspective,
+ // we must map adam IDs to bundle IDs using a bag property.
+ if (objectGraph.client.isWatch) {
+ const systemAppsMap = new Map();
+ const nonDeletableSystemAppsMap = new Map();
+ for (const systemApp of objectGraph.bag.systemApps) {
+ const bundleId = serverData.asString(systemApp, "bundle-id", "coercible");
+ const adamId = serverData.asString(systemApp, "id", "coercible");
+ if (isSome(bundleId) && isSome(adamId)) {
+ systemAppsMap.set(adamId, bundleId);
+ }
+ }
+ for (const nonDeletableSystemApp of objectGraph.bag.nonDeletableSystemApps) {
+ const bundleId = serverData.asString(nonDeletableSystemApp, "bundle-id", "coercible");
+ const adamId = serverData.asString(nonDeletableSystemApp, "id", "coercible");
+ if (isSome(bundleId) && isSome(adamId)) {
+ nonDeletableSystemAppsMap.set(adamId, bundleId);
+ }
+ }
+ const unsupportedSystemApps = new Map();
+ implementation = {
+ bundleIdFromData(data) {
+ const mappedBundleId = systemAppsMap.get(data.id);
+ if (!serverData.isNull(mappedBundleId)) {
+ return mappedBundleId;
+ }
+ else {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId");
+ }
+ },
+ isSystemAppFromData(data) {
+ return systemAppsMap.has(data.id);
+ },
+ isUnsupportedDeletableSystemAppFromData(data) {
+ if (systemAppsMap.has(data.id) && !nonDeletableSystemAppsMap.has(data.id)) {
+ // We must use the bundleID from the bag here, rather than a bundleID from `data`,
+ // as AppConduit is expecting the bundleIDs which are provided in the bag
+ const bundleId = systemAppsMap.get(data.id);
+ if (serverData.isDefinedNonNullNonEmpty(bundleId)) {
+ const valueFromMap = unsupportedSystemApps.get(bundleId);
+ if (isSome(valueFromMap)) {
+ return valueFromMap;
+ }
+ const isUnsupported = !objectGraph.client.deletableSystemAppCanBeInstalledOnWatchWithBundleID(bundleId);
+ unsupportedSystemApps.set(bundleId, isUnsupported);
+ return isUnsupported;
+ }
+ }
+ return false;
+ },
+ adamIdFromSystemBundleId(bundleId) {
+ for (const [key, value] of systemAppsMap) {
+ if (value === bundleId) {
+ return key;
+ }
+ }
+ return undefined;
+ },
+ };
+ }
+ else {
+ implementation = {
+ bundleIdFromData(data) {
+ return contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId");
+ },
+ isSystemAppFromData(data) {
+ return mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp");
+ },
+ isUnsupportedDeletableSystemAppFromData(data) {
+ return false;
+ },
+ adamIdFromSystemBundleId(bundleId) {
+ // This path shouldn't run on any other platforms.
+ return undefined;
+ },
+ };
+ }
+ return implementation;
+}
+//# sourceMappingURL=sad.js.map \ No newline at end of file