import * as validation from "@jet/environment/json/validation"; import * as models from "../../../api/models"; import * as serverData from "../../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../../foundation/media/attributes"; import * as mediaDataStructure from "../../../foundation/media/data-structure"; import * as mediaNetwork from "../../../foundation/media/network"; import * as mediaRelationship from "../../../foundation/media/relationships"; import { ResponseMetadata } from "../../../foundation/network/network"; import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; import * as urls from "../../../foundation/network/urls"; import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; import * as metricsHelpersLocation from "../../metrics/helpers/location"; import * as odpCommon from "../../personalization/on-device-recommendations-common"; import { defaultRoomShelfContentType } from "../../room/room-page"; import { platformPrefersLargeTitles } from "../../room/room-common"; import * as groupingTypes from "../grouping-types"; import { createLockupShelf, lockupShelfTokenFromBaseTokenAndMediaApiData, } from "./grouping-lockup-shelf-controller-common"; import { GroupingShelfController, routesForFeaturedContentIds } from "./grouping-shelf-controller"; import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; import { isNothing, isSome } from "@jet/environment"; export class GroupingPersonalizedLockupShelfController extends GroupingShelfController { // region Constructors constructor() { super("GroupingPersonalizedLockupShelfController"); this.supportedFeaturedContentIds = groupingTypes.recommendationsLockupShelfFeaturedContentIds; } // endregion // region Shelf Builder shelfRoute(objectGraph) { return routesForFeaturedContentIds(this.supportedFeaturedContentIds, [ `${Parameters.isOnDeviceRecommendationsShelf}?`, `${Parameters.onDeviceRecommendationsUseCase}?`, ]); } // endregion // region Shelf Creation Prerequisites /** * For a given mediaApiData extract the actual shelfContents array needed to render this shelf * * @param mediaApiData The outer shelfContents object containing the shelf contents */ initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { const shelfContents = mediaRelationship.relationship(mediaApiData, "contents"); let shelfData = shelfContents ? shelfContents.data : null; if (!shelfData || shelfData.length === 0) { shelfData = mediaRelationship.relationshipCollection(mediaApiData, "children"); } const recoMetrics = mediaDataStructure.metricsFromMediaApiObject(shelfContents); return { shelfContents: shelfData || [], containsODPShelfContents: false, recoMetrics: recoMetrics, candidates: null, isHiddenShelf: objectGraph.client.isWeb, responseTimingValues: null, }; } /** * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` * needed to render this shelf * * @param objectGraph The App Store dependency graph * @param shelfUrl The url that this controller handled on a secondary fetch * @param parameters The extracted parameters from the shelf url */ async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { var _a; const onDeviceRecommendationsUseCase = parameters[Parameters.onDeviceRecommendationsUseCase]; if ((onDeviceRecommendationsUseCase === null || onDeviceRecommendationsUseCase === void 0 ? void 0 : onDeviceRecommendationsUseCase.length) > 0) { return await odpCommon .recommendedAppsForUseCase(objectGraph, onDeviceRecommendationsUseCase, "shelf") .then((response) => { return { shelfContents: mediaDataStructure.dataCollectionFromDataContainer(response.dataContainer), containsODPShelfContents: true, recoMetrics: response.recoMetrics, candidates: response.candidates, isHiddenShelf: false, }; }) .catch(async (error) => { if (error instanceof odpCommon.NoODPCandidatesError) { return await GroupingPersonalizedLockupShelfController.fetchODPFallbackContent(objectGraph, shelfUrl, shelfToken, parameters).catch((fallbackError) => { return GroupingPersonalizedLockupShelfController.makeHiddenShelfData(shelfToken); }); } else { return GroupingPersonalizedLockupShelfController.makeHiddenShelfData(shelfToken); } }); } else if (((_a = shelfToken.recommendationsHref) === null || _a === void 0 ? void 0 : _a.length) > 0) { return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => { const personalizedFeaturedContentData = mediaDataStructure.dataFromDataContainer(objectGraph, mediaApiData); const personalizedLockupShelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, personalizedFeaturedContentData); personalizedLockupShelfData.responseTimingValues = mediaApiData[ResponseMetadata.timingValues]; personalizedLockupShelfData.shelfTitle = mediaAttributes.attributeAsString(personalizedFeaturedContentData, "name"); return personalizedLockupShelfData; }); } else { return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => { const personalizedFeaturedContentData = mediaDataStructure.dataCollectionFromDataContainer(mediaApiData); const personalizedLockupShelfData = { shelfContents: personalizedFeaturedContentData || [], containsODPShelfContents: false, recoMetrics: null, candidates: null, isHiddenShelf: false, responseTimingValues: mediaApiData[ResponseMetadata.timingValues], }; personalizedLockupShelfData.shelfTitle = shelfToken.title; return personalizedLockupShelfData; }); } } /** * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type * * @param objectGraph The App Store dependency graph * @param baseShelfToken The base grouping shelf token created by the grouping-controller * @param mediaApiData The outer data object containing the FC properties and data * @param groupingParseContext The parse context for the grouping page so far */ shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { const personalizedLockupShelfToken = lockupShelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext); this.addPersonalizationValuesToShelfToken(objectGraph, personalizedLockupShelfToken, mediaApiData, groupingParseContext); return personalizedLockupShelfToken; } // endregion // region Shelf Creation /** * * @param objectGraph The App Store dependency graph * @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 used to generate the grouping page on the initial page load, * this will be missing when this controller renders a secondary or incomplete shelf fetch. */ _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { var _a, _b; if (!shelfToken.isValidRecommendationsShelf) { return null; } let shelf = null; if (isNothing(shelfToken.title)) { // If we've fetched the title in a secondary lookup, update the title here // in the token, since other places may rely on it. shelfToken.title = shelfData.shelfTitle; } if (shelfData.containsODPShelfContents) { shelf = this.personalizedShelf(objectGraph, shelfData, shelfToken, groupingParseContext); const seeAllUrl = new urls.URL() .set("protocol", Protocol.internal) .path(`${Path.onDeviceRecommendations}`) .param(Parameters.onDeviceRecommendationsUseCase, shelfToken.onDeviceRecommendationsUseCase) .param(Parameters.token, JSON.stringify(shelfToken)) .build(); const seeAllAction = new models.FlowAction("page", seeAllUrl); seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); seeAllAction.pageData = this.odpSeeAllPage(objectGraph, shelfData.shelfTitle, defaultRoomShelfContentType); groupingShelfControllerCommon.replaceShelfSeeAllAction(objectGraph, shelf, seeAllAction); shelf.mergeWhenFetched = false; } else if (shelfData.isHiddenShelf) { shelf = GroupingPersonalizedLockupShelfController.makeHiddenShelf(shelfToken); } else if (serverData.isDefinedNonNull(shelfToken.recommendationsHref)) { shelf = this.personalizedShelf(objectGraph, shelfData, shelfToken, groupingParseContext); shelf.url = this.addOnDeviceQueryParamsIfNecessary(objectGraph, shelf.url, shelfToken); } else { shelf = createLockupShelf(objectGraph, shelfToken, shelfData, groupingParseContext); shelf.url = this.addOnDeviceQueryParamsIfNecessary(objectGraph, shelf.url, shelfToken); } const isUsingHeader = isSome(shelf.header); const isMissingHeaderTitle = isUsingHeader && serverData.isNullOrEmpty((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.title); const isMissingLegacyShelfTitle = !isUsingHeader && serverData.isNullOrEmpty(shelf.title); const hasShelfDataTitle = ((_b = shelfData.shelfTitle) === null || _b === void 0 ? void 0 : _b.length) > 0; if (isMissingHeaderTitle && hasShelfDataTitle) { shelf.header.title = shelfData.shelfTitle; } else if (isMissingLegacyShelfTitle && hasShelfDataTitle) { shelf.title = shelfData.shelfTitle; } return shelf; } // endregion // region Helpers /** * * @param objectGraph The App Store dependency graph * @param shelfData The media api shelfContents array for this shelf * @param shelfToken The shelf shelfToken for this current shelf creation request * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, * @private */ personalizedShelf(objectGraph, shelfData, shelfToken, groupingParseContext) { /** * Update shelf impressions for personalized shelves. * reco_algo_id doesn't get passed to Figaro for out of band personalization marker * * By happenstance, the location tracker encoded in the shelf token usually contains the swoosh location, given the happy path fetches *more* items. * For Personalized shelves, we need to pop the "empty" shelf location and replace it with the fully populated one. */ const location = metricsHelpersLocation.currentLocation(shelfToken.metricsLocationTracker); if (location && location.fcKind === shelfToken.featuredContentId) { metricsHelpersLocation.popLocation(shelfToken.metricsLocationTracker); } const shelfMetricsOptions = { id: shelfToken.id, kind: null, softwareType: serverData.asBooleanOrFalse(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage) ? "Arcade" : null, targetType: "swoosh", title: shelfToken.title, pageInformation: shelfToken.metricsPageInformation, locationTracker: shelfToken.metricsLocationTracker, idType: "its_contentId", fcKind: shelfToken.featuredContentId, recoMetricsData: shelfData.recoMetrics, displayStyle: shelfToken.shelfStyle, }; metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, shelfToken.title); const shelf = createLockupShelf(objectGraph, shelfToken, shelfData, groupingParseContext); metricsHelpersLocation.popLocation(shelfToken.metricsLocationTracker); metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); return shelf; } /** * Check in the media api data for this shelf and add any of the necessary personalization fields to the shelfToken. * * @param objectGrpah * @param shelfToken The shelf token in its current state * @param mediaApiData The media api data for this section of the grouping page * @param groupingParseContext The parse context for the grouping page so far * @private */ addPersonalizationValuesToShelfToken(objectGrpah, shelfToken, mediaApiData, groupingParseContext) { const isPersonalizedShelfMarker = shelfToken.featuredContentId === 476 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedShelfMarker */; // If we don't have a logged in user don't show personalized shelves // For search grouping we allow these shelves even if there is no logged in user. // For the personalized shelf markers we are going to allow reco to send back a fallback list of ids so that // even logged out users can see these shelves. // rdar://64005675 (Client to not drop personalized marker response for non-signed users (dsId - 0)) if (!groupingParseContext.hasAuthenticatedUser && !shelfToken.isSearchLandingPage && !isPersonalizedShelfMarker) { objectGrpah.console.log(`Skipping recommendations shelf with fcID ${shelfToken.featuredContentId}: No user logged-in`); return; } const onDevicePersonalizationUseCase = mediaAttributes.attributeAsString(mediaApiData, "onDevicePersonalizationUseCase"); const usingOnDevicePersonalization = (onDevicePersonalizationUseCase === null || onDevicePersonalizationUseCase === void 0 ? void 0 : onDevicePersonalizationUseCase.length) > 0; if (usingOnDevicePersonalization) { shelfToken.onDeviceRecommendationsUseCase = onDevicePersonalizationUseCase; shelfToken.recommendationsHref = mediaApiData.href; shelfToken.isValidRecommendationsShelf = true; return; } const shelfContents = mediaRelationship.relationshipCollection(mediaApiData, "contents"); const hasContents = serverData.isDefinedNonNullNonEmpty(shelfContents); const shelfPersonalizationAvailable = !mediaAttributes.attributeAsBooleanOrFalse(mediaApiData, "noPersonalizationAvailable"); if (!hasContents && shelfPersonalizationAvailable) { shelfToken.recommendationsHref = mediaApiData.href; shelfToken.isValidRecommendationsShelf = true; // tslint:disable-next-line:no-redundant-jump } else { shelfToken.isValidRecommendationsShelf = hasContents; } } /** * For a given shelf url, add the on device personalization query param if its needed * * @param objectGraph * @param urlString The original url string for this shelf * @param shelfToken The shelf shelfToken for this current shelf creation request */ addOnDeviceQueryParamsIfNecessary(objectGraph, urlString, shelfToken) { var _a; if (serverData.isNullOrEmpty(urlString)) { return null; } const isOnDeviceRecommendationsGamesForYouEnabled = objectGraph.host.isiOS; if (!isOnDeviceRecommendationsGamesForYouEnabled) { return urlString; } if (((_a = shelfToken.onDeviceRecommendationsUseCase) === null || _a === void 0 ? void 0 : _a.length) > 0) { const url = new urls.URL(urlString); url.param(Parameters.isOnDeviceRecommendationsShelf, "true"); url.param(Parameters.onDeviceRecommendationsUseCase, shelfToken.onDeviceRecommendationsUseCase); return url.build(); } else { return urlString; } } /** * Creates a page that can be used for side-packing see all pages into a room. * * @param objectGraph * @param {string} title The title of the destination page * @param {ShelfContentType} preferredShelfContentType The content type to use for the page * @returns {GenericPage} A GenericPage which will use the parentShelfItems from the see all to render the initial room */ odpSeeAllPage(objectGraph, title, preferredShelfContentType) { const shelf = new models.Shelf(preferredShelfContentType || defaultRoomShelfContentType); shelf.isHorizontal = false; shelf.items = "parentShelfItems"; const page = new models.GenericPage([shelf]); page.isIncomplete = true; page.title = title; if (platformPrefersLargeTitles(objectGraph)) { page.presentationOptions = ["prefersLargeTitle"]; } return page; } // endregion // region ODP Fallback static async fetchODPFallbackContent(objectGraph, url, shelfToken, parameters) { return await new Promise((resolve, reject) => { const fallbackRequest = groupingShelfControllerCommon.generateShelfRequest(objectGraph, shelfToken, parameters); if (!fallbackRequest) { const errorMessage = `OnDeviceRecommendationsShelfController: Could not construct media API request for: ${url}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); return; } groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, fallbackRequest); fallbackRequest.attributingTo(url.build()); mediaNetwork .fetchData(objectGraph, fallbackRequest) .then((dataContainer) => { const shelfData = mediaDataStructure.dataFromDataContainer(objectGraph, dataContainer); const shelfContents = mediaRelationship.relationship(shelfData, "contents"); const recoMetrics = mediaDataStructure.metricsFromMediaApiObject(shelfContents); const fallbackShelfData = { shelfContents: mediaDataStructure.dataCollectionFromDataContainer(shelfContents), containsODPShelfContents: false, recoMetrics: recoMetrics, candidates: null, responseTimingValues: dataContainer[ResponseMetadata.timingValues], }; resolve(fallbackShelfData); }) .catch((error) => { const errorMessage = `OnDeviceRecommendationsShelfController: Failed to fetch fallback shelf contents: ${url}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(error); }); }); } static makeHiddenShelfData(shelfToken) { const hiddenShelfData = { shelfContents: [], containsODPShelfContents: false, recoMetrics: null, candidates: null, isHiddenShelf: true, }; return hiddenShelfData; } static makeHiddenShelf(shelfToken) { const hiddenShelf = new models.Shelf(shelfToken.shelfStyle); hiddenShelf.isHidden = true; return hiddenShelf; } } //# sourceMappingURL=grouping-personalized-lockup-shelf-controller.js.map