import { isSome } from "@jet/environment"; import * as models from "../../../api/models"; import * as serverData from "../../../foundation/json-parsing/server-data"; import * as mediaDataFetching from "../../../foundation/media/data-fetching"; import * as mediaNetwork from "../../../foundation/media/network"; import { ResponseMetadata } from "../../../foundation/network/network"; import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; import * as urls from "../../../foundation/network/urls"; import * as mediaUrlMapping from "../../builders/url-mapping"; import { shouldUsePrerenderedIconArtwork } from "../../content/content"; import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; import * as metricsHelpersLocation from "../../metrics/helpers/location"; import * as impressionDemotion from "../../personalization/on-device-impression-demotion"; import * as productVariants from "../../product-page/product-page-variants"; import * as refresh from "../../refresh/page-refresh-controller"; import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; /** * A GroupingShelfController is responsible for all the logic around parsing and rending * a single grouping page shelf. * * The `ShelfMetadata` is a type the is specific for a given shelf and has some additional data needed to render * that shelf. */ export class GroupingShelfController { // endregion // region AnyGroupingShelfController /** * Indicates whether this grouping shelf controller can create a shelf for the given mediaApiData. * @param objectGraph The App Store dependency graph * @param mediaApiData The outer data object containing the FC properties and data * @param featuredContentId The featured content id for this shelf data * @param nativeGroupingShelfId The id of the custom shelf type, one not defined on the server */ supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { return this._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId); } /** * Indicates whether this grouping shelf controller can create a shelf for the given mediaApiData. * @param objectGraph The App Store dependency graph * @param mediaApiData The outer data object containing the FC properties and data * @param featuredContentId The featured content id for this shelf data * @param nativeGroupingShelfId The id of the custom shelf type, one not defined on the server */ _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { const isFeaturedContentIdSupported = this.supportedFeaturedContentIds.has(featuredContentId); let isNativeGroupingShelfIdSupported; if (serverData.isDefinedNonNull(nativeGroupingShelfId)) { isNativeGroupingShelfIdSupported = this.supportedNativeGroupingShelfIds.has(nativeGroupingShelfId); } else { isNativeGroupingShelfIdSupported = true; } return isFeaturedContentIdSupported && isNativeGroupingShelfIdSupported; } /** * This method will return a grouping page shelf regardless of the type of controller * @param objectGraph The App Store dependency graph * @param groupingParseContext The parse context for the grouping page so far * @param mediaApiData The outer data object containing the FC properties and data * @param baseShelfToken The base grouping shelf token created by the grouping-controller * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the * grouping page controller */ createShelf(objectGraph, mediaApiData, groupingParseContext, baseShelfToken, baseMetricsOptions) { var _a, _b, _c; const typedMediaApiData = mediaApiData; const shelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, typedMediaApiData); const shelfToken = this.shelfTokenFromBaseTokenAndMediaApiData(objectGraph, typedMediaApiData, baseShelfToken, groupingParseContext); const shelfMetricsOptions = this.shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions); const hasShelfMetricsOptions = serverData.isDefinedNonNullNonEmpty(shelfMetricsOptions); if (hasShelfMetricsOptions && this.shouldImpressShelf()) { metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, shelfToken.title); } /// Reorder the shelf contents based on the impression data if available if (serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) { shelfData.shelfContents = impressionDemotion.personalizeDataItems(shelfData.shelfContents, (_a = groupingParseContext.recoImpressionData) !== null && _a !== void 0 ? _a : {}, (_b = baseMetricsOptions === null || baseMetricsOptions === void 0 ? void 0 : baseMetricsOptions.recoMetricsData) !== null && _b !== void 0 ? _b : {}); } const shelf = this._createShelf(objectGraph, shelfToken, shelfData, groupingParseContext); if (hasShelfMetricsOptions && this.shouldImpressShelf()) { metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker); if (serverData.isDefinedNonNull(shelf)) { metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); // rdar://84952935 (Placeholder shelves are not being impressed // For placeholder shelves we end up replacing the entire shelf, so we need to make sure the original // impression metrics are included in the token, so they can be added when the real content is fetched // We're doing this here because this is where we decide whether the original shelf should be impressed if (((_c = shelf.url) === null || _c === void 0 ? void 0 : _c.length) > 0 && serverData.isDefinedNonNullNonEmpty(shelf.impressionMetrics) && shelfToken.showingPlaceholders) { const originalShelfUrlString = shelf.url; try { // Extract the token from the URL. // Note: Although we have access to the shelfToken here, we do not know that // the url was constructed from token in its current state. To be safe, // if not efficient, we reverse engineer the URL to get the token. const originalShelfUrl = urls.URL.from(originalShelfUrlString); const encodedToken = originalShelfUrl.pathComponents().pop(); const shelfTokenFromUrl = JSON.parse(decodeURIComponent(encodedToken)); // Modify the token to include the impressions metrics. shelfTokenFromUrl.originalPlaceholderShelfImpressionMetrics = shelf.impressionMetrics; groupingShelfControllerCommon.updateShelfUrlWithNewToken(objectGraph, shelf, shelfTokenFromUrl); } catch { shelf.url = originalShelfUrlString; } } } } this.finalizeInitialShelfForDisplay(objectGraph, shelf, shelfToken, shelfData, groupingParseContext); if (hasShelfMetricsOptions && this.shouldPrepareLocationTrackerForNextPosition()) { metricsHelpersLocation.nextPosition(groupingParseContext.metricsLocationTracker); } return shelf; } /** * Initialize a builder with globally unique name. * * @param {string} builderClass Globally unique name. */ constructor(builderClass) { // region Supported Types this.supportedFeaturedContentIds = new Set([]); this.supportedNativeGroupingShelfIds = new Set([]); this.builderClass = builderClass; } /** * Determines the strategy for fetching incomplete shelves based on feature flags and shelf type * * @param objectGraph - The application store object graph. * @returns The strategy for fetching incomplete shelves, either on shelf appearance or on page load. */ incompleteShelfFetchStrategy(objectGraph) { if (objectGraph.client.isiOS) { return models.IncompleteShelfFetchStrategy.OnShelfWillAppear; } else { return models.IncompleteShelfFetchStrategy.OnPageLoad; } } // endregion // region Metrics /** * Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping * page controller * * @param objectGraph The App Store dependency graph * @param shelfToken The shelf shelfToken for this current shelf creation request * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the * grouping page controller */ shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { return baseMetricsOptions; } /** * Whether the shelf itself should be impressed, there are some cases where the shelf itself * does not get impressed, just the contents. */ shouldImpressShelf() { return true; } /** * Whether we should move the location tracker to the next position after creating our shelf */ shouldPrepareLocationTrackerForNextPosition() { return true; } // endregion // Shelf Finalization /** * This method will set any required fields on our shelf once it has created as part of the initial page rendering. * This includes things like timing metrics, hiding empty shelves etc. * * @param objectGraph The App Store dependency graph * @param shelf The created shelf * @param shelfToken The shelf shelfToken for this current shelf creation request * @param shelfData The media api shelfContents array for this shelf * @param groupingParseContext The parse context for the grouping page so far * @private */ finalizeInitialShelfForDisplay(objectGraph, shelf, shelfToken, shelfData, groupingParseContext) { var _a, _b; if (serverData.isNullOrEmpty(shelf)) { return; } // Should not show see all links on search groupings if (shelfToken.isSearchLandingPage) { groupingShelfControllerCommon.modifyShelfForSearchLandingGrouping(objectGraph, shelf, shelfToken); } if (((_a = shelf.url) === null || _a === void 0 ? void 0 : _a.length) > 0 && serverData.isDefinedNonNullNonEmpty(groupingParseContext.additionalShelfParameters)) { shelf.url = urls.URL.from(shelf.url) .append("query", groupingParseContext.additionalShelfParameters) .build(); } // If we're on iOS and no prior fetch strategy has been defined, set the fetchStrategy to OnShelfWillAppear. shelf.fetchStrategy = this.incompleteShelfFetchStrategy(objectGraph); // Shelf will fetch content after sending follow-up fetch request. const willFetchShelfContent = isSome(shelf) && ((_b = shelf.url) === null || _b === void 0 ? void 0 : _b.length) > 0; if (serverData.isNullOrEmpty(shelf.items) && !willFetchShelfContent) { shelf.isHidden = true; } shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf); } /** * This method will set any required fields on our shelf once it has been fetched as a result of a secondary fetch. * This includes things like timing metrics, hiding empty shelves etc. * * @param objectGraph The AppStore dependency graph * @param shelf The created shelf * @param shelfToken The shelf shelfToken for this current shelf creation request * @param shelfData The media api shelfContents array for this shelf * @private */ finalizeSecondaryShelfForDisplay(objectGraph, shelf, shelfToken, shelfData) { if (serverData.isNullOrEmpty(shelf)) { return; } if (shelfToken.remainingItems.length) { const remainingIds = shelfToken.remainingItems.map((data) => { return data.id; }); objectGraph.console.warn("Could not load items for: " + remainingIds.join(",")); } if (shelf) { shelf.mergeWhenFetched = groupingShelfControllerCommon.shelfFetchShouldMergeWhenFetched(objectGraph, shelfToken); shelf.networkTimingMetrics = shelfData.responseTimingValues; shelf.nextPreferredContentRefreshDate = refresh.nextPreferredContentRefreshDateForController(refresh.newPageRefreshController()); } // Merge the original impression metrics with the newly created impression metrics. if (serverData.isDefinedNonNullNonEmpty(shelfToken.originalPlaceholderShelfImpressionMetrics)) { // If the `shelf.impressionMetrics` is null, we just defer to the original metrics. if (serverData.isNull(shelf.impressionMetrics)) { shelf.impressionMetrics = shelfToken.originalPlaceholderShelfImpressionMetrics; } else { for (const key in shelfToken.originalPlaceholderShelfImpressionMetrics.fields) { if (Object.prototype.hasOwnProperty.call(shelfToken.originalPlaceholderShelfImpressionMetrics.fields, key)) { shelf.impressionMetrics.fields[key] = shelfToken.originalPlaceholderShelfImpressionMetrics.fields[key]; } } } } if (!shelfToken.hasExistingContent && serverData.isNullOrEmpty(shelf.items)) { shelf.isHidden = true; } // Should not show see all links on search groupings if (shelfToken.isSearchLandingPage) { groupingShelfControllerCommon.modifyShelfForSearchLandingGrouping(objectGraph, shelf, shelfToken); } shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf); } // endregion // region ShelfBuilder async handleShelf(objectGraph, url, parameters, matchedRuleIdentifier) { const tokenJson = parameters["token"]; const shelfToken = JSON.parse(tokenJson); shelfToken.isFirstRender = false; try { const shelfData = await this.secondaryShelfDataForShelfUrl(objectGraph, url, shelfToken, parameters); const shelf = this._createShelf(objectGraph, shelfToken, shelfData, null); this.finalizeSecondaryShelfForDisplay(objectGraph, shelf, shelfToken, shelfData); return shelf; } catch (error) { if (shelfToken && !shelfToken.hasExistingContent) { const hiddenShelf = new models.Shelf(shelfToken.shelfStyle); hiddenShelf.isHidden = true; return hiddenShelf; } else { throw error; } } } shelfRoute(objectGraph) { if (serverData.isDefinedNonNullNonEmpty(this.supportedNativeGroupingShelfIds)) { return routesForNativeGroupingShelfIds(this.supportedNativeGroupingShelfIds); } else { return routesForFeaturedContentIds(this.supportedFeaturedContentIds); } } // endregion // region Static Base Helpers /** * This is a standard default implementation for the secondary shelf data fetch. This can be used for all the * grouping shelf controls that dont implement a custom ShelfDataType * @param objectGraph * @param shelfUrl * @param parameters */ static async secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => { const hydratedItems = groupingShelfControllerCommon.hydratedRemainingItemsForShelfTokenFromMediaApiData(objectGraph, shelfToken, mediaApiData); return { shelfContents: hydratedItems, responseTimingValues: mediaApiData[ResponseMetadata.timingValues], }; }); } /** * This is a standard default implementation for the media api request, for an incomplete grouping shelf. * * @param objectGraph * @param shelfUrl * @param parameters */ static async secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters) { const urlString = shelfUrl.build(); let request; if (mediaUrlMapping.isMediaUrl(objectGraph, shelfUrl)) { request = new mediaDataFetching.Request(objectGraph, urlString); } else { request = groupingShelfControllerCommon.generateShelfRequest(objectGraph, shelfToken, parameters); } if (!request) { return await Promise.reject(new Error(`Could not construct media API request for: ${shelfUrl}`)); } request.includingAdditionalPlatforms(defaultMediaApiPlatforms(objectGraph)); request.includingAttributes(defaultMediaApiAttributes(objectGraph)); request.usingCustomAttributes(productVariants.shouldFetchCustomAttributes(objectGraph)); request.attributingTo(shelfUrl.build()); return await mediaNetwork.fetchData(objectGraph, request).then((mediaApiData) => { groupingShelfControllerCommon.flushRequestedItemsFromShelfToken(shelfToken, request.ids); return mediaApiData; }); } } export function createShelfAccessibilityMetadata(objectGraph, shelf) { var _a; let accessibilityLabel = objectGraph.loc.string("Shelves.Accessibility.Label"); if (isSome(shelf.title)) { accessibilityLabel = `${shelf.title}, ${accessibilityLabel}`; } else if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.title)) { accessibilityLabel = `${shelf.header.title}, ${accessibilityLabel}`; } const accessibilityRoleDescription = objectGraph.loc.string("Shelves.Accessibility.RoleDescription"); return { label: accessibilityLabel, roleDescription: accessibilityRoleDescription, }; } function routeForFeaturedContentId(featuredContentId, nativeGroupingShelfId, additionalQueryParams) { const query = serverData.isDefinedNonNullNonEmpty(additionalQueryParams) ? [...additionalQueryParams] : []; query.push(`${Parameters.groupingFeaturedContentId}=${featuredContentId}`); if (serverData.isDefinedNonNullNonEmpty(nativeGroupingShelfId)) { query.push(`${Parameters.nativeGroupingShelfId}=${nativeGroupingShelfId}`); } return { protocol: Protocol.internal, path: `/${Path.grouping}/${Path.shelf}/{token}`, query: query, }; } export function routesForFeaturedContentIds(featuredContentIds, additonalQueryParams) { const routes = []; for (const featuredContentId of featuredContentIds) { routes.push(routeForFeaturedContentId(featuredContentId, null, additonalQueryParams)); } return routes; } export function routesForNativeGroupingShelfIds(nativeGroupingShelfIds, additonalQueryParams) { const routes = []; for (const nativeGroupingShelfId of nativeGroupingShelfIds) { routes.push(routeForFeaturedContentId(-1 /* FeaturedContentID.Native_GroupingShelf */, nativeGroupingShelfId, additonalQueryParams)); } return routes; } // region Media Api Attributes function defaultMediaApiPlatforms(objectGraph) { return mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph); } function defaultMediaApiAttributes(objectGraph) { const attributes = ["editorialArtwork", "isAppleWatchSupported", "requiredCapabilities", "badge-content"]; if (objectGraph.appleSilicon.isSupportEnabled) { attributes.push("macRequiredCapabilities"); } if (objectGraph.client.isMac) { attributes.push("hasMacIPAPackage"); } if (objectGraph.bag.enableUpdatedAgeRatings) { attributes.push("ageRating"); } if (shouldUsePrerenderedIconArtwork(objectGraph)) { attributes.push("iconArtwork"); } return attributes; } // endregion //# sourceMappingURL=grouping-shelf-controller.js.map