summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/lockups
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/lockups
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/lockups')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js439
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js25
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js1920
3 files changed, 2384 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js
new file mode 100644
index 0000000..f668735
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js
@@ -0,0 +1,439 @@
+/**
+ * Builder code specific to Ad Lockups
+ */
+import { isSome } from "@jet/environment";
+import { AdInteractionAction, AdTransparencyAction, CompoundAction, Screenshots, SearchAd, SearchAdOpportunity, } from "../../api/models";
+import { AdvertActionMetrics, } from "../../api/models/metrics/advert-action-metrics";
+import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, asBooleanOrFalse, asArray, } from "../../foundation/json-parsing/server-data";
+import { attributeAsArrayOrEmpty, attributeAsBooleanOrFalse, attributeAsDictionary, attributeAsString, } from "../../foundation/media/attributes";
+import { advertInstanceIdForData } from "../ads/ad-common";
+import * as content from "../content/content";
+import { addClickEventsToAdLockup } from "../metrics/helpers/clicks";
+import { addImpressionsFieldsToAd, impressionOptionsForLockup } from "../metrics/helpers/impressions";
+import { metricsPlatformDisplayStyleFromData } from "../metrics/helpers/util";
+import { appDataHasVariant } from "../product-page/product-page-variants";
+import { adLogger } from "../search/search-ads";
+import { asString } from "@apple-media-services/media-api";
+// region Shared
+/**
+ * Whether or not data is for ad.
+ *
+ * Note that this attribute *may not be initially part of the original response*.
+ * - Sponsored Search: Since Ad Rotation, `iad` attribute is populated from contents of `iads` dictionary.
+ * - Search Landing: `iad` attribute is populated from contents of `OnDeviceAdvert`.
+ *
+ * @param data Data to check if it is an ad.
+ * @see`iadAttributesForType` and `decorateiAdAttributeFromOnDeviceAd`
+ */
+export function isAdvert(objectGraph, data) {
+ const adDictionary = attributeAsDictionary(data, "iad");
+ return isDefinedNonNullNonEmpty(adDictionary);
+}
+// endregion
+// region Data Overrides
+/**
+ * Populates Ad-specific customizations for given lockup to a **otherwise fully configured ad lockup**
+ * @param objectGraph The object graph.
+ * @param data Data that will provide the ad data.
+ * @param lockup The lockup to modify.
+ * @param metricsOptions The lockup metrics options.
+ * @param applyAdOfferDisplayProperties Whether to apply the default ad OfferDisplayProperties. Some callers of this function want to enforce their own offer styling.
+ */
+export function performAdOverridesforLockup(objectGraph, data, lockup, metricsOptions, applyAdOfferDisplayProperties = true) {
+ // If the lockup is not an ad, it's ad eligible. Just apply the metrics modifications and return early.
+ if (!metricsOptions.isAdvert) {
+ performMetricsOverridesForLockup(objectGraph, data, lockup, metricsOptions);
+ return;
+ }
+ let searchAd;
+ if (objectGraph.props.enabled("advertSlotReporting")) {
+ lockup.searchAdOpportunity = searchAdOpportunityFromData(objectGraph, data, metricsOptions.pageInformation);
+ searchAd = lockup.searchAdOpportunity.searchAd;
+ }
+ else {
+ lockup.searchAd = searchAdFromData(objectGraph, data, metricsOptions.pageInformation);
+ searchAd = lockup.searchAd;
+ }
+ /**
+ * Advert Action Metrics (AzulC+)
+ */
+ const advertType = content.isArcadeSupported(objectGraph, data)
+ ? "arcadeApp"
+ : "standardApp";
+ const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation);
+ const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId");
+ const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder");
+ const purchaseType = isPreorder ? "preorder" : "standard";
+ const clickActionAdActionMetrics = new AdvertActionMetrics(searchAd.instanceId, data.id, bundleId, advertType, "advertPressed", purchaseType, reportingDestination);
+ lockup.clickAction = attachAdActionMetricsToAction(objectGraph, lockup.clickAction, clickActionAdActionMetrics);
+ const buttonActionAdActionMetrics = new AdvertActionMetrics(searchAd.instanceId, data.id, bundleId, advertType, "offerButtonPress", purchaseType, reportingDestination);
+ lockup.buttonAction = attachAdActionMetricsToAction(objectGraph, lockup.buttonAction, buttonActionAdActionMetrics);
+ lockup.itemBackground = objectGraph.props.enabled("insetAdItemBackground") ? "insetAd" : "ad";
+ // Offer button style
+ if (lockup.offerDisplayProperties && applyAdOfferDisplayProperties) {
+ lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "colored", "ad");
+ }
+ // Rating
+ if (!attributeAsBooleanOrFalse(data, "iad.format.userRating")) {
+ lockup.rating = null;
+ lockup.ratingCount = null;
+ }
+ performMetricsOverridesForLockup(objectGraph, data, lockup, metricsOptions);
+}
+/**
+ * Whether or not an advert is eligible to use CPP deeplinks, regardless of whether or not one exists
+ * @param data Data that will provide the ad data.
+ */
+export function isCppDeeplinkEnabledForAdvert(data) {
+ const iAd = attributeAsDictionary(data, "iad");
+ const searchAdCppDeepLinkEnabled = asBooleanOrFalse(iAd, "passthroughAdInfo.deepLinkEligible");
+ const targetedAdCppDeeplinkingEnabled = asBooleanOrFalse(data, "meta.passthroughAdInfo.deepLinkEligible");
+ return searchAdCppDeepLinkEnabled || targetedAdCppDeeplinkingEnabled;
+}
+/**
+ * Whether or not a custom creative advert is eligible to use CPP deeplinks, regardless of whether or not one exists
+ * @param data Data that will provide the ad data.
+ */
+export function isCustomCreativeDeeplinkEnabledForAdvert(data) {
+ const extraAdInfo = getExtraAdInfo(data);
+ if (isSome(extraAdInfo)) {
+ return asBooleanOrFalse(extraAdInfo, "passthroughAdInfo.deepLinkEligible");
+ }
+ else {
+ return asBooleanOrFalse(data, "meta.passthroughAdInfo.deepLinkEligible");
+ }
+}
+/**
+ * The custom creative ad deeplink url, if there is one.
+ * @param data Data that will provide the ad data.
+ */
+export function getCustomCreativeDeepLinkUrl(data) {
+ const extraAdInfo = getExtraAdInfo(data);
+ const creativeDetails = asArray(extraAdInfo, "creativeDetails");
+ if (isSome(creativeDetails)) {
+ return asString(creativeDetails[0], "deepLink");
+ }
+ else {
+ return asString(data, "meta.alignedRegionDetails.deepLink");
+ }
+}
+/**
+ * The tapDestination of the custom creative advert which is also the cppId for the PP.
+ * @param data Data that will provide the ad data.
+ */
+export function getTapDestinationIdForAdvert(data) {
+ const extraAdInfo = getExtraAdInfo(data);
+ const creativeDetails = asArray(extraAdInfo, "creativeDetails");
+ if (isSome(creativeDetails)) {
+ return asString(creativeDetails[0], "tapDestination");
+ }
+ else {
+ return asString(data, "meta.alignedRegionDetails.tapDestination");
+ }
+}
+/**
+ * Local function to extract the extraAdInfo from the meta in the data.
+ * @param data Data that will provide the ad data.
+ */
+function getExtraAdInfo(data) {
+ const extraAdInfoString = asString(data, "meta.extraAdInfo");
+ if (isSome(extraAdInfoString)) {
+ return JSON.parse(extraAdInfoString);
+ }
+ return null;
+}
+/**
+ * Perform overrides for metrics for Ad Lockups
+ * @param data Data for ad lockup
+ * @param lockup Lockup to apply overrides to
+ * @param baseMetricsOptions Base metrics options to extend.
+ */
+function performMetricsOverridesForLockup(objectGraph, data, lockup, baseMetricsOptions) {
+ const platformDisplayStyle = metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, null);
+ const impressionsMetricOptions = impressionOptionsForLockup(objectGraph, data, lockup, platformDisplayStyle, baseMetricsOptions, true);
+ addClickEventsToAdLockup(objectGraph, lockup, impressionsMetricOptions);
+ addImpressionsFieldsToAd(objectGraph, lockup, impressionsMetricOptions, impressionsMetricOptions.pageInformation.iAdInfo);
+}
+/**
+ * Creates a template type string for the medium ad format
+ * @param lockup The lockup whose artwork to use for the template type string
+ * @returns The native ad template type for the medium format ad
+ */
+export function getTemplateTypeForMediumAdFromLockupWithScreenshots(screenshots) {
+ var _a;
+ const templateString = [
+ "MEDRIVER_",
+ "U",
+ "I",
+ ((_a = screenshots === null || screenshots === void 0 ? void 0 : screenshots.artwork) !== null && _a !== void 0 ? _a : []).length, // Count of assets used.
+ ].join("");
+ return templateString;
+}
+/**
+ * Creates a template type string for the medium ad format
+ * @param lockup The lockup whose artwork to use for the template type string
+ * @returns The native ad template type for the medium format ad
+ */
+export function getTemplateTypeForMediumAdFromLockupWithCustomCreative() {
+ const templateString = [
+ "MEDRIVER_",
+ "U",
+ "I",
+ 1,
+ "_2x1", // Aspect Ratio used.
+ ].join("");
+ return templateString;
+}
+/**
+ * Create an opportunity for an ad model
+ * @param objectGraph The object graph.
+ * @param data Data to build `SearchAd` for.
+ * @param pageInformation Metrics page information.
+ * @returns The search ad opportunity represented in the data
+ */
+export function searchAdOpportunityFromData(objectGraph, data, pageInformation) {
+ const searchAd = searchAdFromData(objectGraph, data, pageInformation);
+ const lifecycleEventPayloads = searchAdLifecycleEventPayloads(searchAd.instanceId, pageInformation);
+ return new SearchAdOpportunity(searchAd.instanceId, lifecycleEventPayloads, searchAd);
+}
+/**
+ * Create a missed opportunity for an ad model. This differentiates from `searchAdOpportunityFromData` because the
+ * opportunity is represented by an organic content that is in an ad eligible position.
+ * @param objectGraph The object graph.
+ * @param pageInformation Metrics page information.
+ * @returns The search ad opportunity for the missed opportunity
+ */
+export function searchAdMissedOpportunityFromId(objectGraph, pageInformation) {
+ let adInstanceId;
+ const placementType = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo.placementType;
+ if (isDefinedNonNull(placementType) && objectGraph.props.enabled("advertSlotReporting")) {
+ try {
+ adInstanceId = objectGraph.ads.getIdentifierForMissedOpportunity(placementType);
+ }
+ catch {
+ adInstanceId = objectGraph.random.nextUUID();
+ adLogger(objectGraph, `Error: getIdentifierForMissedOpportunity threw exception. Assigned ${adInstanceId}`);
+ }
+ }
+ else {
+ adInstanceId = objectGraph.random.nextUUID();
+ adLogger(objectGraph, `Error: placementType was null or empty. Assigned ${adInstanceId}`);
+ }
+ const lifecycleEventPayloads = searchAdLifecycleEventPayloads(adInstanceId, pageInformation);
+ return new SearchAdOpportunity(adInstanceId, lifecycleEventPayloads);
+}
+/**
+ * Create the backing search ad model for any representation of ad.
+ * @param objectGraph The object graph.
+ * @param data Data to build `SearchAd` for.
+ * @param pageInformation Information of the page hosting the lockup
+ * @returns The search ad model
+ */
+function searchAdFromData(objectGraph, data, pageInformation) {
+ let instanceId = advertInstanceIdForData(objectGraph, data);
+ if (isNull(instanceId) || instanceId.length === 0) {
+ instanceId = objectGraph.random.nextUUID();
+ adLogger(objectGraph, `Error: instanceId was null or empty. Assigned ${instanceId}`);
+ }
+ const iAd = attributeAsDictionary(data, "iad");
+ const impressionId = attributeAsString(data, "iad.impressionId");
+ const transparencyData = attributeAsString(data, "iad.privacy");
+ const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId");
+ const transparencyAction = new AdTransparencyAction(transparencyData);
+ transparencyAction.title = objectGraph.adsLoc.string("IAD_PRIVACY_MARKER_BUTTON_TITLE");
+ const advertType = content.isArcadeSupported(objectGraph, data)
+ ? "arcadeApp"
+ : "standardApp";
+ const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder");
+ const purchaseType = isPreorder ? "preorder" : "standard";
+ const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, pageInformation);
+ const adActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "markerPress", purchaseType, reportingDestination);
+ const lifecycleEventPayloads = searchAdLifecycleEventPayloads(instanceId, pageInformation);
+ const action = attachAdActionMetricsToAction(objectGraph, transparencyAction, adActionMetrics);
+ return new SearchAd(instanceId, iAd, lifecycleEventPayloads, impressionId, action);
+}
+/**
+ * Create the backing metadata for any representation of ad opportunity.
+ * @param instanceId The identifier for the ad opportunity we're tracking
+ * @param pageInformation Information of the page hosting the lockup
+ * @returns Payloads to be sent to PromotedContent
+ */
+export function searchAdLifecycleEventPayloads(instanceId, pageInformation) {
+ const pageIdentifierRaw = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.baseFields.pageId;
+ const pageIdentifier = typeof pageIdentifierRaw === "string" ? pageIdentifierRaw : "unknown";
+ return {
+ placed: {
+ adInstanceId: instanceId,
+ pageIdentifier: pageIdentifier,
+ },
+ pageEnter: {
+ pageIdentifier: pageIdentifier,
+ },
+ pageExit: {
+ pageIdentifier: pageIdentifier,
+ },
+ onScreen: {
+ adInstanceId: instanceId,
+ },
+ offScreen: {
+ adInstanceId: instanceId,
+ },
+ visible: {
+ adInstanceId: instanceId,
+ },
+ completed: {
+ adInstanceId: instanceId,
+ },
+ };
+}
+/**
+ * Populates Ad-specific customizations for given card to an otherwise fully configured Today card
+ * @param objectGraph The object graph.
+ * @param data Data that will provide the ad data.
+ * @param card The Today card to modify.
+ * @param metricsOptions The card metrics options.
+ */
+export function performAdOverridesForCard(objectGraph, data, card, metricsOptions) {
+ let instanceId = advertInstanceIdForData(objectGraph, data);
+ if (isNull(instanceId) || instanceId.length === 0) {
+ instanceId = objectGraph.random.nextUUID();
+ adLogger(objectGraph, `Error: instanceId was null or empty. Assigned ${instanceId}`);
+ }
+ const advertType = content.isArcadeSupported(objectGraph, data)
+ ? "arcadeApp"
+ : "standardApp";
+ const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation);
+ const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId");
+ const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder");
+ const purchaseType = isPreorder ? "preorder" : "standard";
+ const adActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "advertPressed", purchaseType, reportingDestination);
+ card.clickAction = attachAdActionMetricsToAction(objectGraph, card.clickAction, adActionMetrics);
+}
+/**
+ * Attaches `AdActionMetrics` to an existing action enabling native ad interaction instrumentation.
+ * This works by wrapping the existing action, along with a new `AdInteractionAction`, in a `CompoundAction`, which are executed at the same time.
+ *
+ * @param objectGraph The Object Graph.
+ * @param existingAction The existing Action to wrap.
+ * @param adActionMetrics The adActionMetrics object for the `AdInteractionAction`.
+ * @returns The new Action containing the existing and new Actions.
+ */
+export function attachAdActionMetricsToAction(objectGraph, existingAction, adActionMetrics) {
+ const adInteractionAction = new AdInteractionAction(adActionMetrics);
+ const action = new CompoundAction([adInteractionAction, existingAction]);
+ // Re-use the title from the existing action.
+ action.title = existingAction.title;
+ return action;
+}
+// endregion
+// region Asset Overrides
+/**
+ * Performs iAd Asset overrides, where a field dictates which assets to use
+ * @param data The data to source overrides from
+ * @param lockup The lockup to modify
+ * @param metricsOptions The metrics options that this operation has side effects on.
+ */
+export function performAssetOverridesForMixedMediaAdLockupIfNeeded(objectGraph, data, lockup, metricsOptions) {
+ if (!shouldPerformAssetOverridesForData(objectGraph, data)) {
+ return;
+ }
+ const iAdAssetOverrides = attributeAsArrayOrEmpty(data, "iad.assetOverride");
+ if (iAdAssetOverrides.length) {
+ const didApplyAssetOverrides = applyAssetOverridesToMixedMediaAdLockup(objectGraph, lockup, iAdAssetOverrides);
+ if (metricsOptions.pageInformation.iAdInfo && metricsOptions.pageInformation.iAdInfo.iAdIsPresent) {
+ if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) {
+ metricsOptions.pageInformation.iAdInfo.setSpecifiedAlignedRegionUsed(didApplyAssetOverrides);
+ }
+ }
+ }
+}
+/**
+ * Whether or not an asset override should occur.
+ * This is used to control specific scenarios when asset overrides should not occur, e.g. when it clashes with other features.
+ *
+ * @param data The apps resource to determine if asset overrides should be done for.
+ */
+function shouldPerformAssetOverridesForData(objectGraph, data) {
+ /**
+ * If an AB Test is being run on given app, don't accept the override.
+ */
+ const appHasABTest = appDataHasVariant(objectGraph, data, "abExperiment");
+ return !appHasABTest;
+}
+/**
+ * Apply asset overrides to the ad lock up, based on a given list of overrides
+ * @param lockup Lockup to modify
+ * @param iAdAssetOverrides Asset overrides specified
+ * @return {boolean} - Return whether or not the overrides were applied
+ */
+function applyAssetOverridesToMixedMediaAdLockup(objectGraph, lockup, iAdAssetOverrides) {
+ const unusedAssetOverrides = new Set(iAdAssetOverrides);
+ const videoOverrides = [];
+ const screenshotOverrides = [];
+ const isOverrideMedia = function (url, checksum) {
+ if (unusedAssetOverrides.size === 0) {
+ return false;
+ }
+ for (const assetOverride of iAdAssetOverrides) {
+ if (assetOverride === checksum || url.indexOf(assetOverride) !== -1) {
+ unusedAssetOverrides.delete(assetOverride);
+ return true;
+ }
+ }
+ return false;
+ };
+ if (iAdAssetOverrides.length && (lockup.screenshots.length || lockup.trailers.length)) {
+ if (lockup.trailers.length) {
+ for (const video of lockup.trailers[0].videos) {
+ if (isOverrideMedia(video.videoUrl, video.preview.checksum)) {
+ videoOverrides.push(video);
+ }
+ }
+ }
+ if (lockup.screenshots.length) {
+ for (const screenshot of lockup.screenshots[0].artwork) {
+ if (isOverrideMedia(screenshot.template, screenshot.checksum)) {
+ screenshotOverrides.push(screenshot);
+ }
+ }
+ }
+ }
+ if (unusedAssetOverrides.size === 0 && (videoOverrides.length || screenshotOverrides.length)) {
+ if (lockup.trailers.length) {
+ lockup.trailers[0].videos = videoOverrides;
+ }
+ if (lockup.screenshots.length) {
+ lockup.screenshots[0] = new Screenshots(screenshotOverrides, lockup.screenshots[0].mediaPlatform);
+ }
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+// endregion
+// region Reporting Destionation
+/**
+ * Determine the reporting destination based on metrics options
+ */
+export function reportingDestinationFromMetricsOptions(objectGraph, pageInformation) {
+ const iadInfo = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo;
+ if (isNull(iadInfo)) {
+ return "undefined";
+ }
+ const placementType = iadInfo.placementType;
+ switch (placementType) {
+ case "searchLanding":
+ return "promotedContent"; // SLP uses PC-backed Journey reporting
+ case "searchResults":
+ return "searchAds"; // SRP uses SA-backed Journey reporting
+ case "today":
+ case "productPageYMAL":
+ case "productPageYMALDuringDownload":
+ return "promotedContent"; // Chainlink uses PC-backed Journey reporting
+ default:
+ throw new Error(`This method should never be called with value: ${placementType}`);
+ }
+}
+// endregion
+//# sourceMappingURL=ad-lockups.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js
new file mode 100644
index 0000000..7273a4f
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js
@@ -0,0 +1,25 @@
+import * as mediaAttributes from "../../foundation/media/attributes";
+/**
+ * Use to obtain an clientIdentifier for an editorial context.
+ * @param data The data from a media response
+ * @return {ClientIdentifier} A client identifier that corresponds to the given editorial context.
+ */
+export function clientIdentifierForEditorialContextInData(objectGraph, data) {
+ const rawEditorialContext = mediaAttributes.attributeAsString(data, "editorialContext");
+ switch (rawEditorialContext) {
+ case "Watch":
+ return "com.apple.AppStore.BridgeStoreExtension" /* ClientIdentifier.AppStore_BridgeStoreExtension */;
+ case "Messages":
+ return "com.apple.MobileSMS" /* ClientIdentifier.MobileSMS */;
+ case "RealityDevice":
+ if (objectGraph.bag.enableDeviceDrivenDiscoveryContent) {
+ return "VisionAppStore" /* ClientIdentifier.VisionAppStore */;
+ }
+ else {
+ return null;
+ }
+ default:
+ return null;
+ }
+}
+//# sourceMappingURL=editorial-context.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js
new file mode 100644
index 0000000..1de013a
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js
@@ -0,0 +1,1920 @@
+//
+// lockups.ts
+// AppStoreKit
+//
+// Created by Jonathan Ellenbogen on 11/15/16.
+// Copyright (c) 2016 Apple Inc. All rights reserved.
+//
+import * as validation from "@jet/environment/json/validation";
+import { isNothing, isSome } from "@jet/environment/types/optional";
+import { makeBundlePageIntent } from "../../api/intents/bundle-page-intent";
+import { makeEditorialPageIntentByID } from "../../api/intents/editorial/editorial-page-intent";
+import { makeGroupingPageIntentByID } from "../../api/intents/grouping-page-intent";
+import { makeProductPageIntent } from "../../api/intents/product-page-intent";
+import { makeRoutableArticlePageIntent, } from "../../api/intents/routable-article-page-intent";
+import * as models from "../../api/models";
+import { AdvertActionMetrics, } from "../../api/models/metrics/advert-action-metrics";
+import * as productPageUtil from "../../common/product-page/product-page-util";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import { editorialCardFromData } from "../../foundation/media/associations";
+import * as mediaAttributes from "../../foundation/media/attributes";
+import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes";
+import { platformAttributeAsDictionary } from "../../foundation/media/platform-attributes";
+import * as mediaRelationship from "../../foundation/media/relationships";
+import * as mediaUrlBuilder from "../../foundation/media/url-builder";
+import { BuyParameters } from "../../foundation/metrics/buy-parameters";
+import * as http from "../../foundation/network/http";
+import { InAppPurchaseInstallPageParameters, Parameters, Path, ProductPageParameters, Protocol, } from "../../foundation/network/url-constants";
+import * as urls from "../../foundation/network/urls";
+import * as color from "../../foundation/util/color-util";
+import * as objects from "../../foundation/util/objects";
+import * as gameModels from "../../gameservicesui/src/api/data-models/game";
+import { makeClickMetrics } from "../../gameservicesui/src/utility/metrics";
+import * as adCommon from "../ads/ad-common";
+import * as appEvent from "../app-promotions/app-event";
+import * as appPromotionsCommon from "../app-promotions/app-promotions-common";
+import * as arcadeCommon from "../arcade/arcade-common";
+import * as arcadeUpsell from "../arcade/arcade-upsell";
+import * as mediaUrlMapping from "../builders/url-mapping";
+import * as ageRatings from "../content/age-ratings";
+import { createArtworkForResource } from "../content/artwork/artwork";
+import * as contentAttributes from "../content/attributes";
+import * as content from "../content/content";
+import * as flowPreview from "../content/flow-preview";
+import { isMediaDark } from "../editorial-pages/editorial-media-util";
+import * as editorialComponentMediaUtil from "../editorial-pages/editorial-page-component-media-util";
+import { makeEditorialPageURL } from "../editorial-pages/editorial-page-intent-controller-utils";
+import * as filtering from "../filtering";
+import { makeGroupingPageCanonicalURL } from "../grouping/grouping-page-url";
+import * as externalDeepLink from "../linking/external-deep-link";
+import { getLocale } from "../locale";
+import * as metricsHelpersClicks from "../metrics/helpers/clicks";
+import * as metricsHelpersImpressions from "../metrics/helpers/impressions";
+import * as metricsHelpersLocation from "../metrics/helpers/location";
+import * as metricsHelpersModels from "../metrics/helpers/models";
+import * as metricsUtil from "../metrics/helpers/util";
+import * as offerFormatting from "../offers/offer-formatting";
+import * as offers from "../offers/offers";
+import { getPlatform, inferPreviewPlatformFromDeviceFamilies } from "../preview-platform";
+import * as variants from "../product-page/product-page-variants";
+import * as reviews from "../product-page/reviews";
+import { customCreativeArtworkFromData, customCreativeVideoFromData } from "../search/custom-creative";
+import * as metadataRibbon from "../search/metadata-ribbon/metadata-ribbon";
+import * as searchTagsRibbon from "../search/metadata-ribbon/search-tags-ribbon";
+import * as sharing from "../sharing";
+import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils";
+import * as adLockup from "./ad-lockups";
+import { reportingDestinationFromMetricsOptions } from "./ad-lockups";
+export const forTesting = {
+ copyDataIntoLockup,
+};
+export function offerTypeForMediaType(objectGraph, type, supportsArcade) {
+ switch (type) {
+ case "in-apps":
+ return supportsArcade ? "arcade" : "inAppPurchase";
+ default:
+ return supportsArcade ? "arcadeApp" : "app";
+ }
+}
+function isArcadeLockupWordmarkSupported(objectGraph) {
+ if (objectGraph.client.isTV && objectGraph.host.clientIdentifier === "com.apple.Arcade") {
+ return false;
+ }
+ else {
+ return true;
+ }
+}
+/**
+ * Takes the metadata ribbon items type candidates from the `SearchExperimentData` and determines which should be shown in each slot,
+ * prioritizing the natural order from the candidates arrays
+ * @param objectGraph The ObjectGraph instance for the App Store
+ * @param data The data representing the mixed media lockup data
+ * @param lockup The lockup object the metadata object will be added to
+ * @param searchExperimentData The experiment data for the search experiments, to use its displayStyle object
+ */
+function copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentDataForPage, options) {
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
+ if (serverData.isNullOrEmpty(data)) {
+ return;
+ }
+ lockup.showMetadataInformationInLockup = objectGraph.bag.isLLMSearchTagsEnabled || objectGraph.client.isPad;
+ /// This needs to be guarded against iPhone as natively this will affect all iOS UI components, including iPad.
+ if (!objectGraph.client.isPhone) {
+ return;
+ }
+ /// If the individual lockup has a metadata ribbon field, that takes precedence over the page level metadata ribbon
+ const searchExperimentDataForLockup = serverData.asDictionary(data, "meta");
+ const metadataRibbonItemSlots = (_b = (_a = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _a === void 0 ? void 0 : _a.metadataRibbon) !== null && _b !== void 0 ? _b : (_c = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _c === void 0 ? void 0 : _c.metadataRibbon;
+ let llmMetadataRibbonItems = [];
+ if (objectGraph.bag.isLLMSearchTagsEnabled) {
+ llmMetadataRibbonItems =
+ (_g = (_e = (_d = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _d === void 0 ? void 0 : _d.llmRibbon) !== null && _e !== void 0 ? _e : (_f = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _f === void 0 ? void 0 : _f.metadataRibbon) !== null && _g !== void 0 ? _g : (_h = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _h === void 0 ? void 0 : _h.metadataRibbon;
+ }
+ else {
+ llmMetadataRibbonItems =
+ (_k = (_j = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _j === void 0 ? void 0 : _j.metadataRibbon) !== null && _k !== void 0 ? _k : (_l = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _l === void 0 ? void 0 : _l.metadataRibbon;
+ }
+ const metadataRibbonItems = metadataRibbon.createMetadataRibbonItemsForLockup(objectGraph, data, lockup, metadataRibbonItemSlots, options);
+ const searchTagsRibbonItems = searchTagsRibbon.createSearchTagsRibbonItemsForLockup(objectGraph, data, lockup, llmMetadataRibbonItems, options);
+ lockup.metadataRibbonItems = metadataRibbonItems;
+ lockup.searchTagRibbonItems = searchTagsRibbonItems;
+ lockup.shouldEvenlyDistributeRibbonItems = !objectGraph.bag.isLLMSearchTagsEnabled;
+}
+/**
+ * Configures all the data into the lockup object
+ * @param objectGraph The ObjectGraph instance for the App Store
+ * @param data The branch IAP used by the contingent offer
+ * @param lockup The lockup object the metadata object will be added to
+ * @param options Lockup configuration options
+ * @param isParentAppFree A flag used when creating an IAP lockup with missing parent app data in its relationships property.
+ */
+function copyDataIntoLockup(objectGraph, data, lockup, options, parentData, copyAdditionalDataIntoLockup) {
+ if (!data) {
+ return;
+ }
+ validation.context("copyDataIntoLockup", () => {
+ var _a, _b, _c, _d, _e, _f;
+ const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder");
+ // Drop this item if it's not a preorder, but contained in a pre-order exclusive shelf.
+ if (options.isContainedInPreorderExclusiveShelf && !isPreorder) {
+ return null;
+ }
+ const attributePlatform = (_a = options === null || options === void 0 ? void 0 : options.attributePlatformOverride) !== null && _a !== void 0 ? _a : contentAttributes.defaultAttributePlatform(objectGraph);
+ const productVariantData = variants.productVariantDataForData(objectGraph, data);
+ lockup.productVariantID = variants.productVariantIDForVariantData(productVariantData);
+ options.metricsOptions.productVariantData = productVariantData;
+ lockup.adamId = data.id;
+ const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId", options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ lockup.bundleId = bundleId;
+ lockup.decorations = [];
+ let clientIdentifierOverride = null;
+ if (options && options.clientIdentifierOverride) {
+ clientIdentifierOverride = options.clientIdentifierOverride;
+ }
+ lockup.icon = content.iconFromData(objectGraph, data, {
+ useCase: options.artworkUseCase,
+ withJoeColorPlaceholder: options.useJoeColorIconPlaceholder,
+ joeColorPlaceholderSelectionLogic: options.joeColorPlaceholderSelectionLogic,
+ overrideTextColorKey: options.overrideArtworkTextColorKey,
+ }, clientIdentifierOverride, productVariantData, options.attributePlatformOverride);
+ if (options && options.titleObjectPath) {
+ lockup.title = contentAttributes.contentAttributeAsString(objectGraph, data, options.titleObjectPath, options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ }
+ else {
+ lockup.title = mediaAttributes.attributeAsString(data, "name");
+ }
+ if (objectGraph.client.isWeb) {
+ lockup.isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible");
+ }
+ // Only use an ad override language if this is an ad.
+ lockup.useAdsLocale = options.metricsOptions.isAdvert && isSome(objectGraph.bag.adsOverrideLanguage);
+ // Don't display "Apple Arcade" on a lockup if it's not an arcade app or we're running in the
+ // apple arcade app (tvOS)
+ const isArcadeLockup = content.isArcadeSupported(objectGraph, data, options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ if (isArcadeLockup && isArcadeLockupWordmarkSupported(objectGraph) && !options.shouldHideArcadeHeader) {
+ // tvOS uses a 'pill' decoration over artwork to display apple arcade
+ // while everywhere else just uses the heading text slot to display a string
+ if (objectGraph.client.isTV) {
+ lockup.decorations.push("arcade");
+ }
+ else {
+ lockup.heading = options.metricsOptions.isAdvert
+ ? objectGraph.adsLoc.string("Lockup.Heading.Arcade")
+ : objectGraph.loc.string("Lockup.Heading.Arcade");
+ }
+ }
+ if (options.shouldShowFriendsPlayingShowcase) {
+ lockup.decorations.push("showcaseFriendsPlaying");
+ }
+ // Subtitle
+ const allowMultilineTertiaryLabel = !isArcadeLockup && !isPreorder && ((_b = options.isMultilineTertiaryTitleAllowed) !== null && _b !== void 0 ? _b : true);
+ if (!options.isSubtitleHidden && !isBadgeMultilineFromData(objectGraph, data, allowMultilineTertiaryLabel)) {
+ lockup.subtitle = subtitleFromData(objectGraph, data, options);
+ }
+ // Tertiary badge
+ lockup.tertiaryTitle = badgeFromData(objectGraph, data, allowMultilineTertiaryLabel, options.hideCompatibilityBadge);
+ lockup.tertiaryTitleAction = badgeActionFromData(objectGraph, data);
+ lockup.tertiaryTitleArtwork = badgeArtworkFromData(objectGraph, data);
+ lockup.developerTagline = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle", options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ lockup.editorialTagline = content.notesFromData(objectGraph, data, "tagline", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ lockup.editorialDescription = content.notesFromData(objectGraph, data, "standard", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ lockup.shortEditorialDescription = content.notesFromData(objectGraph, data, "short", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ lockup.ageRating = ageRatings.name(objectGraph, data, true);
+ lockup.productDescription = contentAttributes.contentAttributeAsString(objectGraph, data, "description.standard", options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ if ((_c = options === null || options === void 0 ? void 0 : options.shouldShowSupportedPlatformLabel) !== null && _c !== void 0 ? _c : false) {
+ copySupportedPlatformLabelIntoLockup(objectGraph, data, lockup, options);
+ }
+ if (!reviews.shouldSuppressReviews(objectGraph, data)) {
+ const ratingCount = mediaAttributes.attributeAsNumber(data, "userRating.ratingCount");
+ if (ratingCount > 0 || !(options && options.hideZeroRatings)) {
+ lockup.rating = mediaAttributes.attributeAsNumber(data, "userRating.value");
+ const count = mediaAttributes.attributeAsNumber(data, "userRating.ratingCount");
+ const adsOverrideLanguage = options.metricsOptions.isAdvert
+ ? objectGraph.bag.adsOverrideLanguage
+ : null;
+ lockup.ratingCount = objectGraph.loc.formattedCountForPreferredLocale(objectGraph, count, adsOverrideLanguage);
+ }
+ }
+ const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, options.metricsOptions, options.metricsClickOptions);
+ metricsHelpersLocation.pushContentLocation(objectGraph, metricsClickOptions, lockup.title);
+ const offerData = offers.offerDataFromData(objectGraph, data, options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ const includeBetaApps = (_d = options === null || options === void 0 ? void 0 : options.includeBetaApps) !== null && _d !== void 0 ? _d : false;
+ const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, clientIdentifierOverride);
+ const offerButtonMetricsClickOptions = objects.shallowCopyOf(metricsClickOptions);
+ const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, includeBetaApps, metricsPlatformDisplayStyle, offerButtonMetricsClickOptions, "default", options === null || options === void 0 ? void 0 : options.referrerData);
+ const allOfferDiscountData = serverData.asArrayOrEmpty(offerData, "discounts");
+ let buttonAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, offerButtonMetricsClickOptions, "default", clientIdentifierOverride, options.shouldNavigateToProductPage);
+ const cppDeeplinkUrl = contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "customDeepLink", attributePlatform);
+ const isAdvert = adLockup.isAdvert(objectGraph, data);
+ // CPP deeplinks used for ads have a requirement to not be used when targeting criteria is too narrow.
+ const isCppDeeplinkingEnabled = !isAdvert || adLockup.isCppDeeplinkEnabledForAdvert(data);
+ const hasCppDeepLink = isCppDeeplinkingEnabled && (cppDeeplinkUrl === null || cppDeeplinkUrl === void 0 ? void 0 : cppDeeplinkUrl.length) > 0;
+ const hasExternalDeepLink = ((_e = options === null || options === void 0 ? void 0 : options.externalDeepLinkUrl) === null || _e === void 0 ? void 0 : _e.length) > 0;
+ let deepLinkUrl;
+ if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) {
+ if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) {
+ const customCreativeDeepLinkUrl = adLockup.getCustomCreativeDeepLinkUrl(data);
+ const isCustomCreativeDeeplinkingEnabled = isAdvert && adLockup.isCustomCreativeDeeplinkEnabledForAdvert(data);
+ const hasCustomCreativeDeepLink = isCustomCreativeDeeplinkingEnabled && (customCreativeDeepLinkUrl === null || customCreativeDeepLinkUrl === void 0 ? void 0 : customCreativeDeepLinkUrl.length) > 0;
+ if (hasCustomCreativeDeepLink) {
+ deepLinkUrl = customCreativeDeepLinkUrl;
+ }
+ else if (hasCppDeepLink) {
+ deepLinkUrl = cppDeeplinkUrl;
+ }
+ else if (hasExternalDeepLink) {
+ deepLinkUrl = options.externalDeepLinkUrl;
+ }
+ }
+ else if (hasCppDeepLink || hasExternalDeepLink) {
+ deepLinkUrl = hasCppDeepLink ? cppDeeplinkUrl : options.externalDeepLinkUrl;
+ }
+ }
+ else if (hasCppDeepLink || hasExternalDeepLink) {
+ deepLinkUrl = hasCppDeepLink ? cppDeeplinkUrl : options.externalDeepLinkUrl;
+ }
+ if (isSome(deepLinkUrl)) {
+ // Configure cross link as well as deep link action.
+ buttonAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, buttonAction, offerAction.adamId, bundleId, deepLinkUrl, includeBetaApps, offerButtonMetricsClickOptions);
+ // Configure cross link title and subtitle.
+ if (((_f = options.crossLinkSubtitle) === null || _f === void 0 ? void 0 : _f.length) > 0) {
+ lockup.crossLinkTitle = objectGraph.loc.uppercased(mediaAttributes.attributeAsString(data, "name"));
+ lockup.crossLinkSubtitle = options.crossLinkSubtitle;
+ }
+ }
+ lockup.buttonAction = buttonAction;
+ // Beta apps
+ lockup.includeBetaApps = includeBetaApps;
+ lockup.developerName = mediaAttributes.attributeAsString(data, "artistName");
+ if (isNothing(lockup.developerName)) {
+ // Some newer APIs use `developerName` instead of `artistName`.
+ lockup.developerName = mediaAttributes.attributeAsString(data, "developerName");
+ }
+ // Ensure we grab its children.
+ lockup.children = childrenFromLockupData(objectGraph, data, options);
+ if (isSome(copyAdditionalDataIntoLockup)) {
+ copyAdditionalDataIntoLockup();
+ }
+ metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker);
+ const shareSheetData = sharing.shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride);
+ if (shareSheetData) {
+ const shareMenuAction = new models.BlankAction();
+ const shareMetricsOptions = objects.shallowCopyOf(metricsClickOptions);
+ shareMetricsOptions.actionType = "share";
+ shareMetricsOptions.targetType = "lockup";
+ metricsHelpersClicks.addClickEventToAction(objectGraph, shareMenuAction, shareMetricsOptions);
+ const shareMenuData = new models.LockupContextMenuData();
+ shareMenuData.shareAction = shareMenuAction;
+ shareMenuData.shareSheetData = shareSheetData;
+ lockup.contextMenuData = shareMenuData;
+ }
+ const resolvedParentData = parentData !== null && parentData !== void 0 ? parentData : parentDataFromInAppData(objectGraph, data);
+ let isParentAppFree = false;
+ if (resolvedParentData) {
+ const parentOfferData = offers.offerDataFromData(objectGraph, resolvedParentData);
+ const parentPrice = offers.priceFromOfferData(objectGraph, parentOfferData);
+ isParentAppFree = !(parentPrice > 0);
+ }
+ const offerType = offerTypeForMediaType(objectGraph, data.type, isArcadeLockup);
+ if (options) {
+ lockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, data, isPreorder, options.isContainedInPreorderExclusiveShelf, options.offerStyle, options.offerEnvironment, allOfferDiscountData[0], isParentAppFree, "default", options.shouldNavigateToProductPage, options.metricsOptions.isAdvert, null, options.parentAppData, options.isBuyDisallowed);
+ }
+ else {
+ lockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, data, isPreorder, options.isContainedInPreorderExclusiveShelf, null, null, allOfferDiscountData[0], isParentAppFree, "default");
+ }
+ if (!options || !options.skipDefaultClickAction) {
+ if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) {
+ lockup.clickAction = productActionFromData(objectGraph, data, metricsClickOptions, {
+ clientIdentifierOverride: clientIdentifierOverride,
+ productVariantData: productVariantData,
+ alignedRegionDeepLinkUrl: adLockup.getCustomCreativeDeepLinkUrl(data),
+ isCppDeepLinkEligible: isCppDeeplinkingEnabled,
+ });
+ }
+ else {
+ lockup.clickAction = productActionFromData(objectGraph, data, metricsClickOptions, {
+ clientIdentifierOverride: clientIdentifierOverride,
+ productVariantData: productVariantData,
+ isCppDeepLinkEligible: isCppDeeplinkingEnabled,
+ });
+ }
+ }
+ if (options && options.ordinal) {
+ lockup.ordinal = options.ordinal;
+ }
+ const editorialBadgeInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialBadgeInfo", options === null || options === void 0 ? void 0 : options.attributePlatformOverride);
+ const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (editorialBadgeInfo && !supportsVisionOSCompatibleIOSBinary) {
+ const badgeType = serverData.asString(editorialBadgeInfo, "editorialBadgeType");
+ const hasEditorsChoiceBadge = badgeType && badgeType === "editorialPriority";
+ lockup.isEditorsChoice = hasEditorsChoiceBadge;
+ }
+ // Flow preview actions
+ if (!isAdvert) {
+ lockup.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, false, clientIdentifierOverride, lockup.clickAction, options.metricsOptions, metricsClickOptions, options.externalDeepLinkUrl, lockup.subtitle, lockup.title);
+ }
+ const metricsImpressionOptions = metricsHelpersImpressions.impressionOptionsForLockup(objectGraph, data, lockup, metricsPlatformDisplayStyle, options.metricsOptions, options.canDisplayArcadeOfferButton);
+ metricsHelpersImpressions.addImpressionFields(objectGraph, lockup, metricsImpressionOptions);
+ });
+}
+/**
+ * Configures the tertiary icons and label for showing unsupported apps in a bundle.
+ * @param objectGraph The ObjectGraph instance for the App Store
+ * @param data The branch IAP used by the contingent offer
+ * @param lockup The lockup object the metadata object will be added to
+ * @param options Lockup configuration options
+ */
+function copySupportedPlatformLabelIntoLockup(objectGraph, data, lockup, options) {
+ const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data);
+ const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac);
+ const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ const isBuyable = content.buyableOnDevice(objectGraph, data, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleBinary);
+ /// If its buyable no need to display compatibility label
+ if (isBuyable) {
+ return;
+ }
+ const additionalPlatforms = appPlatforms.filter((platform) => platform !== objectGraph.client.deviceType);
+ if (additionalPlatforms.length === 0) {
+ return;
+ }
+ lockup.tertiaryTitleIcons = additionalPlatforms.map((platform) => content.systemImageNameForAppPlatform(platform));
+ if (additionalPlatforms.length === 1) {
+ const platformTitle = content.appPlatformTitle(objectGraph, additionalPlatforms[0]);
+ lockup.tertiaryTitle = objectGraph.loc
+ .string("AppStore.Bundles.ProductPage.OnlyAvailable.Message")
+ .replace("@@platform@@", platformTitle);
+ }
+ else {
+ // We only support showing up to 2 platforms. Grab the first 2
+ const platformTitle1 = content.appPlatformTitle(objectGraph, additionalPlatforms[0]);
+ const platformTitle2 = content.appPlatformTitle(objectGraph, additionalPlatforms[1]);
+ lockup.tertiaryTitle = objectGraph.loc
+ .string("AppStore.Bundles.ProductPage.AvailableOnTwo.Message")
+ .replace("@@platform1@@", platformTitle1)
+ .replace("@@platform2@@", platformTitle2);
+ }
+}
+export function childrenFromLockupData(objectGraph, data, options) {
+ const childrenRelationship = mediaRelationship.relationship(data, "apps");
+ if (childrenRelationship) {
+ const listOptions = {
+ lockupOptions: {
+ ...options,
+ shouldCreateScreenshotsLockup: options === null || options === void 0 ? void 0 : options.shouldIncludeScreenshotsForChildren,
+ },
+ filter: 0 /* filtering.Filter.None */,
+ };
+ return lockupsFromDataContainer(objectGraph, childrenRelationship, listOptions);
+ }
+ return null;
+}
+function copyDataIntoInAppPurchaseLockup(objectGraph, data, lockup, options) {
+ if (!data) {
+ return;
+ }
+ validation.context("copyDataIntoInAppPurchaseLockup", () => {
+ var _a;
+ const parentData = (_a = parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : options.parentAppData;
+ const isStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(parentData, "supportsStreamlinedBuy");
+ const iapData = iapDataFromData(objectGraph, data);
+ data = iapData;
+ copyDataIntoLockup(objectGraph, data, lockup, options, parentData);
+ lockup.productIdentifier = mediaAttributes.attributeAsString(data, "offerName");
+ lockup.parent = lockupFromData(objectGraph, parentData, options);
+ lockup.description = mediaAttributes.attributeAsString(data, "description.standard");
+ lockup.isVisibleByDefault = mediaAttributes.attributeAsBooleanOrFalse(data, "isMerchandisedVisibleByDefault");
+ lockup.isSubscription = mediaAttributes.attributeAsBooleanOrFalse(data, "isSubscription");
+ const offerData = offers.offerDataFromData(objectGraph, data);
+ const discountOfferData = serverData.asArrayOrEmpty(offerData, "discounts");
+ lockup.offerDisplayProperties.hasDiscount = discountOfferData.length > 0;
+ lockup.offerDisplayProperties.subscriptionFamilyId = mediaAttributes.attributeAsString(data, "subscriptionFamilyId");
+ // Action for App Install (needed in case the parent app is not installed)
+ const installRequiredAction = new models.FlowAction("inAppPurchaseInstall");
+ installRequiredAction.presentationContext = "presentModalFormSheet";
+ const installRequiredActionUrl = configureIAPInstallPageUrl(objectGraph, lockup.adamId, parentData.id);
+ installRequiredAction.pageUrl = installRequiredActionUrl;
+ // Install Page
+ const sidepackInstallPage = new models.InAppPurchaseInstallPage();
+ sidepackInstallPage.parentLockup = objects.shallowCopyOf(lockup.parent);
+ sidepackInstallPage.lockup = objects.shallowCopyOf(lockup);
+ sidepackInstallPage.preInstallOfferDescription = offerFormatting.installPagePreInstallTrialDescription(objectGraph, offerData);
+ installRequiredAction.pageData = sidepackInstallPage;
+ const productIdentifier = mediaAttributes.attributeAsString(data, "offerName");
+ const parentBundleId = contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId");
+ const firstVersionSupportingMerchIAP = mediaAttributes.attributeAsString(parentData, "firstVersionSupportingInAppPurchaseApi");
+ const hasDiscountedOffer = serverData.isDefinedNonNullNonEmpty(discountedOfferFromData(data));
+ const metricsOptions = metricsHelpersImpressions.impressionOptionsForLockup(objectGraph, data, lockup, "iap", options.metricsOptions, options.canDisplayArcadeOfferButton);
+ metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, lockup.title);
+ if (isStreamlinedBuy && hasDiscountedOffer) {
+ const inAppPurchaseAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, lockup.parent.buttonAction);
+ if (lockup.parent) {
+ inAppPurchaseAction.appTitle = lockup.parent.title;
+ }
+ inAppPurchaseAction.productTitle = lockup.title;
+ inAppPurchaseAction.streamlineBuyAction = streamlineBuyActionForIAP(objectGraph, data, parentData, lockup, options);
+ lockup.buttonAction = inAppPurchaseAction;
+ lockup.subtitle = mediaAttributes.attributeAsString(parentData, "name");
+ }
+ else if (firstVersionSupportingMerchIAP) {
+ const inAppPurchaseAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, installRequiredAction, firstVersionSupportingMerchIAP);
+ if (lockup.parent) {
+ inAppPurchaseAction.appTitle = lockup.parent.title;
+ }
+ inAppPurchaseAction.productTitle = lockup.title;
+ const clickOptions = {
+ ...options.metricsOptions,
+ id: lockup.adamId,
+ idType: "its_id",
+ actionDetails: { parentAdamId: parentData.id },
+ };
+ metricsHelpersClicks.addClickEventToAction(objectGraph, inAppPurchaseAction, clickOptions);
+ lockup.buttonAction = inAppPurchaseAction;
+ }
+ else {
+ const alert = new models.AlertAction("default");
+ alert.title = objectGraph.loc.string("SEED_IN_APP_UNSUPPORTED_MESSAGE_OPTION_1");
+ alert.message = "";
+ alert.isCancelable = true;
+ lockup.buttonAction = alert;
+ }
+ metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker);
+ // Click action to the product page
+ if (!options || !options.skipDefaultClickAction) {
+ const clickAction = iAPActionFromData(objectGraph, data, metricsOptions);
+ lockup.clickAction = clickAction;
+ lockup.productAction = clickAction;
+ }
+ metricsHelpersImpressions.addImpressionFieldsToInAppPurchaseLockup(objectGraph, lockup, metricsOptions);
+ }, "item.offer.buyParams");
+}
+/**
+ * Creates the discounted buy action for the streamline buy Action
+ * @param objectGraph The ObjectGraph instance for the App Store
+ * @param data The branch IAP used by the contingent offer
+ * @param parentData The branch app used by the contingent offer
+ * @param lockup The lockup object the metadata object will be added to
+ * @param options Lockup configuration options
+ */
+function streamlineBuyActionForIAP(objectGraph, data, parentData, lockup, options) {
+ var _a;
+ const clickOptions = {
+ ...options.metricsOptions,
+ id: parentData.id,
+ targetId: parentData.id,
+ idType: "its_id",
+ actionDetails: { parentAdamId: parentData.id },
+ };
+ const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, options.clientIdentifierOverride);
+ const offerButtonMetricsClickOptions = objects.shallowCopyOf(clickOptions);
+ const discountedOfferData = discountedOfferFromData(data);
+ const offerData = offers.offerDataFromData(objectGraph, data);
+ const appOfferData = offers.offerDataFromData(objectGraph, parentData);
+ const appBuyParams = new BuyParameters(serverData.asString(appOfferData, "buyParams"));
+ let currentBuyParams = (_a = serverData.asString(discountedOfferData, "buyParams")) !== null && _a !== void 0 ? _a : serverData.asString(offerData, "buyParams");
+ currentBuyParams += `&appAdamId=${serverData.asString(parentData, "id")}`;
+ currentBuyParams += `&appExtVrsId=${appBuyParams.get("appExtVrsId", "")}`;
+ currentBuyParams += `&bid=${contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId")}`;
+ currentBuyParams += `&bvrs=1.0`;
+ currentBuyParams += `&offerName=${contentAttributes.contentAttributeAsString(objectGraph, data, "offerName")}`;
+ const offerId = serverData.asString(discountedOfferData, "offerId");
+ if (serverData.isDefinedNonNullNonEmpty(offerId)) {
+ currentBuyParams += `&adHocOfferId=${offerId}`;
+ }
+ offerData["buyParams"] = currentBuyParams;
+ const subscribeAction = offers.offerActionFromOfferData(objectGraph, offerData, data, false, false, metricsPlatformDisplayStyle, offerButtonMetricsClickOptions, "default", options === null || options === void 0 ? void 0 : options.referrerData, false, parentData.id);
+ return subscribeAction;
+}
+function copyDataIntoTrailersLockup(objectGraph, data, lockup, videoConfiguration, options) {
+ if (!data) {
+ return;
+ }
+ validation.context("copyDataIntoTrailersLockup", () => {
+ copyDataIntoLockup(objectGraph, data, lockup, options);
+ lockup.trailers = content.trailersFromData(objectGraph, data, videoConfiguration, options.metricsOptions, lockup.adamId);
+ });
+}
+function copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options, cropCode) {
+ var _a;
+ if (!data) {
+ return;
+ }
+ if (options.isNetworkConstrained) {
+ return;
+ }
+ const isAd = (_a = options.metricsOptions.isAdvert) !== null && _a !== void 0 ? _a : false;
+ validation.context("copyMediaIntoMixedMediaLockup", () => {
+ if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) {
+ if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) {
+ const customCreativeData = platformAttributeAsDictionary(data, contentAttributes.bestAttributePlatformFromData(objectGraph, data), "creativeAttributes");
+ if (isSome(customCreativeData)) {
+ lockup.alignedRegionArtwork = customCreativeArtworkFromData(objectGraph, data, customCreativeData, cropCode);
+ lockup.alignedRegionVideo = customCreativeVideoFromData(objectGraph, data, customCreativeData, videoConfiguration, cropCode);
+ }
+ }
+ }
+ lockup.screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */, null, options.clientIdentifierOverride, null, isAd, cropCode);
+ const firstScreenshots = lockup.screenshots[0];
+ lockup.trailers = [];
+ const trailers = content.trailersFromData(objectGraph, data, videoConfiguration, options.metricsOptions, lockup.adamId, isAd, cropCode);
+ if (serverData.isDefinedNonNull(trailers)) {
+ if (serverData.isNullOrEmpty(firstScreenshots) ||
+ trailers.mediaPlatform.isEqualTo(firstScreenshots.mediaPlatform)) {
+ lockup.trailers.push(trailers);
+ }
+ }
+ });
+}
+/**
+ * Copy the data contained in a platform response into a `ScreenshotsLockup`.
+ * @param data The store platform response data to read from.
+ * @param lockup The ScreenshotsLockup data model to copy store platform data to
+ * @param options Options customizing what data is needed.
+ */
+function copyDataIntoScreenshotsLockup(objectGraph, data, lockup, options) {
+ if (!data) {
+ return;
+ }
+ validation.context("copyDataIntoScreenshotsLockup", () => {
+ copyDataIntoLockup(objectGraph, data, lockup, options);
+ lockup.screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */, null, options.clientIdentifierOverride);
+ });
+}
+/**
+ * Copy the data contained in a platform response into a `PosterLockup`.
+ * @param data The store platform response data to read from.
+ * @param lockup The PosterLockup data model to copy store platform data to
+ * @param options Options customizing what data is needed.
+ */
+function copyDataIntoPosterLockup(objectGraph, data, lockup, options) {
+ if (!data) {
+ return;
+ }
+ validation.context("copyDataIntoPosterLockup", () => {
+ copyDataIntoLockup(objectGraph, data, lockup, options);
+ lockup.epicHeading = content.posterEpicHeadingArtworkFromData(objectGraph, data);
+ lockup.posterArtwork = content.posterArtworkFromData(objectGraph, data);
+ lockup.posterVideo = content.posterEditorialVideoFromData(objectGraph, data, 19 /* content.ArtworkUseCase.GroupingHero */);
+ if (lockup.offerDisplayProperties) {
+ lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "white", "lightOverArtwork");
+ }
+ if (lockup.posterVideo) {
+ lockup.isDark = color.isDarkColor(lockup.posterVideo.preview.backgroundColor);
+ }
+ else if (lockup.posterArtwork) {
+ lockup.isDark = color.isDarkColor(lockup.posterArtwork.backgroundColor);
+ }
+ else {
+ lockup.isDark = false;
+ }
+ const supportsArcade = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade");
+ const isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder");
+ if (isPreorder) {
+ const fallbackString = supportsArcade ? objectGraph.loc.string("Offer.Label.ComingSoon") : null;
+ let preorderDate;
+ if (objectGraph.client.isVision || objectGraph.client.isWeb) {
+ preorderDate = content.dynamicPreorderDateFromData(objectGraph, data, fallbackString);
+ }
+ else {
+ preorderDate = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, data, fallbackString));
+ }
+ if (isSome(preorderDate)) {
+ lockup.footerText = preorderDate;
+ }
+ }
+ });
+}
+export function subtitleFromData(objectGraph, data, lockupOptions = null) {
+ if (isNothing(data)) {
+ return null;
+ }
+ return validation.context("subtitleFromData", () => {
+ let subtitle;
+ if (lockupOptions && lockupOptions.subtitleObjectPath) {
+ subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, lockupOptions.subtitleObjectPath, lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride);
+ }
+ if (serverData.isNullOrEmpty(subtitle)) {
+ subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle", lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride);
+ }
+ if (subtitle) {
+ return subtitle;
+ }
+ else {
+ return categoryFromData(objectGraph, data, lockupOptions);
+ }
+ });
+}
+/**
+ * Creates the compability badge text for showing on a lockup.
+ *
+ * @param objectGraph Current object graph
+ * @param data Product data
+ * @returns Built badge text, or null
+ */
+export function badgeFromData(objectGraph, data, allowMultiline = false, hideCompatibilityBadge) {
+ return validation.context("badgeFromData", () => {
+ if (hideCompatibilityBadge) {
+ return null;
+ }
+ const doesClientSupportMacOSCompatibleIOSBinary = objectGraph.client.isMac || objectGraph.client.isWeb;
+ const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary);
+ const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (supportsMacOSCompatibleIOSBinary || supportsVisionOSCompatibleBinary) {
+ // 1. Build loc string base.
+ let locKey = "";
+ const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data);
+ if (content.supportsPlatform(appPlatforms, "pad")) {
+ locKey = "Platform.DesignedForPad";
+ }
+ else if (content.supportsPlatform(appPlatforms, "phone")) {
+ locKey = "Platform.DesignedForPhone";
+ }
+ if ((locKey === null || locKey === void 0 ? void 0 : locKey.length) > 0) {
+ if (supportsMacOSCompatibleIOSBinary) {
+ // 2. Check if app is verified.
+ const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data);
+ if (!isVerifiedForAppleSiliconMac) {
+ locKey += ".NotVerified";
+ // 3. Use expanded two-line not verified badge.
+ if (allowMultiline) {
+ locKey += ".Expanded";
+ }
+ }
+ }
+ return objectGraph.loc.string(locKey);
+ }
+ }
+ return null;
+ });
+}
+/**
+ * Creates the compability badge artwork for showing on a lockup.
+ *
+ * @param objectGraph Current object graph
+ * @param data Product data
+ * @returns An iPhone/iPad symbol artwork, or null
+ */
+export function badgeArtworkFromData(objectGraph, data) {
+ const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ if (supportsVisionOSCompatibleBinary && objectGraph.client.isVision) {
+ const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data);
+ if (content.supportsPlatform(appPlatforms, "pad")) {
+ return createArtworkForResource(objectGraph, "systemimage://ipad.landscape");
+ }
+ else if (content.supportsPlatform(appPlatforms, "phone")) {
+ return createArtworkForResource(objectGraph, "systemimage://iphone");
+ }
+ }
+ return null;
+}
+export function badgeActionFromData(objectGraph, data) {
+ return validation.context("badgeActionFromData", () => {
+ // Add action for unverified macOS apps.
+ const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac);
+ if (!supportsMacOSCompatibleIOSBinary) {
+ return null;
+ }
+ const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data);
+ if (isVerifiedForAppleSiliconMac) {
+ return null;
+ }
+ const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data);
+ if (!content.supportsPlatform(appPlatforms, "pad") && !content.supportsPlatform(appPlatforms, "phone")) {
+ return null;
+ }
+ const linkAction = new models.FlowAction("article");
+ linkAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.appleSiliconMacUnverifiedBadgeEditorialItemId}`;
+ return linkAction;
+ });
+}
+/// Does the badge require multiple lines?
+export function isBadgeMultilineFromData(objectGraph, data, allowMultiline) {
+ return validation.context("isBadgeMultilineFromData", () => {
+ if (!allowMultiline) {
+ return false;
+ }
+ const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac);
+ if (!supportsMacOSCompatibleIOSBinary) {
+ return false;
+ }
+ const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data);
+ if (isVerifiedForAppleSiliconMac) {
+ return false;
+ }
+ const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data);
+ return content.supportsPlatform(appPlatforms, "pad") || content.supportsPlatform(appPlatforms, "phone");
+ });
+}
+function isVerifiedForAppleSiliconMacFromData(objectGraph, data) {
+ // Don't badge unverified apps when Apple Silicon support is not enabled.
+ if (!objectGraph.appleSilicon.isSupportEnabled) {
+ return true;
+ }
+ // Check response.
+ const isVerifiedForAppleSiliconMac = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "isVerifiedForAppleSiliconMac", "ios");
+ if (serverData.isDefinedNonNull(isVerifiedForAppleSiliconMac)) {
+ return isVerifiedForAppleSiliconMac;
+ }
+ return false;
+}
+export function categoryFromData(objectGraph, data, lockupOptions = null) {
+ return validation.context("categoryFromData", () => {
+ // genreDisplayName is the preferred field to use for the determining the category name
+ // It should always be available, but we have fallbacks below just in case.
+ const genreName = contentAttributes.contentAttributeAsString(objectGraph, data, "genreDisplayName", lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride);
+ if ((genreName === null || genreName === void 0 ? void 0 : genreName.length) > 0) {
+ return genreName;
+ }
+ // Fallback to interating over the genres list
+ const genres = mediaRelationship.relationshipCollection(data, "genres");
+ if (genres.length > 0) {
+ let candidateGenre = genres[0];
+ // If the primary genre is Games, search for the first sub-genre of games
+ const gamesId = 6014 /* constants.GenreIds.Games */.toString();
+ if (candidateGenre.id === gamesId) {
+ for (const genre of genres) {
+ const parentGenreId = mediaAttributes.attributeAsString(genre, "parentId");
+ if (genre.id !== gamesId && parentGenreId === gamesId) {
+ candidateGenre = genre;
+ break;
+ }
+ }
+ }
+ return mediaAttributes.attributeAsString(candidateGenre, "name");
+ }
+ else {
+ // Fallback to genreNames
+ const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames");
+ return genreNames.length > 0 ? genreNames[0] : null;
+ }
+ });
+}
+/**
+ * Configures an internal URL for use as the pageUrl of an 'install' FlowAction,
+ * using the provided parameters.
+ * @param inAppPurchaseAdamId The adamId for the lockup to appear in the header.
+ * @param parentAdamId The adamId for the lockup that is being offered.
+ * @returns A fully configured internal URL for fetching an app install page.
+ */
+export function configureIAPInstallPageUrl(objectGraph, inAppPurchaseAdamId, parentAdamId) {
+ const parameters = new http.FormBuilder()
+ .param(Parameters.id, parentAdamId)
+ .param(InAppPurchaseInstallPageParameters.inAppPurchaseId, inAppPurchaseAdamId)
+ .build();
+ return `${Protocol.internal}:/${Path.product}/${Path.install}/?${parameters}`;
+}
+/**
+ * Configures an internal url to be used as the url for a clickAction from an IAP
+ * lockup to a product page.
+ * @param productUrl The URL for the product.
+ * @param inAppPurchaseAdamId The adamId for the lockup from which the click occurs.
+ * @param inAppPurchaseType The type for the in-app purchase.
+ * @returns A fully configured internal URL for a product click action via IAP.
+ */
+export function configureProductUrlFromInAppPurchase(objectGraph, productUrl, inAppProductIdentifier, isSubscription) {
+ const parameters = new http.FormBuilder()
+ .param(ProductPageParameters.url, productUrl)
+ .param(Parameters.offerName, inAppProductIdentifier)
+ .param(ProductPageParameters.isSubscription, isSubscription.toString())
+ .build();
+ return `${Protocol.internal}:/${Path.product}/${Path.lookup}/?${parameters}`;
+}
+export function lockupFromData(objectGraph, data, options) {
+ return validation.context("lockupFromData", () => {
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
+ if (!data) {
+ validation.unexpectedNull("ignoredValue", "data");
+ return null;
+ }
+ // Must setup iAdInfo before any builder methods.
+ const isAd = adLockup.isAdvert(objectGraph, data);
+ options.metricsOptions.isAdvert = isAd;
+ const isAdEligible = adCommon.isAdEligible((_b = (_a = options.metricsOptions.pageInformation) === null || _a === void 0 ? void 0 : _a.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType, options.metricsOptions.locationTracker);
+ options.metricsOptions.isAdEligible = isAdEligible;
+ if (isAd || isAdEligible) {
+ (_d = (_c = options.metricsOptions.pageInformation) === null || _c === void 0 ? void 0 : _c.iAdInfo) === null || _d === void 0 ? void 0 : _d.apply(objectGraph, data);
+ }
+ if (isAd) {
+ (_f = (_e = options.metricsOptions.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP");
+ }
+ if (!mediaAttributes.hasAttributes(data)) {
+ return null;
+ }
+ switch (data.type) {
+ case "in-apps":
+ options.offerEnvironment = "widthConstrainedLockup";
+ return inAppPurchaseLockupFromData(objectGraph, data, options);
+ case "app-events":
+ const parentAppData = mediaRelationship.relationshipData(objectGraph, data, "app");
+ if (serverData.isNullOrEmpty(parentAppData)) {
+ return null;
+ }
+ const appLockup = new models.Lockup();
+ copyDataIntoLockup(objectGraph, parentAppData, appLockup, options);
+ return appLockup;
+ case "contingent-items":
+ case "offer-items":
+ return appPromotionOfferLockupFromData(objectGraph, data, options);
+ default:
+ const lockup = new models.Lockup();
+ copyDataIntoLockup(objectGraph, data, lockup, options);
+ if (isSome((_g = options.metricsOptions.pageInformation) === null || _g === void 0 ? void 0 : _g.iAdInfo)) {
+ if (isAd || isAdEligible) {
+ adLockup.performAdOverridesforLockup(objectGraph, data, lockup, options.metricsOptions);
+ }
+ if (isAd) {
+ if (objectGraph.props.enabled("advertSlotReporting")) {
+ (_h = lockup.searchAdOpportunity) === null || _h === void 0 ? void 0 : _h.setTemplateType("APPLOCKUP");
+ }
+ else {
+ (_j = lockup.searchAd) === null || _j === void 0 ? void 0 : _j.setTemplateType("APPLOCKUP");
+ }
+ }
+ }
+ return lockup;
+ }
+ });
+}
+/**
+ * Configures a app promotion lockup with a contingent-items or offer-items object
+ * @param objectGraph The App Store Object Graph.
+ * @param data The data for the app to go into the lockup.
+ * @param options A set of options customising the lockup.
+ * @returns A Lockup with the desired configuration.
+ */
+export function appPromotionOfferLockupFromData(objectGraph, data, options) {
+ return validation.context("appPromotionOfferLockupFromData", () => {
+ var _a, _b, _c;
+ const parentData = (_a = parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : options.parentAppData;
+ const iapData = iapDataFromData(objectGraph, data);
+ const supportsStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(parentData, "supportsStreamlinedBuy");
+ if (supportsStreamlinedBuy) {
+ const lockup = inAppPurchaseLockupFromData(objectGraph, data, options);
+ lockup.offerDisplayProperties.titles["standard"] = objectGraph.loc.string("OfferButton.Title.Subscribe");
+ lockup.offerDisplayProperties.isStreamlinedBuy = true;
+ // Override any discounts to force showing "Subscribe" title on the offer button.
+ lockup.offerDisplayProperties.hasDiscount = false;
+ // Setup the artwork
+ const rawArtwork = mediaAttributes.attributeAsDictionary(iapData, "artwork");
+ const backupArtwork = appPromotionsCommon.artworkFromPlatformData(objectGraph, parentData, "artwork");
+ const iapArtwork = content.artworkFromApiArtwork(objectGraph, rawArtwork, {
+ useCase: options.artworkUseCase,
+ withJoeColorPlaceholder: options.useJoeColorIconPlaceholder,
+ style: "iap",
+ overrideTextColorKey: options.overrideArtworkTextColorKey,
+ });
+ lockup.icon = iapArtwork !== null && iapArtwork !== void 0 ? iapArtwork : backupArtwork;
+ return lockup;
+ }
+ else {
+ const lockup = new models.Lockup();
+ copyDataIntoLockup(objectGraph, parentData, lockup, options);
+ const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, options.metricsOptions, options.metricsClickOptions);
+ metricsHelpersLocation.pushContentLocation(objectGraph, metricsClickOptions, lockup.title);
+ const parentBundleId = contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId");
+ const productIdentifier = mediaAttributes.attributeAsString(iapData, "offerName");
+ const firstVersionSupportingMerchIAP = mediaAttributes.attributeAsString(parentData, "firstVersionSupportingInAppPurchaseApi");
+ const offerAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, lockup.buttonAction, firstVersionSupportingMerchIAP);
+ offerAction.appTitle = (_b = mediaAttributes.attributeAsString(parentData, "name")) !== null && _b !== void 0 ? _b : "";
+ offerAction.productTitle = (_c = mediaAttributes.attributeAsString(iapData, "name")) !== null && _c !== void 0 ? _c : "";
+ if (data.type === "offer-items") {
+ const discountedOffer = discountedOfferFromData(iapData);
+ const offerId = serverData.asString(discountedOffer, "offerId");
+ if (isSome(offerId) && offerId.length > 0) {
+ offerAction.additionalBuyParams = "adHocOfferId=" + offerId;
+ }
+ }
+ else {
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
+ offerAction.additionalBuyParams = "contingentItemId=" + data.id;
+ }
+ lockup.buttonAction = offerAction;
+ metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker);
+ return lockup;
+ }
+ });
+}
+export function inAppPurchaseLockupFromData(objectGraph, data, options) {
+ return validation.context("inAppPurchaseLockupFromData", () => {
+ const lockup = new models.InAppPurchaseLockup();
+ copyDataIntoInAppPurchaseLockup(objectGraph, data, lockup, options);
+ return lockup;
+ });
+}
+/**
+ * Create a new `ScreenshotsLockup` from a platform response data blob.
+ * @param data The platform response data to read from.
+ * @param options Options customizing what data the returned store item will contain.
+ * @returns A new `Lockup` object.
+ */
+export function screenshotsLockupFromData(objectGraph, data, options) {
+ return validation.context("screenshotsLockupFromData", () => {
+ const lockup = new models.ScreenshotsLockup();
+ copyDataIntoScreenshotsLockup(objectGraph, data, lockup, options);
+ return lockup;
+ });
+}
+/**
+ * Create a new `PosterLockup` from a platform response data blob.
+ * @param data The platform response data to read from.
+ * @param options Options customizing what data the returned store item will contain.
+ * @returns A new `Lockup` object.
+ */
+export function posterLockupFromData(objectGraph, data, options) {
+ return validation.context("posterLockupFromData", () => {
+ const lockup = new models.PosterLockup();
+ copyDataIntoPosterLockup(objectGraph, data, lockup, options);
+ return lockup;
+ });
+}
+/**
+ *
+ * @param data
+ * @param options
+ * @param videoConfiguration
+ * @returns {TrailersLockup}
+ */
+export function trailersLockupFromData(objectGraph, data, options, videoConfiguration) {
+ return validation.context("trailersLockupFromData", () => {
+ const lockup = new models.TrailersLockup();
+ copyDataIntoTrailersLockup(objectGraph, data, lockup, videoConfiguration, options);
+ return lockup;
+ });
+}
+/**
+ *Build a mixed media lockup from given data
+ * @param objectGraph
+ * @param data
+ * @param options
+ * @param videoConfiguration
+ * @param searchExperimentsData
+ * @param cropCode The crop code to use for the media
+ * @returns {MixedMediaLockup}
+ */
+export function mixedMediaLockupFromData(objectGraph, data, options, videoConfiguration, searchExperimentsData = null, cropCode) {
+ return validation.context("mixedMediaLockupFromData", () => {
+ const lockup = new models.MixedMediaLockup();
+ copyDataIntoLockup(objectGraph, data, lockup, options, null, () => {
+ copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options);
+ copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options, cropCode);
+ copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData);
+ });
+ return lockup;
+ });
+}
+/**
+ * Create am image lockup for shelfContents to display within a grouping shelf
+ * @param objectGraph
+ * @param mediaApiData shelfContents to create lockup for.
+ * @param lockupOptions The options needed to customize this lockup
+ * @param collectionShelfDisplayStyle
+ */
+export function imageLockupFromData(objectGraph, mediaApiData, lockupOptions, collectionDisplayStyle) {
+ const isEditorialApiData = mediaApiData.type === "editorial-items";
+ const lockupData = isEditorialApiData
+ ? mediaRelationship.relationshipData(objectGraph, mediaApiData, "primary-content")
+ : mediaApiData;
+ const lockup = lockupFromData(objectGraph, lockupData, lockupOptions);
+ // Determine which artwork to use, giving priority to an attached EI first, and then falling back
+ // to the lockup artwork. Currently we only use the attached EI on visionOS.
+ const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, lockupData);
+ const lockupEditorialMediaData = editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, lockupData, collectionDisplayStyle);
+ const articleEditorialMediaData = editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, mediaApiData, collectionDisplayStyle);
+ const lockupArtwork = lockupEditorialMediaData === null || lockupEditorialMediaData === void 0 ? void 0 : lockupEditorialMediaData.artwork;
+ const articleArtwork = articleEditorialMediaData === null || articleEditorialMediaData === void 0 ? void 0 : articleEditorialMediaData.artwork;
+ let artwork;
+ let isDark;
+ if (isSome(articleArtwork) && objectGraph.client.isVision) {
+ isDark = isMediaDark(objectGraph, articleEditorialMediaData);
+ artwork = articleArtwork;
+ }
+ else if (isSome(lockupArtwork)) {
+ isDark = isMediaDark(objectGraph, lockupEditorialMediaData);
+ artwork = lockupArtwork;
+ }
+ if (isSome(artwork) && isSome(lockup)) {
+ const imageLockup = new models.ImageLockup(artwork, lockup, null, null, isDark);
+ imageLockup.caption = mediaPlatformAttributes.platformAttributeAsString(lockupData, attributePlatform, "editorialNotes.badge");
+ if (isSome(imageLockup.caption) && objectGraph.client.isVision) {
+ imageLockup.caption = objectGraph.loc.uppercased(imageLockup.caption);
+ }
+ imageLockup.title =
+ mediaPlatformAttributes.platformAttributeAsString(lockupData, attributePlatform, "editorialNotes.tagline") || mediaAttributes.attributeAsString(lockupData, "genreDisplayName");
+ imageLockup.impressionMetrics = lockup.impressionMetrics;
+ return imageLockup;
+ }
+ else {
+ return null;
+ }
+}
+function copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData) {
+ var _a;
+ if (!objectGraph.client.isPhone) {
+ return;
+ }
+ const displayStyle = serverData.asString(data.meta, "imageLockupFromData");
+ if (serverData.isDefinedNonNull(displayStyle)) {
+ lockup.screenshotsDisplayStyle = displayStyle;
+ }
+ else if (serverData.isDefinedNonNull((_a = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _a === void 0 ? void 0 : _a.screenshots)) {
+ lockup.screenshotsDisplayStyle = searchExperimentsData.displayStyle.screenshots;
+ }
+}
+/**
+ * Create a mixed media lockup, with some specific ad-related modifications, from the supplied data.
+ * @param objectGraph The App Store Object Graph.
+ * @param data The data for the app to go into the lockup.
+ * @param options A set of options customising the lockup.
+ * @param videoConfiguration A configuration object for any videos in the lockup.
+ * @param searchExperimentsData Data for any search results experiments being run currently.
+ * @param applyAdOfferDisplayProperties Whether to apply the default ad OfferDisplayProperties. Some callers of this function want to enforce their own offer styling.
+ * @returns A MixedMediaLockup with the desired configuration.
+ */
+export function mixedMediaAdLockupFromData(objectGraph, data, options, videoConfiguration, searchExperimentsData, applyAdOfferDisplayProperties = true) {
+ return validation.context("mixedMediaAdLockupFromData", () => {
+ const lockup = new models.MixedMediaLockup();
+ if (!mediaAttributes.attributeAsBooleanOrFalse(data, "iad.format.images")) {
+ copyDataIntoLockup(objectGraph, data, lockup, options, null, () => {
+ copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options);
+ });
+ lockup.screenshots = [];
+ }
+ else {
+ copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options);
+ adLockup.performAssetOverridesForMixedMediaAdLockupIfNeeded(objectGraph, data, lockup, options.metricsOptions);
+ copyDataIntoLockup(objectGraph, data, lockup, options, null, () => {
+ copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData);
+ copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options);
+ });
+ }
+ adLockup.performAdOverridesforLockup(objectGraph, data, lockup, options.metricsOptions, applyAdOfferDisplayProperties);
+ return lockup;
+ });
+}
+/**
+ * Create an lockup that describes the arcade service lockup from upsell data.
+ * This lockup:
+ * - Has a title, e.g. "Apple Arcade".
+ * - May have a subtitle text, e.g. "Play 100+ games".
+ * - Does *NOT* have a `clickAction` on itself (There is no Arcade product page).
+ * - Has two actions for each subscription state.
+ */
+export function arcadeLockupFromData(objectGraph, upsellData, metricsOptions, context, offerStyle, offerEnvironment) {
+ return validation.context("arcadeLockupFromData", () => {
+ const data = upsellData.marketingItemData;
+ const lockup = new models.ArcadeLockup();
+ lockup.title = objectGraph.loc.string("ARCADE_LOCKUP_TITLE", "Apple Arcade");
+ const marketingItemData = upsellData.marketingItemData;
+ metricsOptions = {
+ ...metricsOptions,
+ mercuryMetricsData: metricsUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData),
+ };
+ // This `name` attribute contains strings like "Some Games. \nAll you can play.". Weird.
+ // We trim newlines for arcade service footer lockup since the multiline text looks ugly.
+ let subtitle = arcadeUpsell.descriptionFromData(objectGraph, data);
+ if ((subtitle === null || subtitle === void 0 ? void 0 : subtitle.length) > 0) {
+ // This subtitle string on the upsell data blob is shared by many different views. However, editorial has a tendency to program explicit newlines, since some views look better with them (notably sheets).
+ // For the arcade lockup on some platforms, e.g. iOS, the string appears in a vertically constrained space and we ideally want to fit in 1 line if possible, We'll trim for now... Long term we need an editorial notes key
+ // for strings w/o newlines for this use case.
+ const platformIgnoresSubtitleNewlines = objectGraph.host.isiOS;
+ subtitle = platformIgnoresSubtitleNewlines ? subtitle.replace(/\n/g, " ") : subtitle;
+ lockup.nonsubscribedSubtitle = subtitle;
+ lockup.subscribedSubtitle = subtitle;
+ }
+ // Unsubscribed state action
+ let unsubscribedAction;
+ const unsubscribedActionTitle = arcadeUpsell.callToActionLabelFromData(objectGraph, data);
+ const arcadeLockupInNavBarEnabled = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isVision;
+ if (arcadeLockupInNavBarEnabled) {
+ // This is configured as an `ArcadeAction` directly if the pricing token is present.
+ unsubscribedAction = arcadeUpsell.arcadeOfferButtonActionFromData(objectGraph, upsellData.marketingItemData, context, metricsOptions);
+ unsubscribedAction.title = unsubscribedActionTitle;
+ }
+ else if ((unsubscribedActionTitle === null || unsubscribedActionTitle === void 0 ? void 0 : unsubscribedActionTitle.length) > 0) {
+ unsubscribedAction = arcadeCommon.arcadeSubscribePageFlowAction(objectGraph, models.marketingItemContextFromString("editorialItemCanvas"), null, null, {
+ ...metricsOptions,
+ id: data.id,
+ });
+ unsubscribedAction.title = unsubscribedActionTitle;
+ }
+ else {
+ // If Upsell data is misconfigured and missing description, default to opening Arcade app for unsubscribed state.
+ unsubscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, metricsOptions.pageInformation, metricsOptions.locationTracker);
+ if (preprocessor.GAMES_TARGET) {
+ unsubscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore");
+ }
+ else {
+ unsubscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE");
+ }
+ }
+ lockup.unsubscribedButtonAction = unsubscribedAction;
+ // Subscribed state action
+ const subscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, metricsOptions.pageInformation, metricsOptions.locationTracker);
+ if (preprocessor.GAMES_TARGET) {
+ subscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore");
+ }
+ else {
+ subscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE");
+ }
+ lockup.subscribedButtonAction = subscribedAction;
+ // Impressions:
+ const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, upsellData.marketingItemData, lockup.title, metricsOptions);
+ metricsImpressionOptions.displaysArcadeUpsell = true;
+ // If no targetType is provided, set the correct value for the platform.
+ if (serverData.isNullOrEmpty(metricsImpressionOptions.targetType)) {
+ metricsImpressionOptions.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup";
+ }
+ metricsHelpersImpressions.addImpressionFields(objectGraph, lockup, metricsImpressionOptions);
+ const displayProperties = new models.OfferDisplayProperties("arcade", objectGraph.bag.arcadeAppAdamId, null, offerStyle, null, offerEnvironment, null, null, null, null, null, null, null, null, null, null, null, null, objectGraph.bag.arcadeProductFamilyId);
+ if (preprocessor.GAMES_TARGET) {
+ displayProperties.titles["subscribed"] = objectGraph.loc.string("OfferButton.Arcade.Title.Explore");
+ }
+ else {
+ displayProperties.titles["subscribed"] = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE");
+ }
+ lockup.offerDisplayProperties = displayProperties;
+ return lockup;
+ });
+}
+export function lockupsFromDataContainer(objectGraph, dataContainer, options) {
+ if (serverData.isNull(dataContainer)) {
+ return [];
+ }
+ return lockupsFromData(objectGraph, dataContainer.data, options);
+}
+export function lockupsFromData(objectGraph, dataArray, options) {
+ return validation.context("lockupsFromData", () => {
+ var _a;
+ if (!dataArray) {
+ return [];
+ }
+ const items = [];
+ let isDeferring = false;
+ for (let index = 0; index < dataArray.length; index++) {
+ if (isDeferring) {
+ break;
+ }
+ const lockupData = dataArray[index];
+ if (!mediaAttributes.hasAttributes(lockupData)) {
+ if (options.contentUnavailable) {
+ isDeferring = options.contentUnavailable(index, lockupData);
+ }
+ continue;
+ }
+ const lockupOptions = options.lockupOptions;
+ let filter = 80894 /* filtering.Filter.All */;
+ if (options.includeOrdinals) {
+ const ordinal = options.ordinalDirection === "descending" ? dataArray.length - index : index + 1;
+ lockupOptions.ordinal = objectGraph.loc.decimal(ordinal).toString();
+ }
+ if (serverData.isDefinedNonNull(options.filter)) {
+ filter = options.filter;
+ }
+ if (filtering.shouldFilter(objectGraph, lockupData, filter) && !options.shouldShowOnUnsupportedPlatform) {
+ continue;
+ }
+ const lockup = ((_a = options.lockupOptions.shouldCreateScreenshotsLockup) !== null && _a !== void 0 ? _a : false)
+ ? screenshotsLockupFromData(objectGraph, lockupData, lockupOptions)
+ : lockupFromData(objectGraph, lockupData, lockupOptions);
+ if (serverData.isNull(lockup) || !lockup.isValid()) {
+ continue;
+ }
+ items.push(lockup);
+ metricsHelpersLocation.nextPosition(options.lockupOptions.metricsOptions.locationTracker);
+ }
+ return items;
+ });
+}
+export function screenshotsLockupsFromData(objectGraph, dataArray, options) {
+ return validation.context("screenshotsLockupsFromData", () => {
+ if (!dataArray) {
+ return [];
+ }
+ const items = [];
+ for (let index = 0; index < dataArray.length; index++) {
+ const lockupData = dataArray[index];
+ if (serverData.isNull(lockupData.attributes)) {
+ if (options.contentUnavailable) {
+ options.contentUnavailable(index, lockupData);
+ }
+ continue;
+ }
+ const lockupOptions = options.lockupOptions;
+ let filter = 80894 /* filtering.Filter.All */;
+ if (serverData.isDefinedNonNull(options.filter)) {
+ filter = options.filter;
+ }
+ if (filtering.shouldFilter(objectGraph, lockupData, filter)) {
+ continue;
+ }
+ const lockup = screenshotsLockupFromData(objectGraph, lockupData, lockupOptions);
+ if (!lockup.isValid()) {
+ continue;
+ }
+ items.push(lockup);
+ metricsHelpersLocation.nextPosition(options.lockupOptions.metricsOptions.locationTracker);
+ }
+ return items;
+ });
+}
+/**
+ * Create an action for the provided data
+ * @param objectGraph The App Store Object Graph.
+ * @param data The data to create the action for.
+ * @param metricsOptions The metrics options to use for the action.
+ * @param clientIdentifierOverride A client identifier override, if any.
+ * @param externalDeepLinkUrl A custom deeplink URL, if any.
+ * @param isCppDeepLinkEligible Whether the action should be eligible for CPP Deep Links, if any. Used for restricting deep links on ads.
+ * @returns A configured action for the provided data.
+ */
+export function actionFromData(objectGraph, data, metricsOptions, clientIdentifierOverride, externalDeepLinkUrl = null, isCppDeepLinkEligible) {
+ return validation.context(`actionFromData: ${data.type}`, () => {
+ switch (data.type) {
+ case "apps":
+ case "app-bundles": {
+ return productActionFromData(objectGraph, data, metricsOptions, {
+ clientIdentifierOverride: clientIdentifierOverride,
+ externalDeepLinkUrl: externalDeepLinkUrl,
+ isCppDeepLinkEligible: isCppDeepLinkEligible,
+ });
+ }
+ case "in-apps": {
+ return iAPActionFromData(objectGraph, data, metricsOptions);
+ }
+ case "editorial-items": {
+ return editorialItemActionFromData(objectGraph, data, metricsOptions, clientIdentifierOverride);
+ }
+ case "tags":
+ case "editorial-pages": {
+ return editorialPageActionFromData(objectGraph, data, metricsOptions);
+ }
+ case "multiple-system-operators":
+ return msoActionFromData(objectGraph, data, metricsOptions);
+ case "groupings":
+ return groupingActionFromData(objectGraph, data, metricsOptions);
+ case "developers":
+ default: {
+ return genericActionFromData(objectGraph, data, metricsOptions);
+ }
+ }
+ });
+}
+function iAPActionFromData(objectGraph, data, options) {
+ return validation.context("iAPActionFromData", () => {
+ var _a;
+ const parentData = parentDataFromInAppData(objectGraph, data);
+ if (!parentData) {
+ return null;
+ }
+ const clickAction = new models.FlowAction("product");
+ const parentUrl = urls.URL.from(mediaAttributes.attributeAsString(parentData, "url"));
+ // Attach the productVariantID/cppId/ppid to the parent URL, if it exists.
+ const productVariantData = (_a = options.productVariantData) !== null && _a !== void 0 ? _a : variants.productVariantDataForData(objectGraph, parentData);
+ const productVariantID = variants.productVariantIDForVariantData(productVariantData);
+ if (serverData.isDefinedNonNull(productVariantID)) {
+ parentUrl.param(Parameters.productVariantID, productVariantID);
+ }
+ const parentUrlString = parentUrl.toString();
+ const inAppProductIdentifier = mediaAttributes.attributeAsString(data, "offerName");
+ const isSubscription = mediaAttributes.attributeAsBooleanOrFalse(data, "isSubscription");
+ clickAction.pageUrl = configureProductUrlFromInAppPurchase(objectGraph, parentUrlString, inAppProductIdentifier, isSubscription);
+ clickAction.title = mediaAttributes.attributeAsString(data, "name");
+ metricsHelpersClicks.addClickEventToAction(objectGraph, clickAction, options);
+ return clickAction;
+ });
+}
+export function editorialItemActionTypeFromData(objectGraph, data) {
+ if (serverData.isNullOrEmpty(data)) {
+ return 0 /* EditorialItemActionType.Unknown */;
+ }
+ return validation.context("editorialItemActionFromData", () => {
+ const link = mediaAttributes.attributeAsDictionary(data, "link");
+ if (linkIsExternal(link)) {
+ return 1 /* EditorialItemActionType.ExternalLink */;
+ }
+ const substyle = mediaAttributes.attributeAsString(data, "displaySubStyle");
+ const isListArticle = substyle === "List" || substyle === "NumberedList";
+ if (mediaAttributes.attributeAsBooleanOrFalse(data, "isCanvasAvailable") || isListArticle) {
+ return 2 /* EditorialItemActionType.Article */;
+ }
+ // This is a bit of a workaround to get the product data from `primary-content`, if it's available, for app events
+ // as this is where it's currently made available for an EI.
+ const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content");
+ const cardContents = mediaRelationship.relationshipCollection(data, "card-contents");
+ const isPrimaryContentProduct = serverData.isDefinedNonNullNonEmpty(primaryContent) && primaryContent.length === 1;
+ const isProduct = serverData.isDefinedNonNullNonEmpty(cardContents) && cardContents.length === 1;
+ if ((isProduct && cardContents[0].type === "app-events") ||
+ (isPrimaryContentProduct && primaryContent[0].type === "app-events")) {
+ return 4 /* EditorialItemActionType.AppEvent */;
+ }
+ if (isProduct) {
+ return 3 /* EditorialItemActionType.Product */;
+ }
+ if ((objectGraph.client.isVision || preprocessor.GAMES_TARGET) && isSome(data.href) && data.href.length > 0) {
+ return 5 /* EditorialItemActionType.Href */;
+ }
+ return 0 /* EditorialItemActionType.Unknown */;
+ });
+}
+/**
+ * Create an action for an editorial story
+ * @param objectGraph The App Store Object Graph.
+ * @param data The data for the editorial story
+ * @param options A set of options customising the lockup.
+ * @param clientIdentifierOverride A client identifier override.
+ * @param articleRecoMetricsData The recommendation metrics data for the editorial item
+ * @param todayCardConfig The config options for the editorial Item
+ * @param canvasFilter The canvas filter for this editorial item, used to fetch a specific story
+ * @returns A click action for an editorial item
+ */
+export function editorialItemActionFromData(objectGraph, data, options, clientIdentifierOverride, articleRecoMetricsData, todayCardConfig) {
+ if (serverData.isNullOrEmpty(data)) {
+ return null;
+ }
+ return validation.context("editorialItemActionFromData", () => {
+ var _a;
+ let flowDestination;
+ let pageUrlString;
+ let destinationIntent;
+ let presentation;
+ switch (editorialItemActionTypeFromData(objectGraph, data)) {
+ case 1 /* EditorialItemActionType.ExternalLink */:
+ return editorialItemExternalLinkActionFromData(objectGraph, data, options);
+ case 2 /* EditorialItemActionType.Article */:
+ flowDestination = "article";
+ const pageUrl = urls.URL.from(mediaAttributes.attributeAsString(data, "url"));
+ if (serverData.isDefinedNonNull(articleRecoMetricsData)) {
+ pageUrl.param(Parameters.recoMetrics, JSON.stringify(articleRecoMetricsData));
+ }
+ if (objectGraph.client.isiOS && isSome(todayCardConfig) && !todayCardConfig.isHorizontalShelfContext) {
+ pageUrl.param(Parameters.todayCardConfig, JSON.stringify(todayCardConfig));
+ }
+ const editorialCardId = (_a = editorialCardFromData(data)) === null || _a === void 0 ? void 0 : _a.id;
+ if (isSome(editorialCardId)) {
+ pageUrl.param(Parameters.editorialCardId, editorialCardId);
+ }
+ pageUrlString = pageUrl.build();
+ if (objectGraph.client.isWeb) {
+ destinationIntent = makeRoutableArticlePageIntent({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: data.id,
+ });
+ pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destinationIntent);
+ }
+ if (preprocessor.GAMES_TARGET) {
+ // For Luck Seed1 we always show stories as modal across all platform,
+ // in the future we should follow App Store and push them on Mac,
+ // according to the specs https://quip-apple.com/CEplADSB1MJR
+ // Need to support proper dual column layout first:
+ // rdar://141251194 ([Seed 3] [Stories] Dynamic page layout (single and dual column))
+ if (objectGraph.host.isMac) {
+ presentation = "stackPush";
+ }
+ else {
+ presentation = "sheetPresent";
+ }
+ }
+ break;
+ case 3 /* EditorialItemActionType.Product */:
+ const productData = mediaRelationship.relationshipCollection(data, "card-contents")[0];
+ return actionFromData(objectGraph, productData, options, clientIdentifierOverride);
+ case 4 /* EditorialItemActionType.AppEvent */:
+ // This is a bit of a workaround to get the product data from `primary-content`, if it's available, for app events
+ // as this is where it's currently made available for an EI.
+ const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content");
+ const isPrimaryContentProduct = serverData.isDefinedNonNullNonEmpty(primaryContent) && primaryContent.length === 1;
+ const appEventData = isPrimaryContentProduct
+ ? primaryContent[0]
+ : mediaRelationship.relationshipCollection(data, "card-contents")[0];
+ const eventProductData = mediaRelationship.relationshipData(objectGraph, appEventData, "app");
+ if (serverData.isNullOrEmpty(eventProductData)) {
+ return null;
+ }
+ const appEventOrDate = appEvent.appEventOrPromotionStartDateFromData(objectGraph, appEventData, eventProductData, false, false, "dark", "infer", false, options, false, true, null, false, false);
+ // Return early if we received a Date, as this means the App Event shouldn't be accessible yet.
+ if (serverData.isNull(appEventOrDate) || appEventOrDate instanceof Date) {
+ return null;
+ }
+ return appPromotionsCommon.detailPageClickActionFromData(objectGraph, appEventData, eventProductData, appEventOrDate, options, true);
+ case 5 /* EditorialItemActionType.Href */:
+ flowDestination = "page";
+ pageUrlString = mediaUrlMapping.hrefToRoutableUrl(objectGraph, data.href);
+ break;
+ default:
+ flowDestination = "unknown";
+ const link = mediaAttributes.attributeAsDictionary(data, "link");
+ if (objectGraph.client.isWeb) {
+ destinationIntent = makeEditorialPageIntentByID({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: data.id,
+ });
+ pageUrlString = makeEditorialPageURL(objectGraph, destinationIntent);
+ }
+ else {
+ pageUrlString = serverData.asString(link, "url");
+ }
+ }
+ if (isNothing(pageUrlString)) {
+ return null;
+ }
+ const action = new models.FlowAction(flowDestination);
+ action.pageUrl = pageUrlString;
+ if (isSome(presentation)) {
+ action.presentation = presentation;
+ }
+ let title = content.notesFromData(objectGraph, data, "name");
+ if (serverData.isNull(title)) {
+ title = serverData.asString(data, "label");
+ }
+ action.title = title;
+ if (destinationIntent) {
+ action.destination = destinationIntent;
+ }
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, options);
+ return action;
+ });
+}
+/**
+ * Creates a flow action to display a product page.
+ * @param objectGraph Dependency soup
+ * @param data The media api data to create the action from.
+ * @param metricsOptions Metrics dependencies
+ * @param options.clientIdentifierOverride The preferred client identifier to use for the product page.
+ * @param options.externalDeepLinkUrl An external deep link for the propuct to be used when opening the app, if any.
+ * @param options.isCppDeepLinkEligible Whether a CPP deep link can be used for this lockup, if available.
+ * @param options.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.
+ * @returns A `FlowAction` object pointing to a product page.
+ */
+function productActionFromData(objectGraph, data, metricsOptions, options) {
+ var _a, _b, _c, _d;
+ if (!data) {
+ return null;
+ }
+ const clientIdentifierOverride = (_a = options.clientIdentifierOverride) !== null && _a !== void 0 ? _a : null;
+ const externalDeepLinkUrl = (_b = options.externalDeepLinkUrl) !== null && _b !== void 0 ? _b : null;
+ const isCppDeepLinkEligible = (_c = options.isCppDeepLinkEligible) !== null && _c !== void 0 ? _c : false;
+ const isCppDeepLinkDisabled = !isCppDeepLinkEligible;
+ const productVariantData = (_d = options.productVariantData) !== null && _d !== void 0 ? _d : variants.productVariantDataForData(objectGraph, data);
+ return validation.context("productActionFromData", () => {
+ var _a, _b, _c;
+ let productUrl = mediaAttributes.attributeAsString(data, "url");
+ if (!productUrl) {
+ validation.unexpectedNull("ignoredValue", "string", "url");
+ return null;
+ }
+ let productPageOptions = {};
+ const url = new urls.URL(productUrl);
+ if (metricsOptions.isAdvert) {
+ const lineItem = serverData.asString(data, "iad.lineItem");
+ if (lineItem !== null && lineItem.length > 0) {
+ url.param(metricsHelpersModels.iAdURLLineItemParameterStringToken, lineItem);
+ }
+ const iAdClickFields = (_a = metricsOptions.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.clickFields;
+ url.param(metricsHelpersModels.iAdURLParameterStringToken, JSON.stringify(iAdClickFields));
+ productPageOptions = {
+ iAdClickFields: serverData.asJSONData(iAdClickFields),
+ iAdLineItem: lineItem,
+ };
+ const instanceId = adCommon.advertInstanceIdForData(objectGraph, data);
+ if (isSome(instanceId)) {
+ const advertType = content.isArcadeSupported(objectGraph, data)
+ ? "arcadeApp"
+ : "standardApp";
+ const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation);
+ const bundleId = mediaAttributes.attributeAsString(data, "platformAttributes.ios.bundleId");
+ const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder");
+ const purchaseType = isPreorder ? "preorder" : "standard";
+ const dismissAdActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "productPageDismissed", purchaseType, reportingDestination);
+ url.param(metricsHelpersModels.iAdDismissAdActionMetricsParameterStringToken, JSON.stringify(dismissAdActionMetrics));
+ productPageOptions.iAdDismissAdActionMetrics = serverData.asJSONData(dismissAdActionMetrics);
+ }
+ }
+ const productVariantID = variants.productVariantIDForVariantData(productVariantData);
+ if (serverData.isDefinedNonNull(productVariantID)) {
+ url.param(Parameters.productVariantID, productVariantID);
+ }
+ if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) {
+ if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) {
+ const alignedRegionDeeplinkUrl = adLockup.getCustomCreativeDeepLinkUrl(data);
+ if (isSome(alignedRegionDeeplinkUrl)) {
+ url.param(externalDeepLink.alignedRegionDeepLinkQueryParameter, alignedRegionDeeplinkUrl);
+ }
+ else {
+ const tapDestinationCppId = adLockup.getTapDestinationIdForAdvert(data);
+ if (isSome(tapDestinationCppId)) {
+ url.param(Parameters.productVariantID, tapDestinationCppId);
+ }
+ }
+ }
+ }
+ productPageOptions.externalDeepLinkUrl = externalDeepLinkUrl;
+ if (serverData.isDefinedNonNull(externalDeepLinkUrl)) {
+ url.param(externalDeepLink.externalDeepLinkQueryParameter, externalDeepLinkUrl);
+ }
+ const platformFromIntent = getPlatform(objectGraph).platform;
+ const platformInferredFromData = safelyInferPlatformFromData(objectGraph, data);
+ if (objectGraph.client.isWeb) {
+ // For the web client, we add the platform query parameter if the active intent's platform
+ // differs from the "default" platform inferred from the app's data.
+ if (platformFromIntent && platformFromIntent !== platformInferredFromData) {
+ url.param("platform", platformFromIntent);
+ }
+ }
+ else {
+ // For non-web clients, propagate CPP, search term, and client identifier parms, which are not needed for web.
+ productPageOptions.isCppDeepLinkDisabled = isCppDeepLinkDisabled;
+ url.param(externalDeepLink.cppDeepLinkDisabledQueryParameter, isCppDeepLinkDisabled.toString());
+ // Add a searchTerm to the product URL to propagate to purchases on the subsequent product page.
+ const searchTerm = (_c = (_b = metricsOptions.pageInformation) === null || _b === void 0 ? void 0 : _b.searchTermContext) === null || _c === void 0 ? void 0 : _c.term;
+ if (isSome(searchTerm)) {
+ url.param("searchTerm", searchTerm);
+ }
+ productPageOptions.clientIdentifierOverride = clientIdentifierOverride;
+ if ((clientIdentifierOverride === null || clientIdentifierOverride === void 0 ? void 0 : clientIdentifierOverride.length) > 0) {
+ url.param("clientIdentifierOverride", clientIdentifierOverride);
+ }
+ }
+ productUrl = url.toString();
+ if (preprocessor.GAMES_TARGET) {
+ return gameModels.viewGameActionWithMediaAPIData(data, objectGraph, makeClickMetrics(objectGraph, data.id, "lockup", "navigate", [data.id]));
+ }
+ const action = new models.FlowAction("product");
+ action.pageUrl = productUrl;
+ action.pageData = productPageUtil.createProductPageSidePackFromResponse(objectGraph, data, productPageOptions);
+ action.title = mediaAttributes.attributeAsString(data, "name");
+ if (metricsOptions && metricsOptions.pageInformation) {
+ action.referrerUrl = metricsOptions.pageInformation.pageUrl;
+ }
+ // Extract and add additional options for pre-orders
+ const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder");
+ if (isPreorder) {
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, {
+ ...metricsOptions,
+ offerType: "preorder",
+ offerReleaseDate: offers.expectedReleaseDateFromData(objectGraph, data),
+ }, true);
+ }
+ else {
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, metricsOptions, true);
+ }
+ const productId = serverData.asString(data, "id");
+ if (isSome(productId)) {
+ action.destination =
+ data.type === "app-bundles"
+ ? makeBundlePageIntent({
+ ...getLocale(objectGraph),
+ id: productId,
+ })
+ : makeProductPageIntent({
+ ...getLocale(objectGraph),
+ platform: inferPreviewPlatform(objectGraph, data),
+ id: productId,
+ });
+ }
+ return action;
+ });
+}
+/**
+ * Safely infers a preview platform from media data, handling any potential errors.
+ */
+function safelyInferPlatformFromData(objectGraph, data) {
+ try {
+ return inferPreviewPlatformFromDeviceFamilies(objectGraph, { data: [data] });
+ }
+ catch (error) {
+ objectGraph.console.error(`Error inferring preview platform from data: ${error}`);
+ return undefined;
+ }
+}
+function inferPreviewPlatform(objectGraph, data) {
+ const platformFromIntent = getPlatform(objectGraph).platform;
+ if (platformFromIntent) {
+ return platformFromIntent;
+ }
+ return safelyInferPlatformFromData(objectGraph, data);
+}
+/**
+ * Creates a flow action to display a grouping page
+ * @param data The media api data to create the action from.
+ * @returns A `FlowAction` object pointing to a grouping page.
+ */
+function groupingActionFromData(objectGraph, data, options) {
+ if (!data) {
+ return null;
+ }
+ return validation.context("groupingActionFromData", () => {
+ if (!data.href) {
+ validation.unexpectedNull("ignoredValue", "string", "href");
+ return null;
+ }
+ const action = new models.FlowAction("page");
+ if (objectGraph.client.isWeb) {
+ const destination = makeGroupingPageIntentByID({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: data.id,
+ });
+ action.destination = destination;
+ action.pageUrl = makeGroupingPageCanonicalURL(objectGraph, destination);
+ }
+ else {
+ action.pageUrl = mediaUrlBuilder
+ .buildURLFromRequest(objectGraph, mediaUrlMapping.mediaApiGroupingURLFromHref(objectGraph, data.href))
+ .toString();
+ }
+ action.title = nameFromGroupingData(objectGraph, data);
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, options);
+ return action;
+ });
+}
+/**
+ * Creates a flow action to display a mso-page.
+ * @param data The media api data to create the action from.
+ * @returns A `FlowAction` object pointing to a mso page.
+ */
+function msoActionFromData(objectGraph, data, options) {
+ if (!data) {
+ return null;
+ }
+ return validation.context("msoActionFromPlatformData", () => {
+ const roomUrl = mediaAttributes.attributeAsString(data, "url");
+ if (!roomUrl) {
+ validation.unexpectedNull("ignoredValue", "string", "url");
+ return null;
+ }
+ const action = new models.FlowAction("mso");
+ action.pageUrl = roomUrl;
+ action.title = mediaAttributes.attributeAsString(data, "name");
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, options);
+ return action;
+ });
+}
+/**
+ * Creates a flow action to display an editorial page.
+ *
+ * editorial-pages asset type does not have a url attribute, so we have to synthesize one
+ * - The "web" client uses the URL defined by the `EditorialPageIntentController`
+ * - Other clients use the `href` attribute
+ *
+ * @param data The media api data to create the action from.
+ * @param options The required options passed to the lockup creation method.
+ * @returns A `FlowAction` object pointing to a product page.
+ */
+function editorialPageActionFromData(objectGraph, data, options) {
+ if (!data) {
+ return null;
+ }
+ return validation.context("editorialPageActionFromData", () => {
+ const action = new models.FlowAction("page");
+ if (objectGraph.client.isWeb) {
+ const editorialPageIntent = makeEditorialPageIntentByID({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: data.id,
+ });
+ action.destination = editorialPageIntent;
+ action.pageUrl = makeEditorialPageURL(objectGraph, editorialPageIntent);
+ }
+ else {
+ const href = data.href;
+ if (serverData.isNullOrEmpty(href)) {
+ validation.unexpectedNull("ignoredValue", "string", "href");
+ return null;
+ }
+ action.pageUrl = mediaUrlMapping.hrefToRoutableUrl(objectGraph, href);
+ }
+ action.title = content.editorialNotesFromData(objectGraph, data, "name");
+ if (serverData.isNullOrEmpty(action.title)) {
+ action.title = mediaAttributes.attributeAsString(data, "name");
+ }
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, options);
+ return action;
+ });
+}
+/**
+ * Creates a flow action to display a generic page
+ * @param data The media api data to create the action from.
+ * @returns A `FlowAction` object pointing to a product page.
+ */
+function genericActionFromData(objectGraph, data, options) {
+ if (!data) {
+ return null;
+ }
+ return validation.context("genericActionFromData", () => {
+ const type = serverData.asString(data, "type");
+ const url = mediaAttributes.attributeAsString(data, "url");
+ if (!url) {
+ validation.unexpectedNull("ignoredValue", "string", "url");
+ return null;
+ }
+ const action = new models.FlowAction("page");
+ action.pageUrl = url;
+ if (type === "groupings") {
+ action.title = nameFromGroupingData(objectGraph, data);
+ }
+ else {
+ action.title = mediaAttributes.attributeAsString(data, "name");
+ }
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, options);
+ return action;
+ });
+}
+/**
+ * Whether or not link blob is to external url.
+ * @param link JSON blob for 'link'
+ * @returns boolean whether url is external link.
+ */
+function linkIsExternal(link) {
+ const target = serverData.asString(link, "target");
+ return target && target === "external";
+}
+/**
+ * Create an `ExternalUrlAction` for an External Link card.
+ * The title of the action is either the url domain or provided short description.
+ * @param data Media Api data
+ * @returns A configured `ExternalUrlAction` for card.
+ */
+export function editorialItemExternalLinkActionFromData(objectGraph, data, options) {
+ return validation.context("editorialItemExternalLinkActionFromData", () => {
+ const link = mediaAttributes.attributeAsDictionary(data, "link");
+ const urlString = serverData.asString(link, "url");
+ const action = new models.ExternalUrlAction(urlString);
+ // Title is short description or url domain
+ const linkDescription = content.notesFromData(objectGraph, data, "short");
+ if (linkDescription) {
+ action.title = linkDescription;
+ }
+ else {
+ const url = new urls.URL(urlString);
+ action.title = url.host;
+ }
+ metricsHelpersClicks.addClickEventToAction(objectGraph, action, options);
+ return action;
+ });
+}
+/**
+ * Creates a shallow copy of the provided lockups array, overriding any of the properties
+ * supplied. This function makes sure to override the properties non-destructively; that
+ * is, the overrides are only applied to the copied array's lockups.
+ * @param lockups The lockups to copy.
+ * @param overrideStyle The style to apply for the copied lockups' offerDisplayProperties.
+ * @param filterAds Whether to filter ads. In some places where we add ads to a set of lockups, showing that ad in a copied location isn't supported.
+ * @returns {Lockup[]}
+ */
+export function shallowCopyLockupsOverridingProperties(objectGraph, lockups, overrideStyle, filterAds = false) {
+ var _a, _b;
+ if (!lockups) {
+ return null;
+ }
+ // Store a count of the removed ads so we know how many items we've filtered from the beginning of the list of lockups.
+ let removedAdCount = 0;
+ const copies = [];
+ for (const lockup of lockups) {
+ if (filterAds && serverData.isDefinedNonNull((_a = lockup.searchAd) !== null && _a !== void 0 ? _a : (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.searchAd)) {
+ removedAdCount += 1;
+ continue;
+ }
+ const copy = objects.shallowCopyOf(lockup);
+ if (overrideStyle && copy.offerDisplayProperties) {
+ copy.offerDisplayProperties = copy.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, overrideStyle);
+ }
+ /// Adjust the impression index of the copy by the number of ads that have been removed prior to this item.
+ if (removedAdCount > 0) {
+ // Take a copy of the impressionMetrics.fields, as the copy is only shallow and we don't want to affect the original.
+ const impressionMetricsFields = objects.shallowCopyOf(copy.impressionMetrics.fields);
+ const impressionIndex = serverData.asNumber(impressionMetricsFields.impressionIndex);
+ if (isSome(impressionIndex)) {
+ impressionMetricsFields.impressionIndex = impressionIndex - removedAdCount;
+ copy.impressionMetrics = new models.ImpressionMetrics(impressionMetricsFields, copy.impressionMetrics.id, copy.impressionMetrics.custom);
+ }
+ }
+ copies.push(copy);
+ }
+ return copies;
+}
+/**
+ * Generates a name from a grouping media api resource
+ * @param data The grouping data from a genre response
+ * @return {string} A string of the grouping page's name
+ */
+export function nameFromGroupingData(objectGraph, data) {
+ const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames");
+ if (serverData.isDefinedNonNullNonEmpty(genreNames)) {
+ return genreNames[0];
+ }
+ else {
+ return mediaAttributes.attributeAsString(data, "name");
+ }
+}
+export function deviceHasCapabilitiesFromData(objectGraph, data) {
+ if (!data) {
+ return false;
+ }
+ if (objectGraph.client.isWatch || objectGraph.client.isWeb || objectGraph.client.isCompanionVisionApp) {
+ // For the "watch" client, opt out of capability checks until we land <rdar://47322408>.
+ // For the "web" client, we treat it like it can run anything, since we can't infer actual capabilites.
+ // For the companion app, we can't call down to the remote device to check its capabilities,
+ // so we defer this check to later on.
+ return true;
+ }
+ const requiredCapabilitiesString = content.requiredCapabilitiesFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled);
+ if (serverData.isNullOrEmpty(requiredCapabilitiesString)) {
+ // If we don't have any capabilities, this is vacuously true.
+ return true;
+ }
+ const splitCapabilities = requiredCapabilitiesString.split(" ");
+ const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data);
+ return objectGraph.client.deviceHasCapabilitiesIncludingCompatibilityCheckIsVisionOSCompatibleIOSApp(splitCapabilities, supportsVisionOSCompatibleIOSBinary);
+}
+/**
+ * Finds the IAP data from an app promotion
+ * @param data The app promotion data
+ * @returns data of the IAP
+ */
+export function iapDataFromData(objectGraph, data) {
+ if (!data) {
+ return null;
+ }
+ switch (data.type) {
+ case "contingent-items":
+ return mediaRelationship.relationshipData(objectGraph, data, "branch");
+ case "offer-items":
+ return mediaRelationship.relationshipData(objectGraph, data, "salables");
+ default:
+ break;
+ }
+ return data;
+}
+/**
+ * Finds the discounted offer data from an IAP.
+ * @param data The IAP data
+ * @returns the discounted offer
+ */
+export function discountedOfferFromData(data) {
+ if (!data) {
+ return null;
+ }
+ const contingentOffer = serverData.asDictionary(data, "meta.contingentItemOffer");
+ if (serverData.isDefinedNonNullNonEmpty(contingentOffer)) {
+ return contingentOffer;
+ }
+ const winbackOffer = serverData.asDictionary(data, "meta.discountOffer");
+ if (serverData.isDefinedNonNullNonEmpty(winbackOffer)) {
+ return winbackOffer;
+ }
+ return null;
+}
+/**
+ * Finds the parent app data from a app promotion.
+ * @param data The contingent-offer or offer-item data
+ * @returns the parent app data
+ */
+export function parentDataFromInAppData(objectGraph, data) {
+ if (!data) {
+ return null;
+ }
+ switch (data.type) {
+ case "contingent-items":
+ return mediaRelationship.relationshipData(objectGraph, data, "branch-app");
+ case "offer-items":
+ const iapData = mediaRelationship.relationshipData(objectGraph, data, "salables");
+ return mediaRelationship.relationshipData(objectGraph, iapData, "app");
+ default:
+ break;
+ }
+ return mediaRelationship.relationshipData(objectGraph, data, "app");
+}
+export function cleanupArcadeDownloadPackLockupMetricsIfNeeded(lockup, objectGraph) {
+ if (!objectGraph.bag.arcadeDownloadPacksMetricsEventsEnabled) {
+ lockup.clickAction.actionMetrics.clearAll();
+ lockup.buttonAction.actionMetrics.clearAll();
+ if (lockup.buttonAction instanceof models.OfferStateAction) {
+ lockup.buttonAction.defaultAction.actionMetrics.clearAll();
+ }
+ }
+ if (!objectGraph.bag.arcadeDownloadPacksImpressionEventsEnabled) {
+ lockup.impressionMetrics = null;
+ }
+}
+//# sourceMappingURL=lockups.js.map \ No newline at end of file