From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../src/common/search/content/search-categories.js | 334 +++++++ .../common/search/content/search-content-common.js | 31 + .../search/content/search-lockup-collection.js | 171 ++++ .../src/common/search/content/search-results.js | 723 ++++++++++++++ .../src/common/search/content/search-shelves.js | 53 + .../tmp/src/common/search/custom-creative.js | 71 ++ .../search/guided-search/guided-search-metrics.js | 132 +++ .../common/search/guided-search/guided-search.js | 105 ++ .../common/search/landing/search-landing-cohort.js | 84 ++ .../landing/search-landing-shelf-controller.js | 836 ++++++++++++++++ .../category-metadata-ribbon-item.js | 38 + .../metadata-ribbon/chart-metadata-ribbon-item.js | 75 ++ .../developer-metadata-ribbon-item.js | 37 + .../divider-metadata-ribbon-item.js | 8 + .../editors-choice-metadata-ribbon-item.js | 21 + .../game-controller-metadata-ribbon-item.js | 36 + .../metadata-ribbon-item-factory.js | 24 + .../search/metadata-ribbon/metadata-ribbon.js | 27 + ...nked-secondary-category-metadata-ribbon-item.js | 22 + .../search/metadata-ribbon/search-tags-ribbon.js | 72 ++ ...ondary-short-categories-metadata-ribbon-item.js | 27 + .../short-category-metadata-ribbon-item.js | 36 + .../star-rating-metadata-ribbon-item.js | 20 + .../metadata-ribbon/tag-metadata-ribbon-item.js | 18 + .../tmp/src/common/search/search-ads-odml.js | 87 ++ .../app-store/tmp/src/common/search/search-ads.js | 1047 ++++++++++++++++++++ .../tmp/src/common/search/search-common.js | 59 ++ .../tmp/src/common/search/search-facets.js | 146 +++ .../src/common/search/search-landing-page-utils.js | 386 ++++++++ .../tmp/src/common/search/search-page-url.js | 5 + .../src/common/search/search-results-fetching.js | 496 ++++++++++ .../search/search-results-learn-more-notice.js | 46 + .../src/common/search/search-results-pipeline.js | 248 +++++ .../src/common/search/search-spell-correction.js | 98 ++ .../tmp/src/common/search/search-token.js | 57 ++ .../app-store/tmp/src/common/search/search.js | 1024 +++++++++++++++++++ .../common/search/shelves/search-history-shelf.js | 148 +++ .../src/common/search/sponsored-search-fetching.js | 122 +++ .../tmp/src/common/search/web-search-action.js | 23 + 39 files changed, 6993 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/search.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/search') diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js new file mode 100644 index 0000000..927de7c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js @@ -0,0 +1,334 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationships from "../../../foundation/media/relationships"; +import { pageRouter } from "../../builders/routing"; +import { hrefToRoutableUrl } from "../../builders/url-mapping"; +import * as content from "../../content/content"; +import { addClickEventToAction, addClickEventToSeeAllAction } from "../../metrics/helpers/clicks"; +import { addImpressionFields } from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../../metrics/helpers/page"; +import { createClickMetricsOptionsForChartOrCategory, createMetricsOptionsForChartOrCategory, } from "../../metrics/helpers/search/search-shelves"; +import { combinedRecoMetricsDataFromMetricsData } from "../../metrics/helpers/util"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import * as searchShelves from "./search-shelves"; +import * as groupingShelfControllerCommon from "../../grouping/shelf-controllers/grouping-shelf-controller-common"; +/** + * Takes the raw page data and creates a charts and categories page + * @param objectGraph The App Store Object Graph + * @param resultData The data representing the charts and categories page + * @returns A charts and categories page + */ +export function searchChartsAndCategoriesPageFromData(objectGraph, resultData) { + var _a, _b; + /// Get the page's metadata + const pageTitle = mediaAttributes.attributeAsString(resultData, "title"); + const selectedTabId = serverData.asString(resultData, "meta.autoSelectedTabId"); + const sourceShelfCanonicalId = serverData.asString(resultData, "meta.sourceShelfCanonicalId"); + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "SearchLanding", sourceShelfCanonicalId, resultData); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + /// Generate a new SearchPageContext (mainly for making shelf contexts) + const searchPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + adStitcher: null, + adIncidentRecorder: null, + pageType: searchShelves.SearchPageType.ChartsAndCategories, + }; + /// Prep the shelves data, mappings, orderings, and page tabs list + const shelvesData = serverData.asArrayOrEmpty(resultData, "data"); + const shelvesMapping = {}; + const shelfOrdering = []; + const pageTabsCollector = []; + const pageTabsLocationTracker = metricsHelpersLocation.newLocationTracker(); + for (const shelfData of shelvesData) { + /// Generate the shelf attributes for the shelf + const shelfAttributes = searchShelves.shelfAttributesFromData(objectGraph, shelfData, models.SearchLandingPageContentKind.CategoriesAndCharts, models.SearchPageKind.CategoriesAndCharts); + /// Since the shelves act as their own page, we need to set a new location tracker for each shelf + const shelfPageContext = { + ...searchPageContext, + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + }; + /// Generate the shelf context + const shelfContext = searchShelves.baseShelfContext(objectGraph, shelfData, shelfAttributes, shelfPageContext); + /// Push the shelf content location so each shelf has the correct parent and starting index + metricsHelpersLocation.pushContentLocation(objectGraph, shelfContext.metricsOptions, shelfAttributes.title); + /// Make the shelf + const shelf = createChartsCategoryShelf(objectGraph, shelfData, true, shelfAttributes, shelfPageContext, shelfContext); + metricsHelpersLocation.popLocation(shelfContext.metricsOptions.locationTracker); + if (isNothing(shelf)) { + continue; + } + /// Add the shelf to the shelves mapping for the shelf id + shelvesMapping[shelf.id] = shelf; + /// Add the shelf id to the shelf ordering (to maintain ordered shelves) + shelfOrdering.push(shelf.id); + /// Make a pageTab metadata model from the shelf and action + const pageTab = new models.PageTab(); + const action = new models.PageTabChangeAction(shelfData.id, shelfAttributes.title); + /// Generate the click actions + const pageTabClickActionOptions = { + id: shelfAttributes.title, + canonicalId: (_a = serverData.asString(shelfData.meta, "canonicalId")) !== null && _a !== void 0 ? _a : undefined, + actionType: "navigate", + targetType: "button", + pageInformation: searchPageContext.metricsPageInformation, + locationTracker: pageTabsLocationTracker, + }; + addClickEventToAction(objectGraph, action, pageTabClickActionOptions); + pageTab.action = action; + pageTab.id = shelf.id; + pageTab.title = `${(_b = shelf.title) !== null && _b !== void 0 ? _b : ""}`; /// We need a deep copy of the string as we will remove the reference possibly later. + pageTabsCollector.push(pageTab); + metricsHelpersLocation.nextPosition(pageTabsLocationTracker); + } + /// Return an empty page if there are no real shelves to show + if (!serverData.isDefinedNonNullNonEmpty(shelfOrdering)) { + return new models.SearchChartsAndCategoriesPage(); + } + /// Create the page tabs + const pageTabs = new models.PageTabs(); + /// The id needs to be static but it isn't tied to the payload + pageTabs.id = objectGraph.random.nextUUID(); + /// Add the pageTabs as its own shelf so it can be independent of the chart and category shelves + /// as well as to control the segmented control for swapping between category shelf orderings (e.g. category containers) + const pageTabsShelf = new models.Shelf("pageTabs"); + pageTabsShelf.items = [pageTabs]; + shelvesMapping[pageTabs.id] = pageTabsShelf; + /// Make the page and add the shelf mappings (which includes our pageTabs shelf) + const page = new models.SearchChartsAndCategoriesPage(); + page.shelfMapping = shelvesMapping; + const shelfOrderings = {}; + /** Normally, we would have the shelf orderings be based on some identifier in the payload on a container which contains + an array of shelves. E.g. + { + Container { + containerId: string + shelves: [ + { + shelfId1: string, + ... + }, + ... + ] + } + } + shelf orderings = { containerId: [shelfId1, ...]} + + However, the current protocol has no container, just a loose array of shelves. + { + shelves: [ + { + shelfId1: string, + ... + }, + ... + ] + } + + Therefore, we have to fake our ordering by having the ordering be the shelves' own identifiers. + + shelf orderings = { shelfId1: [shelfId1], shelfId2: [shelfId2], ...} + + We also will append the pageTabs shelf if we have multiple shelves representing multiple tabs on the page. + */ + for (const shelfOrderingElement of shelfOrdering) { + if (pageTabsCollector.length > 1) { + shelfOrderings[shelfOrderingElement] = [pageTabs.id, shelfOrderingElement]; + } + else { + shelfOrderings[shelfOrderingElement] = [shelfOrderingElement]; + } + } + for (const shelf of Object.values(shelvesMapping)) { + shelf.title = undefined; + } + /// Set the data on the page + page.title = pageTitle; + page.pageTabs = pageTabs; + page.columnCount = 2; + page.shelfOrderings = shelfOrderings; + /// If we don't have a reco selected default tab, just default to the first one + page.defaultShelfOrdering = shelfOrdering.includes(selectedTabId) ? selectedTabId : shelfOrdering[0]; + /// Set the data on the page tabs + pageTabs.tabs = pageTabsCollector; + pageTabs.selectedTabId = page.defaultShelfOrdering; + /// Add the page metrics + addMetricsEventsToPageWithInformation(objectGraph, page, searchPageContext.metricsPageInformation); + return page; +} +/** + * Creates a charts and categories shelf from the shelf data + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @param isForSeeAllPage Whether or not this shelf will be displayed on the see-all page + * @param shelfAttributes The shelf's attributes + * @param searchPageContext The context for the page this shelf belongs to + * @param searchShelfContext The shelf's context + * @returns A charts and categories shelf + */ +export function createChartsCategoryShelf(objectGraph, data, isForSeeAllPage, shelfAttributes, searchPageContext, searchShelfContext) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + const items = []; + const chartsAndCategoriesData = mediaRelationships.relationshipCollection(data, "contents"); + const shelf = new models.Shelf("searchChartsAndCategories"); + shelf.isHorizontal = false; + shelf.id = data.id; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { + shelf.contentsMetadata = { + type: "searchLandingChartsAndCategoriesSection", + numberOfColumns: shelfAttributes.displayStyle.layoutSize, + }; + } + if (shelfAttributes.hasSeeAll) { + const action = new models.FlowAction("searchChartsAndCategories"); + action.pageUrl = shelfAttributes.seeAllLink; + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + const seeAllMetricsOptions = { + ...searchShelfContext.metricsOptions, + targetType: "button", + }; + addClickEventToSeeAllAction(objectGraph, action, action.pageUrl, seeAllMetricsOptions); + shelf.seeAllAction = action; + } + addImpressionFields(objectGraph, shelf, searchShelfContext.metricsOptions); + const brickUseCase = isForSeeAllPage + ? content.SearchChartOrCategoryBrickUseCase.seeAllPage + : content.SearchChartOrCategoryBrickUseCase.other; + for (const chartOrCategory of chartsAndCategoriesData) { + let name = null; + let badge = null; + if (chartOrCategory.type === "tags") { + const brickName = (_b = mediaAttributes.attributeAsString(chartOrCategory, "name")) !== null && _b !== void 0 ? _b : "tagbrick"; + name = brickName; + } + else { + const editorialNotes = mediaAttributes.attributeAsDictionary(chartOrCategory, "editorialNotes"); + if (serverData.isDefinedNonNullNonEmpty(editorialNotes)) { + name = serverData.asString(editorialNotes, "name"); + badge = serverData.asString(editorialNotes, "badge"); + } + } + const artwork = content.searchChartOrCategoryArtworkFromData(objectGraph, chartOrCategory, brickUseCase, (_c = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _c === void 0 ? void 0 : _c.layoutDensity); + const kind = mediaAttributes.attributeAsString(chartOrCategory, "kind"); + const linkData = mediaAttributes.attributeAsDictionary(chartOrCategory, "link"); + const linkUrl = serverData.asString(linkData, "url"); + const dataCollection = mediaRelationships.relationshipCollection(chartOrCategory, "primary-content"); + let isTitleRequired = true; + let action = null; + if (chartOrCategory.type === "tags") { + const href = serverData.asString(chartOrCategory, "href"); + const url = hrefToRoutableUrl(objectGraph, href); + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(url); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = url; + flowAction.title = name; + action = flowAction; + } + else if (isSome(linkUrl)) { + switch (kind) { + case "CategoryChart": + const topChartAction = new models.FlowAction("topCharts"); + topChartAction.pageUrl = linkUrl; + topChartAction.title = name; + action = topChartAction; + break; + case "External": + // For external links, we don't require a title we can just show an image + isTitleRequired = false; + const target = serverData.asString(linkData, "target"); + if (target === "external") { + action = new models.ExternalUrlAction(linkUrl); + action.title = name !== null && name !== void 0 ? name : ""; + } + else { + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(linkUrl); + const linkAction = new models.FlowAction(flowPage); + linkAction.pageUrl = linkUrl; + linkAction.title = name !== null && name !== void 0 ? name : ""; + action = linkAction; + } + break; + default: + break; + } + } + else if (serverData.isDefinedNonNullNonEmpty(dataCollection)) { + const chartMetadata = dataCollection[0]; + const chartHref = chartMetadata.href; + const url = hrefToRoutableUrl(objectGraph, chartHref); + if ((url === null || url === void 0 ? void 0 : url.length) > 0) { + const flowAction = new models.FlowAction("page"); + flowAction.pageUrl = url; + flowAction.title = name; + action = flowAction; + } + else { + continue; + } + } + else { + continue; + } + const chartClickOptions = createClickMetricsOptionsForChartOrCategory(objectGraph, (_d = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _d === void 0 ? void 0 : _d.layoutDensity, chartOrCategory, searchShelfContext); + addClickEventToAction(objectGraph, action, chartClickOptions); + if (isTitleRequired && serverData.isNullOrEmpty(name)) { + continue; + } + let chartOrCategoryModel = new models.SearchChartOrCategory(name, artwork, null, null, badge, action, (_e = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _e === void 0 ? void 0 : _e.layoutDensity, artworkSafeArea((_f = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _f === void 0 ? void 0 : _f.layoutDensity), textSafeArea((_g = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _g === void 0 ? void 0 : _g.layoutDensity)); + let chartModelMetricsOptions = createMetricsOptionsForChartOrCategory(objectGraph, chartOrCategoryModel, chartOrCategory, searchShelfContext); + const artworkOptions = { + useCase: 18 /* content.ArtworkUseCase.GroupingBrick */, + }; + const collectionIcons = groupingShelfControllerCommon.artworkForTags(objectGraph, chartOrCategory, 1060, 520, artworkOptions, chartModelMetricsOptions); + if (isSome(collectionIcons) && collectionIcons.length > 0) { + const collectionIconBackgroundColor = collectionIcons[0].backgroundColor; + if (isSome(collectionIconBackgroundColor) && (collectionIconBackgroundColor === null || collectionIconBackgroundColor === void 0 ? void 0 : collectionIconBackgroundColor.type) === "rgb") { + chartOrCategoryModel = new models.SearchChartOrCategory(name, null, collectionIcons, (_h = content.closestTagBackgroundColorForIcon(collectionIconBackgroundColor)) !== null && _h !== void 0 ? _h : undefined, badge, action, (_j = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _j === void 0 ? void 0 : _j.layoutDensity, artworkSafeArea((_k = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _k === void 0 ? void 0 : _k.layoutDensity), textSafeArea((_l = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _l === void 0 ? void 0 : _l.layoutDensity)); + chartModelMetricsOptions = createMetricsOptionsForChartOrCategory(objectGraph, chartOrCategoryModel, chartOrCategory, searchShelfContext); + } + } + addImpressionFields(objectGraph, chartOrCategoryModel, chartModelMetricsOptions); + items.push(chartOrCategoryModel); + metricsHelpersLocation.nextPosition(searchShelfContext.metricsOptions.locationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + shelf.items = items; + return shelf; +} +function artworkSafeArea(density) { + switch (density) { + // Tile + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + return models.ChartOrCategorySafeArea.defaultTileArtworkSafeArea; + // Pill + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + return models.ChartOrCategorySafeArea.defaultPillArtworkSafeArea; + // Round + case models.GenericSearchPageShelfDisplayStyleDensity.Density3: + return undefined; + default: + return undefined; + } +} +function textSafeArea(density) { + switch (density) { + // Tile + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + return models.ChartOrCategorySafeArea.defaultTileTextSafeArea; + // Pill + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + return models.ChartOrCategorySafeArea.defaultPillTextSafeArea; + default: + return undefined; + } +} +//# sourceMappingURL=search-categories.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js new file mode 100644 index 0000000..59b2fe5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js @@ -0,0 +1,31 @@ +/** + * Common functions for search results content. + */ +import { isDefinedNonNullNonEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsBooleanOrFalse, attributeAsString } from "../../../foundation/media/attributes"; +// region Editorial Content +/** + * Returns the headline / tagline for Editorial Search Results + * @param resultData Data to determine tagline for. + */ +export function editorialSearchResultTagline(objectGraph, resultData) { + // Flag to disable showing specific headings, e.g. "App of the Day" that may be unnatural in search result + const showLabelInSearch = attributeAsBooleanOrFalse(resultData, "showLabelInSearch"); + if (!showLabelInSearch) { + return null; + } + // LOC: AOTD & GOTD badges in Search result for the Editorial Item Card AOTD & GOTD stories do not show up correctly for JA-JP and TH-TH + // Always use alternative label, if one is provided. + const alternateLabel = attributeAsString(resultData, "alternateLabel"); + if (isDefinedNonNullNonEmpty(alternateLabel)) { + return alternateLabel; // No newline flattening needed for alternativeLabel + } + // Otherwise, fallback to franchise label, if any. + const label = attributeAsString(resultData, "label"); + if (isDefinedNonNullNonEmpty(label)) { + return label.replace(/\n/g, " "); + } + return null; +} +// endregion +//# sourceMappingURL=search-content-common.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js new file mode 100644 index 0000000..c5c85e7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js @@ -0,0 +1,171 @@ +import { TodayCardDisplayStyle } from "./../../today/today-types"; +/** + * Builder for SearchLockupCollection. + */ +import { SearchLockupCollection } from "../../../api/models"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsArrayOrEmpty, attributeAsString } from "../../../foundation/media/attributes"; +import { relationshipCollection } from "../../../foundation/media/relationships"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import { notesFromData } from "../../content/content"; +import { currentEditorialCollectionTreatment, } from "../../../foundation/experimentation/search-results-experiments"; +import { actionFromData, lockupsFromData } from "../../lockups/lockups"; +import { addImpressionFields, impressionOptions } from "../../metrics/helpers/impressions"; +import { popLocation, pushContentLocation } from "../../metrics/helpers/location"; +import { cardDisplayStyleFromData } from "../../today/today-card-util"; +import { editorialSearchResultTagline } from "./search-content-common"; +/** + * Whether or not a given result data should be rendered as `SearchLockupCollection` model. + * Semantically, every `collection` should be rendered as a lockup collection, but collections are currently backed by many different types of articles. + * @param resultData Data from results endpoint. + * @param searchResponseMetadata Meta blob from initial search request. + */ +export function resultDataShouldRenderLockupCollection(objectGraph, resultData, searchResponseMetadata) { + // Filter supported platforms + if (!(objectGraph.host.isiOS || objectGraph.host.isVision)) { + return false; + } + if (currentEditorialCollectionTreatment(objectGraph, searchResponseMetadata) !== + 1 /* EditorialCollectionExperimentType.Swoosh */) { + return false; + } + switch (resultData.type) { + case "groupings": + // Groupings can only be rendered if `contentIds` is present + const collectionAdamIds = attributeAsArrayOrEmpty(resultData, "contentIds"); + return isDefinedNonNullNonEmpty(collectionAdamIds); + case "rooms": + case "multirooms": + // Rooms, and multirooms always render as collection. + return true; + case "editorial-items": + // Some subtypes of editorial items render as collection. + const cardDisplayStyle = cardDisplayStyleFromData(resultData); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.NumberedList: + case TodayCardDisplayStyle.Grid: + case TodayCardDisplayStyle.River: + return true; + default: + return false; + } + default: + return false; + } +} +/** + * Create a `SearchLockupCollection` from search results. + * @param resultData Data from results endpoint. + * @param lockupOptions Options for lockups shared throughout search results page. + * @param metricsOptions Metrics context. + */ +export function lockupCollectionFromResultData(objectGraph, resultData, metricsOptions) { + // Text + const heading = lockupCollectionHeadingFromResultData(objectGraph, resultData); + const headingArtwork = lockupCollectionHeadingArtworkFromResultData(objectGraph, resultData); + const title = lockupCollectionTitleFromResultData(objectGraph, resultData); + // Metrics for impression + location + const lockupCollectionMetrics = impressionOptions(objectGraph, resultData, title, { + targetType: "card", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }); + pushContentLocation(objectGraph, lockupCollectionMetrics, title); + // Click Action ("See All") + const clickMetrics = { + actionType: "navigate", + targetType: "button", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + id: "See All", + idType: "sequential", + }; + const seeAllAction = actionFromData(objectGraph, resultData, clickMetrics, objectGraph.host.clientIdentifier); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + // Lockups + const listOptions = { + lockupOptions: { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "lockup", + }, + skipDefaultClickAction: objectGraph.client.isVision, + artworkUseCase: 8 /* ArtworkUseCase.SearchIcon */, + hideCompatibilityBadge: objectGraph.client.isVision, + }, + filter: 128 /* Filter.UnsupportedPlatform */, + }; + let lockupData = relationshipCollection(resultData, "card-contents"); + if (isNullOrEmpty(lockupData)) { + lockupData = relationshipCollection(resultData, "top-apps"); + } + const items = lockupsFromData(objectGraph, lockupData, listOptions); + popLocation(metricsOptions.locationTracker); + // Lockup Collection + const lockupCollection = new SearchLockupCollection(heading, title, items, seeAllAction, headingArtwork); + addImpressionFields(objectGraph, lockupCollection, lockupCollectionMetrics); + if (isNullOrEmpty(items)) { + return null; + } + return lockupCollection; +} +// endregion +// region Heading +/** + * Returns the title for given editorial results search results data. + * @param resultData The result data to get a title for. + */ +function lockupCollectionTitleFromResultData(objectGraph, resultData) { + const resultType = resultData.type; + switch (resultType) { + case "developers": + return attributeAsString(resultData, "name"); + default: + return notesFromData(objectGraph, resultData, "name"); + } +} +/** + * Returns the heading for given editorial results search results data. + * This builds on `editorialSearchResultTagline` with some additional search-specific logic + * @param resultData The result data to get a heading for. + */ +function lockupCollectionHeadingFromResultData(objectGraph, resultData) { + const resultType = resultData.type; + if (resultType === "developers") { + return objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_DEVELOPER_TITLE_CASE"); + } + const editorialTagline = editorialSearchResultTagline(objectGraph, resultData); + if (isDefinedNonNullNonEmpty(editorialTagline)) { + return editorialTagline; + } + if (objectGraph.client.isVision) { + return objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_COLLECTION_TITLE_CASE"); + } + else { + return objectGraph.loc.string("Search.EditorialSearchResultType.Heading.Collection"); + } +} +/** + * Returns the heading artwork to use for a given editorial search results data. + * @param resultData The result data to get heading artwork for. + */ +function lockupCollectionHeadingArtworkFromResultData(objectGraph, resultData) { + if (!objectGraph.client.isVision) { + return null; + } + const resultType = resultData.type; + let imageName; + switch (resultType) { + case "developers": + imageName = "person.crop.square"; + break; + default: + imageName = "appstore"; + break; + } + return createArtworkForResource(objectGraph, `systemimage://${imageName}`); +} +// endregion +//# sourceMappingURL=search-lockup-collection.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js new file mode 100644 index 0000000..5f363b3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js @@ -0,0 +1,723 @@ +/** + * A builder for all SearchResultsContainers variants except for `SearchLockupCollection`. + * To be cleaned in the future. + */ +import * as validation from "@jet/environment/json/validation"; +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import { EditorialSearchResult } from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as errors from "../../../foundation/util/errors"; +import * as client from "../../../foundation/wrappers/client"; +import * as appEvents from "../../app-promotions/app-event"; +import * as appPromotionsCommon from "../../app-promotions/app-promotions-common"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import * as content from "../../content/content"; +import { editorialDisplayOptionsFromClientParams, extractEditorialClientParams, } from "../../editorial-pages/editorial-data-util"; +import * as filtering from "../../filtering"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersUtil from "../../metrics/helpers/util"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import { defaultTodayCardConfiguration, todayCardFromData } from "../../today/today-card-util"; +import { TodayCardDisplayStyle, TodayParseContext } from "../../today/today-types"; +import * as common from "./search-content-common"; +import * as searchLockupCollection from "./search-lockup-collection"; +export function searchResultFromData(objectGraph, resultData, searchResponseMetadata, personalizationDataContainer, metricsOptions, isNetworkConstrained, searchEntity, searchExperimentsData) { + return validation.context("searchResultFromData", () => { + let searchResult = null; + const resultType = resultData.type; + const standardLockupOptions = { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "card", + createUniqueImpressionId: true, + }, + hideZeroRatings: true, + artworkUseCase: 8 /* content.ArtworkUseCase.SearchIcon */, + isNetworkConstrained: isNetworkConstrained, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mixedMediaLockup"), + clientIdentifierOverride: clientIdentifierOverrideForSearchEntity(objectGraph, searchEntity), + isMultilineTertiaryTitleAllowed: false, + }; + const condensedBehavior = condensedBehaviorForData(objectGraph, resultData, searchExperimentsData); + switch (resultType) { + case "rooms": + case "multirooms": + case "developers": + case "editorial-items": + case "groupings": + if (resultType !== "editorial-items" && resultType !== "developers" && objectGraph.client.isVision) { + return null; + } + const todayCardConfig = defaultTodayCardConfiguration(objectGraph); + todayCardConfig.isSearchContext = true; + const todayCard = todayCardFromData(objectGraph, resultData, todayCardConfig, new TodayParseContext(metricsOptions.pageInformation, metricsOptions.locationTracker)); + if (todayCard && todayCard.media && todayCard.media.kind === "inAppPurchase") { + if (objectGraph.client.isVision) { + return null; + } + const iapMedia = todayCard.media; + const todayCardInAppPurchaseLockup = iapMedia.lockup; + todayCardInAppPurchaseLockup.theme = "dark"; + searchResult = new models.InAppPurchaseSearchResult(todayCardInAppPurchaseLockup); + } + else if (searchLockupCollection.resultDataShouldRenderLockupCollection(objectGraph, resultData, searchResponseMetadata)) { + const lockupCollection = searchLockupCollection.lockupCollectionFromResultData(objectGraph, resultData, metricsOptions); + if (lockupCollection) { + searchResult = lockupCollection; + } + } + else { + const editorialSearchResult = editorialSearchResultFromData(objectGraph, resultData, standardLockupOptions.metricsOptions, condensedBehavior); + if (editorialSearchResult) { + if (editorialSearchResult.title) { + editorialSearchResult.title = editorialSearchResult.title.replace(/\n/g, " "); + } + if (editorialSearchResult instanceof EditorialSearchResult && editorialSearchResult.subtitle) { + editorialSearchResult.subtitle = editorialSearchResult.subtitle.replace(/\n/g, " "); + } + searchResult = editorialSearchResult; + } + } + break; + case "in-apps": + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + return null; + } + // Ensure the parent app data is available before proceeding with building the lockup. + const parentData = lockups.parentDataFromInAppData(objectGraph, resultData); + if (serverData.isNullOrEmpty(parentData)) { + return null; + } + const inAppPurchaseLockup = lockups.inAppPurchaseLockupFromData(objectGraph, resultData, standardLockupOptions); + inAppPurchaseLockup.theme = "dark"; + modifyMetadataBadgeForSearchExperiment(objectGraph, inAppPurchaseLockup, searchExperimentsData); + searchResult = new models.InAppPurchaseSearchResult(inAppPurchaseLockup); + break; + case "apps": + case "app-bundles": + default: + // There should never be an iad in the non-ad search results, so remove it here + // before creating the lockups. + delete resultData.attributes["iad"]; + if (resultType === "app-bundles") { + if (objectGraph.client.isVision) { + return null; + } + standardLockupOptions.shouldIncludeScreenshotsForChildren = + objectGraph.featureFlags.isEnabled("voyager_bundles_2025A"); + const bundleLockup = lockups.lockupFromData(objectGraph, resultData, standardLockupOptions); + bundleLockup.showMetadataInformationInLockup = true; + modifyMetadataBadgeForSearchExperiment(objectGraph, bundleLockup, searchExperimentsData); + searchResult = new models.BundleSearchResult(bundleLockup); + } + else { + const lockup = lockups.mixedMediaLockupFromData(objectGraph, resultData, standardLockupOptions, { + canPlayFullScreen: false, + playbackControls: {}, + }, searchExperimentsData); + modifyMetadataBadgeForSearchExperiment(objectGraph, lockup, searchExperimentsData); + // Extract out any associated app event from meta + const appEventSearchResult = appEventSearchResultFromData(objectGraph, resultData, lockup, standardLockupOptions, personalizationDataContainer, metricsOptions); + if (serverData.isDefinedNonNull(appEventSearchResult)) { + searchResult = appEventSearchResult; + searchResult.condensedBehavior = "never"; + } + else { + // If no app event in meta, fallback to a regular mixed media lockup + searchResult = new models.AppSearchResult(lockup); + } + } + break; + } + if (serverData.isDefinedNonNull(searchResult) && serverData.isNull(searchResult.condensedBehavior)) { + searchResult.condensedBehavior = condensedBehavior; + } + return searchResult; + }); +} +/** + * Indicates whether a search result will display an app event + * @param objectGraph The object graph + * @param searchResultData The data for the search result + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app states that is used to determine if the app has been installed by the user in the past + * @param metricsOptions Metrics options for built lockups + * @param personalizationDataContainer Personalization criteria used for sorting and filtering app events + * @returns a boolean result + */ +export function searchResultWillUseAppEventDisplay(objectGraph, searchResultData, installStates, appStates, metricsOptions, personalizationDataContainer) { + const appEventEligibleToDisplay = searchResultIsEligibleToDisplayAppEvent(objectGraph, searchResultData); + if (!appEventEligibleToDisplay) { + return false; + } + const { dataItems } = selectedAppEventDataItems(objectGraph, searchResultData, personalizationDataContainer); + let appEvent; + for (const appEventDataItem of dataItems) { + appEvent = sanitizedAppEvent(objectGraph, appEventDataItem, searchResultData, metricsOptions, undefined, undefined); + if (serverData.isDefinedNonNull(appEvent)) { + break; + } + } + const appIsInstalled = serverData.isDefinedNonNull(installStates) && serverData.isDefinedNonNull(installStates[searchResultData.id]) + ? installStates[searchResultData.id] + : false; + const appHasBeenInstalled = serverData.isDefinedNonNull(appStates) && serverData.isDefinedNonNull(appStates[searchResultData.id]) + ? ["downloadable"].includes(appStates[searchResultData.id]) + : false; + // App Events are only displayed in native when the user has installed the app previously and there is a valid app event + return (appIsInstalled || appHasBeenInstalled) && serverData.isDefinedNonNull(appEvent); +} +/** + * Derive the condensed behavior from a search result + * @param objectGraph The global object graph instance + * @param resultData The data blob for this specific search result + * @param pageSearchExperimentsData The page level search experiments data object + */ +function condensedBehaviorForData(objectGraph, resultData, pageSearchExperimentsData) { + var _a, _b; + /// Guard early against incompatible client devices + if (!canHaveCondensedBehaviorForClient(objectGraph)) { + return "never"; + } + const itemSearchExperimentData = resultData.meta; + const itemCondensedStyle = (_a = itemSearchExperimentData === null || itemSearchExperimentData === void 0 ? void 0 : itemSearchExperimentData.displayStyle) === null || _a === void 0 ? void 0 : _a.condensed; + if (serverData.isDefinedNonNull(itemCondensedStyle)) { + return condensedBehaviorFromStyle(objectGraph, itemCondensedStyle); + } + const pageCondensedStyle = (_b = pageSearchExperimentsData === null || pageSearchExperimentsData === void 0 ? void 0 : pageSearchExperimentsData.displayStyle) === null || _b === void 0 ? void 0 : _b.condensed; + if (serverData.isDefinedNonNull(pageCondensedStyle)) { + return condensedBehaviorFromStyle(objectGraph, pageCondensedStyle); + } + return defaultCondensedBehaviorForClient(objectGraph); +} +/** + * Gets the condensed behavior from a given condensed style, or a default behavior based on the client type + * @param objectGraph The global object graph instance + * @param condensedStyle The condensed style on the data model + * @returns The condensed behavior for the native search result view + */ +function condensedBehaviorFromStyle(objectGraph, condensedStyle) { + switch (condensedStyle) { + case "always": + return "always"; + case "never": + return "never"; + case "when-installed": + return "whenInstalled"; + default: + return defaultCondensedBehaviorForClient(objectGraph); + } +} +/** + * Determines the default condensed behavior based for a given client type + * @param objectGraph The global object graph instance + * @param condensedStyle The condensed style on the data model + * @returns The condensed behavior for the native search result view + */ +function defaultCondensedBehaviorForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return "whenInstalled"; + default: + return "never"; + } +} +/** + * Determines if the current client type supports condensed behavior + * @param objectGraph The global object graph instance + * @returns Whether condensed behavior is allowed + */ +function canHaveCondensedBehaviorForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return true; + default: + return false; + } +} +/** + * Modifies the editor's choice badge based on whether the search experiment overrides the metadataPrecendence + * for the badge + * @param objectGraph App Store ObjectGraph + * @param lockup The lockup we need to modify for the experiment + * @param searchExperimentData The experiment data to pull the metadata info from + */ +function modifyMetadataBadgeForSearchExperiment(objectGraph, lockup, searchExperimentData) { + const shouldShowEditorsChoice = metadataPrecedenceTypePreceedsType(objectGraph, searchExperimentData, "editorialBadgeInfo", "userRating"); + if (serverData.isDefinedNonNull(shouldShowEditorsChoice)) { + lockup.isEditorsChoice = lockup.isEditorsChoice && shouldShowEditorsChoice; + } +} +/** + * Determines whether the first metadata type should precede the second type for the given experiment data + * @param objectGraph App Store ObjectGraph + * @param experimentData The experiment data to pull the metadata info from + * @param firstType Does this precede the second type in the experiment order + * @param secondType Does this succeed the second type in the experiment order + * @returns whether the first type precedes the second type + */ +function metadataPrecedenceTypePreceedsType(objectGraph, experimentData, firstType, secondType) { + var _a; + if (serverData.isNull(experimentData) || !objectGraph.client.isPhone) { + return null; + } + const order = (_a = experimentData === null || experimentData === void 0 ? void 0 : experimentData.displayStyle) === null || _a === void 0 ? void 0 : _a.metadataPrecedenceOrder; + if (!serverData.isDefinedNonNullNonEmpty(order)) { + return null; + } + const firstIndex = order.indexOf(firstType); + const secondIndex = order.indexOf(secondType); + if (firstIndex === -1 && secondIndex === -1) { + return null; + } + if (firstIndex === -1) { + return false; + } + if (secondIndex === -1) { + return true; + } + return firstIndex < secondIndex; +} +function editorialSearchResultFromData(objectGraph, resultData, metricsOptions, resultCondensedBehavior) { + return validation.context("editorialSearchResultFromData", () => { + let searchResult; + const title = mediaAttributes.attributeAsString(resultData, "name"); + const resultType = resultData.type; + switch (resultType) { + case "groupings": { + const editorialSearchResult = new models.EditorialSearchResult(title); + const collectionAdamIds = mediaAttributes.attributeAsArrayOrEmpty(resultData, "contentIds"); + if (serverData.isDefinedNonNullNonEmpty(collectionAdamIds)) { + editorialSearchResult.collectionAdamIds = collectionAdamIds; + } + else { + const iconArtwork = content.artworkFromApiArtwork(objectGraph, mediaAttributes.attributeAsDictionary(resultData, "artwork"), { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + allowingTransparency: true, + }); + editorialSearchResult.iconArtwork = iconArtwork; + } + editorialSearchResult.type = "category"; + searchResult = editorialSearchResult; + break; + } + case "rooms": + case "multirooms": { + const editorialSearchResult = new models.EditorialSearchResult(title); + editorialSearchResult.artwork = content.artworkFromApiArtwork(objectGraph, mediaAttributes.attributeAsDictionary(resultData, "artwork"), { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + cropCode: "sr", + }); + editorialSearchResult.collectionAdamIds = mediaAttributes.attributeAsArrayOrEmpty(resultData, "contentIds"); + editorialSearchResult.type = "collection"; + searchResult = editorialSearchResult; + break; + } + case "editorial-items": { + if (objectGraph.bag.searchFilterEditorialItemIds.has(resultData.id)) { + return null; + } + // Bridge objects to Today builder. + const todayParseContext = new TodayParseContext(metricsOptions.pageInformation, metricsOptions.locationTracker); + const shouldResultBeCondensed = resultCondensedBehavior === "always"; + const editorialSearchResult = editorialSearchResultFromTodayCardData(objectGraph, resultData, todayParseContext, shouldResultBeCondensed); + searchResult = editorialSearchResult; + break; + } + case "developers": { + const editorialSearchResult = new models.EditorialSearchResult(title); + editorialSearchResult.artwork = content.artworkFromApiArtwork(objectGraph, mediaAttributes.attributeAsDictionary(resultData, "editorialArtwork.bannerUber"), { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + cropCode: "sr", + }); + editorialSearchResult.type = "developer"; + if (isSome(editorialSearchResult.artwork)) { + searchResult = editorialSearchResult; + } + else { + let topApps = mediaRelationship.relationshipCollection(resultData, "top-apps"); + topApps = topApps.filter((topApp) => { + return !filtering.shouldFilter(objectGraph, topApp, 76532 /* filtering.Filter.DeveloperPage */); + }); + const topAppIds = []; + const topAppArtwork = []; + topApps.forEach((app) => { + topAppIds.push(app.id); + const appIcon = content.iconFromData(objectGraph, app, { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + }); + if (serverData.isDefinedNonNull(appIcon)) { + topAppArtwork.push(appIcon); + } + }); + editorialSearchResult.collectionAdamIds = topAppIds; + editorialSearchResult.collectionAppIcons = topAppArtwork; + // On visionOS, the developer result falls back to a lockup collection if no art is available. + if (objectGraph.client.isVision) { + const lockupCollection = searchLockupCollection.lockupCollectionFromResultData(objectGraph, resultData, metricsOptions); + searchResult = lockupCollection; + } + else { + searchResult = editorialSearchResult; + } + } + break; + } + default: + break; + } + if (serverData.isNull(searchResult)) { + return null; + } + if (searchResult instanceof EditorialSearchResult) { + if (searchResult.collectionAdamIds != null && searchResult.collectionAdamIds.length) { + const lockupCount = searchResult.collectionAdamIds.length; + if (lockupCount <= 5) { + searchResult.artworkGridType = "extraLarge"; + } + else if (lockupCount <= 8) { + searchResult.artworkGridType = "large"; + } + else if (lockupCount <= 16) { + searchResult.artworkGridType = "mixed"; + } + else { + searchResult.artworkGridType = "small"; + } + } + if (objectGraph.client.isVision) { + let badgeText; + let badgeArtwork = createArtworkForResource(objectGraph, "systemimage://appstore"); + switch (searchResult.type) { + case "developer": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_DEVELOPER_TITLE_CASE"); + badgeArtwork = createArtworkForResource(objectGraph, "systemimage://person.crop.square"); + break; + case "category": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_CATEGORY_TITLE_CASE"); + break; + case "collection": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_COLLECTION_TITLE_CASE"); + break; + case "story": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_STORY_TITLE_CASE"); + break; + default: + break; + } + searchResult.badgeText = badgeText; + searchResult.badgeArtwork = badgeArtwork; + } + } + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, resultData, searchResult.title, metricsOptions); + searchResult.clickAction = lockups.actionFromData(objectGraph, resultData, impressionOptions, null); + metricsHelpersImpressions.addImpressionFields(objectGraph, searchResult, impressionOptions); + return searchResult; + }); +} +function editorialSearchResultFromTodayCardData(objectGraph, cardData, todayParseContext, shouldResultBeCondensed) { + // Card configuration to use for building TodayCards that will be converted into editorial search results. + const cardConfig = defaultTodayCardConfiguration(objectGraph); + cardConfig.isSearchContext = true; + if (!objectGraph.client.isVision) { + cardConfig.prevailingCropCodes = + shouldResultBeCondensed && objectGraph.client.isPhone + ? { defaultCrop: "DMGE.AppST01" } + : { defaultCrop: "fo" }; + } + // If we are on visionOS, drop any EIs that have a not-compatible app as the primary content. + if (objectGraph.client.isVision) { + const primaryContent = content.primaryContentForData(objectGraph, cardData); + if (isSome(primaryContent)) { + const runnableOnDevice = content.runnableOnDeviceWithData(objectGraph, primaryContent, objectGraph.client.deviceType, objectGraph.appleSilicon.isSupportEnabled); + if (!runnableOnDevice) { + return null; + } + } + } + const todayCard = todayCardFromData(objectGraph, cardData, cardConfig, todayParseContext); + if (!todayCard) { + return null; + } + const editorialSearchResult = new models.EditorialSearchResult(todayCard.title); + editorialSearchResult.type = "story"; + editorialSearchResult.clickAction = todayCard.clickAction; + let collectionLockups = null; + if (todayCard.media) { + switch (todayCard.media.kind) { + case "brandedSingleApp": + const mediaSingleApp = todayCard.media; + editorialSearchResult.artwork = mediaSingleApp.artworks[0]; + if (serverData.isNull(editorialSearchResult.artwork)) { + editorialSearchResult.iconArtwork = mediaSingleApp.icon; + } + const cardDisplayStyle = mediaAttributes.attributeAsString(cardData, "cardDisplayStyle"); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + const relatedContentData = mediaRelationship.relationshipData(objectGraph, cardData, "card-contents"); + if (relatedContentData) { + editorialSearchResult.title = + mediaAttributes.attributeAsString(relatedContentData, "name") || + editorialSearchResult.title; + } + break; + default: + break; + } + break; + case "list": + const mediaList = todayCard.media; + collectionLockups = mediaList.lockups; + break; + case "river": + const mediaRiver = todayCard.media; + collectionLockups = mediaRiver.lockups; + break; + case "artwork": + const mediaArtwork = todayCard.media; + editorialSearchResult.artwork = mediaArtwork.artworks[0]; + break; + case "grid": + const mediaGrid = todayCard.media; + collectionLockups = mediaGrid.lockups; + break; + case "multiApp": + const multiApp = todayCard.media; + collectionLockups = multiApp.lockups; + break; + case "video": + const mediaVideo = todayCard.media; + editorialSearchResult.artwork = mediaVideo.videos[0].preview; + editorialSearchResult.video = mediaVideo.videos[0]; + if (todayCard.overlay instanceof models.TodayCardThreeLineOverlay) { + const overlay = todayCard.overlay; + editorialSearchResult.title = overlay.title; + editorialSearchResult.subtitle = overlay.description; + } + else { + editorialSearchResult.subtitle = mediaVideo.description; + } + break; + case "appEvent": + const media = todayCard.media; + editorialSearchResult.artwork = media.artworks[0]; + editorialSearchResult.appEventFormattedDates = media.formattedDates; + editorialSearchResult.subtitle = todayCard.inlineDescription; + editorialSearchResult.tintColor = media.tintColor; + editorialSearchResult.type = "appEventStory"; + if (serverData.isDefinedNonNull(todayCard.style)) { + switch (todayCard.style) { + case "light": + case "white": + editorialSearchResult.mediaOverlayStyle = "light"; + break; + case "dark": + editorialSearchResult.mediaOverlayStyle = "dark"; + break; + default: + errors.unreachable(todayCard.style); + break; + } + } + break; + default: + break; + } + } + if (todayCard.overlay) { + switch (todayCard.overlay.kind) { + case "lockup": + const cardOverlayLockup = todayCard.overlay; + if (!editorialSearchResult.artwork || objectGraph.client.isVision) { + collectionLockups = [cardOverlayLockup.lockup]; + } + break; + case "lockupList": + const cardOverlayList = todayCard.overlay; + collectionLockups = cardOverlayList.lockups; + break; + case "paragraph": + const cardOverlayParagraph = todayCard.overlay; + editorialSearchResult.subtitle = cardOverlayParagraph.paragraph.text; + break; + default: + break; + } + } + if (serverData.isDefinedNonNull(collectionLockups)) { + editorialSearchResult.collectionAdamIds = []; + editorialSearchResult.collectionAppIcons = []; + for (const lockup of collectionLockups) { + editorialSearchResult.collectionAdamIds.push(lockup.adamId); + editorialSearchResult.collectionAppIcons.push(lockup.icon); + } + if (collectionLockups.length === 1) { + editorialSearchResult.lockup = collectionLockups[0]; + } + } + const editorialClientParams = extractEditorialClientParams(objectGraph, cardData); + editorialSearchResult.editorialDisplayOptions = editorialDisplayOptionsFromClientParams(editorialClientParams); + /** + * Editorial tagline + */ + const storyTagline = common.editorialSearchResultTagline(objectGraph, cardData); + if ((storyTagline === null || storyTagline === void 0 ? void 0 : storyTagline.length) > 0 && storyTagline !== editorialSearchResult.title) { + editorialSearchResult.tagline = storyTagline; + } + const heroMedia = todayCard.heroMedia; + if (serverData.isDefinedNonNullNonEmpty(heroMedia)) { + if (serverData.isDefinedNonNullNonEmpty(heroMedia.artworks[0])) { + editorialSearchResult.artwork = heroMedia.artworks[0]; + editorialSearchResult.artwork.crop = "em"; + } + else if (serverData.isDefinedNonNullNonEmpty(heroMedia.videos[0])) { + editorialSearchResult.video = heroMedia.videos[0]; + } + } + if (editorialSearchResult.video) { + editorialSearchResult.video.canPlayFullScreen = false; + editorialSearchResult.video.playbackControls = {}; + } + if (!editorialSearchResult.collectionAdamIds && + !editorialSearchResult.artwork && + !editorialSearchResult.iconArtwork) { + return null; + } + return editorialSearchResult; +} +/// Gets the client identifier (or null) that should be used when building lockups for a given search entity. +function clientIdentifierOverrideForSearchEntity(objectGraph, searchEntity) { + return searchEntity === "watch" ? client.watchIdentifier : null; +} +/** + * Indicates whether a search result meets basic sanity requirements to display app events. This does not include business rules for massaging a valid app event. For that, see `sanitizedAppEvent` function + * @param objectGraph The object graph + * @param searchResultData The data for the search result + * @returns a boolean result + */ +function searchResultIsEligibleToDisplayAppEvent(objectGraph, searchResultData) { + if (!appPromotionsCommon.appEventsAreEnabled(objectGraph) || serverData.isNull(searchResultData.meta)) { + return false; + } + // The first organic can't use CPP data from the ad if it has an app event that will be displayed. + // In this case, it should continue to use DPP assets + const appEventDataItems = serverData.asArrayOrEmpty(searchResultData.meta, "associations.app-events.data"); + const hasInAppEvents = appEventDataItems.length > 0; + // App events can be displayed on most search result types, except for these ones + const typesIneligibleToDisplayAppEvent = [ + "rooms", + "multirooms", + "developers", + "editorial-items", + "groupings", + "in-apps", + "app-bundles", + ]; + const typeIsEligibleToDisplayAppEvent = !typesIneligibleToDisplayAppEvent.includes(searchResultData.type); + return typeIsEligibleToDisplayAppEvent && hasInAppEvents; +} +/** + * Sorts and filters app event for personalization, if applicable. May filter app events, and may select just the best one + * @param objectGraph The object graph + * @param resultData The data for the search result + * @param personalizationDataContainer Personalization criteria used for sorting and filtering app events + * @returns an object representing the personalized app events for a user, along with the personalization result + */ +function selectedAppEventDataItems(objectGraph, resultData, personalizationDataContainer) { + const alwaysShowAppEvent = serverData.asBooleanOrFalse(resultData.meta, "associations.app-events.attributes.forceAppEvent"); + const appEventDataItems = serverData.asArrayOrEmpty(resultData.meta, "associations.app-events.data"); + if (alwaysShowAppEvent) { + // In this scenario, there should always be only one event to choose from, + // and we don't want to do any personalization. + return { dataItems: [appEventDataItems[0]] }; + } + const personalizedDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "search", appEventDataItems, false, personalizationDataContainer, false, undefined, resultData.id); + const personalizedDataItems = personalizedDataResult.personalizedData; + if (personalizedDataItems.length <= 0) { + return { dataItems: [] }; + } + return { dataItems: personalizedDataItems, personalizationData: personalizedDataResult }; +} +/** + * Sanitizes a raw app event metadata into an AppEvent model. If the event is not hydratable or has not started, this returns null. + * @param objectGraph The object graph + * @param appEventDataItem The app event that needs to be processed + * @param resultData The data for the search result + * @param baseMetricsOptions Metrics options for built lockups + * @param offerEnvironment Offer environment for the lockup. Typically comes from lockup options + * @param offerStyle Offer style for the lockup. Typically comes from lockup options + * @returns the processed app event with all business rules applied, or null if the app event was disqualified + */ +function sanitizedAppEvent(objectGraph, appEventDataItem, resultData, baseMetricsOptions, offerEnvironment, offerStyle) { + const metricsOptions = { + ...baseMetricsOptions, + targetType: "eventModule", + }; + const appEventOrDate = appEvents.appEventOrPromotionStartDateFromData(objectGraph, appEventDataItem, resultData, false, true, offerEnvironment, offerStyle, false, metricsOptions, false, true, null, false, false); + // Ignore a future AppEvent promotionStartDate here. + if (serverData.isNull(appEventOrDate) || appEventOrDate instanceof Date) { + return null; + } + else { + return appEventOrDate; + } +} +/** + * Creates an app event search result from the data and associated lockup, if an app event exists in the metadata + * @param resultData The data blob + * @param lockup The associated mixed media lockup + * @param standardLockupOptions: The standard lockup options for search results + * @param personalizationDataContainer The data container to use for personalizing the selected app event + * @param baseMetricsOptions Base metrics options for this result + * @returns {AppEventSearchResult} The app event search result, or null + */ +function appEventSearchResultFromData(objectGraph, resultData, lockup, standardLockupOptions, personalizationDataContainer, baseMetricsOptions) { + return validation.context("appEventSearchResultFromData", () => { + const appEventEligibleToDisplay = searchResultIsEligibleToDisplayAppEvent(objectGraph, resultData); + if (!appEventEligibleToDisplay) { + return null; + } + const { dataItems, personalizationData } = selectedAppEventDataItems(objectGraph, resultData, personalizationDataContainer); + let appEvent; + let parentAppData; + for (const appEventDataItem of dataItems) { + const appEventOrNull = sanitizedAppEvent(objectGraph, appEventDataItem, resultData, baseMetricsOptions, standardLockupOptions.offerEnvironment, standardLockupOptions.offerStyle); + if (serverData.isDefinedNonNull(appEventOrNull)) { + appEvent = appEventOrNull; + parentAppData = resultData !== null && resultData !== void 0 ? resultData : mediaRelationship.relationshipData(objectGraph, appEventDataItem, "app"); + break; + } + } + if (serverData.isNull(appEvent)) { + return null; + } + const alwaysShowAppEvent = serverData.asBooleanOrFalse(resultData.meta, "associations.app-events.attributes.forceAppEvent"); + const searchResult = new models.AppEventSearchResult(); + searchResult.lockup = lockup; + searchResult.appEvent = appEvent; + searchResult.alwaysShowAppEvent = alwaysShowAppEvent; + searchResult.clickAction = lockup.clickAction; + const recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(null, personalizationData === null || personalizationData === void 0 ? void 0 : personalizationData.processingType, null); + const impressionOptions = { + ...baseMetricsOptions, + id: appEvent.appEventId, + kind: "inAppEvent", + targetType: "eventModule", + title: appEvent.title, + softwareType: null, + recoMetricsData: recoMetricsData, + }; + if (serverData.isDefinedNonNull(parentAppData)) { + impressionOptions.relatedSubjectIds = [parentAppData.id]; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchResult, impressionOptions); + return searchResult; + }); +} +//# sourceMappingURL=search-results.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js new file mode 100644 index 0000000..03df66d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js @@ -0,0 +1,53 @@ +import * as models from "../../../api/models"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { asInterface } from "../../../foundation/json-parsing/server-data"; +import { createMetricsOptionsForGenericSearchPageShelf } from "../../metrics/helpers/search/search-shelves"; +import { isNothing } from "@jet/environment"; +// The types of search pages shelves can render in +export var SearchPageType; +(function (SearchPageType) { + SearchPageType[SearchPageType["Landing"] = 0] = "Landing"; + SearchPageType[SearchPageType["Results"] = 1] = "Results"; + SearchPageType[SearchPageType["ChartsAndCategories"] = 2] = "ChartsAndCategories"; + SearchPageType[SearchPageType["Focus"] = 3] = "Focus"; +})(SearchPageType || (SearchPageType = {})); +/** + * Collections the shelf's attributes + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @returns The attributes for the shelf + */ +export function shelfAttributesFromData(objectGraph, data, shelfKind = undefined, pageKind = models.SearchPageKind.Default) { + var _a, _b, _c; + const title = (_a = mediaAttributes.attributeAsString(data, "title")) !== null && _a !== void 0 ? _a : undefined; + let displayStyle = (_b = mediaAttributes.attributeAsInterface(data, "displayStyle")) !== null && _b !== void 0 ? _b : undefined; + /// If we are on the see-all page, we currently don't get back a displayStyle object, so we need to manually provide one + /// for the tile brick type as see-all is hard-coded for that density everywhere else (natively) + if (isNothing(displayStyle) && pageKind === models.SearchPageKind.CategoriesAndCharts) { + displayStyle = { + layoutDensity: models.GenericSearchPageShelfDisplayStyleDensity.Density1, + layout: undefined, + layoutSize: undefined, + }; + } + const itemDisplayStyleAttributes = mediaAttributes.attributeAsDictionary(data, "itemDisplayStyle"); + const itemDisplayStyle = asInterface(itemDisplayStyleAttributes); + const hasSeeAll = mediaAttributes.attributeAsBooleanOrFalse(data, "hasSeeAll"); + const displayCount = (_c = mediaAttributes.attributeAsNumber(data, "displayCount")) !== null && _c !== void 0 ? _c : undefined; + const seeAllURL = hasSeeAll ? data.href : undefined; + return new models.SearchShelfAttributes(data.id, title, displayStyle, displayCount, hasSeeAll, seeAllURL, itemDisplayStyle, shelfKind); +} +/** + * Creates a base shelf context for the search shelf + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @param shelfAttributes The shelf's attributes + * @param searchPageContext The context for the page containing the shelf + * @returns A standard shelf context for the shelf + */ +export function baseShelfContext(objectGraph, data, shelfAttributes, searchPageContext) { + return { + metricsOptions: createMetricsOptionsForGenericSearchPageShelf(objectGraph, data, shelfAttributes, searchPageContext), + }; +} +//# sourceMappingURL=search-shelves.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js b/node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js new file mode 100644 index 0000000..9b3aa26 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js @@ -0,0 +1,71 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isNothing, isSome } from "@jet/environment"; +import { asDictionary } from "@apple-media-services/media-api"; +import { artworkFromApiArtwork } from "../content/content"; +import { asString } from "../../foundation/json-parsing/server-data"; +import { Video } from "../../api/models"; +export function getSelectedCustomCreativeId(data) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const customCreativeMeta = asDictionary(data, "meta.creativeAttributes"); + const selectedCustomCreativeId = asString(customCreativeMeta, "creatives.0"); + return selectedCustomCreativeId; + } + return null; +} +/** + * Create a custom creative artwork from the data. + * @param objectGraph Object graph + * @param data that we use to populate custom creative artwork + * @param customCreativeData where we store custom creative data. + * @param cropCode for the artwork + */ +export function customCreativeArtworkFromData(objectGraph, data, customCreativeData, cropCode) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const selectedCustomCreativeId = getSelectedCustomCreativeId(data); + if (isNothing(customCreativeData) || isNothing(selectedCustomCreativeId)) { + return null; + } + const creativeAssetArtwork = artworkFromApiArtwork(objectGraph, serverData.asDictionary(customCreativeData, `${selectedCustomCreativeId}.adCreativeArtwork`), { + allowingTransparency: false, + useCase: 4 /* ArtworkUseCase.LockupScreenshots */, + }); + if (isSome(cropCode) && isSome(creativeAssetArtwork)) { + creativeAssetArtwork.crop = cropCode; + } + return creativeAssetArtwork; + } + else { + return null; + } +} +/** + * Create a custom creative video from the data. + * @param objectGraph Object graph + * @param data that we use to populate custom creative video and preview. + * @param videoConfiguration for the custom creative video. + * @param customCreativeData where we store custom creative data. + * @param cropCode for the artwork + */ +export function customCreativeVideoFromData(objectGraph, data, customCreativeData, videoConfiguration, cropCode) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const selectedCustomCreativeId = getSelectedCustomCreativeId(data); + if (isNothing(selectedCustomCreativeId)) { + return null; + } + const creativeVideoData = serverData.asDictionary(customCreativeData, `${selectedCustomCreativeId}.adCreativeVideo`); + const videoUrl = serverData.asString(creativeVideoData, "video"); + const preview = artworkFromApiArtwork(objectGraph, serverData.asDictionary(creativeVideoData, "previewFrame"), { + allowingTransparency: false, + useCase: 4 /* ArtworkUseCase.LockupScreenshots */, + }); + if (isNothing(videoUrl) || isNothing(preview)) { + return null; + } + if (isSome(cropCode)) { + preview.crop = cropCode; + } + return new Video(videoUrl, preview, videoConfiguration); + } + return null; +} +//# sourceMappingURL=custom-creative.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js new file mode 100644 index 0000000..83cdf90 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js @@ -0,0 +1,132 @@ +/** + * Builds metrics entities for Guided Search + */ +import { ImpressionMetrics, } from "../../../api/models"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { createMetricsClickData, createMetricsSearchData } from "../../metrics/builder"; +import { createBasicLocation, currentPosition } from "../../metrics/helpers/location"; +// region Action Metrics +/** + * Add click + search metrics data to guided search token action + * @param objectGraph The dependency graph for the App Store. + * @param action Action to instrument, e.g. toggle or rewrite. + * @param targetToken The token display label, which may be toggleable. + * @param searchTerm The search term for which this token action was returned for. + * @param metricsOptions The metrics option use for adding instrumentation to token toggle. + */ +export function addEventsToGuidedSearchTokenAction(objectGraph, action, targetToken, searchTerm, metricsOptions) { + // Click Fields + const actionType = "guidedSearch"; + const targetType = "guidedLabel"; + const clickFields = { + actionType: actionType, + location: createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: metricsOptions.locationTracker, + targetType: targetType, + }, targetToken), + searchTerm: searchTerm, + }; + // Search Fields + const searchFields = { + targetId: targetToken, + }; + // SSS: Clicks must be before Search + const clickData = createMetricsClickData(objectGraph, targetToken, targetType, clickFields, ["guidedSearch"]); + action.actionMetrics.addMetricsData(clickData); + const searchData = createMetricsSearchData(objectGraph, searchTerm, targetType, actionType, null, searchFields, [ + "guidedSearch", + ]); + action.actionMetrics.addMetricsData(searchData); +} +export function addEventsToGuidedSearchTokenEntityChangeAction(objectGraph, action, searchTerm, targetEntity, metricsOptions) { + // Click Fields + const actionType = "hint"; + const targetType = "hintsEntity"; + const clickFields = { + actionType: actionType, + location: createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: metricsOptions.locationTracker, + targetType: targetType, + }, targetEntity), + searchTerm: searchTerm, + }; + // Search Fields + const searchFields = { + targetId: targetEntity, + }; + // SSS: Clicks must be before Search + const clickData = createMetricsClickData(objectGraph, targetEntity, targetType, clickFields); + action.actionMetrics.addMetricsData(clickData); + const searchData = createMetricsSearchData(objectGraph, searchTerm, targetType, actionType, null, searchFields); + action.actionMetrics.addMetricsData(searchData); +} +// endregion +// region Page Metrics +/** + * Build a `MetricsFields` for guided search fields used for page and impression events. + * This is attached onto `MetricsPageInformation` for consumption during page and impression field generation. + */ +export function guidedSearchPageInformationFields(objectGraph, request, guidedSearchData) { + const fields = {}; + // field for what tokens were selected on page. + if (isDefinedNonNullNonEmpty(request.guidedSearchTokens)) { + fields["searchSelectedGuidedFacets"] = request.guidedSearchTokens; + } + // field for what the server computed final query was + if (guidedSearchData && guidedSearchData.finalTerm) { + fields["searchGuidedFinalQuery"] = guidedSearchData.finalTerm; + } + if (isNullOrEmpty(fields)) { + return undefined; + } + return fields; +} +// endregion +// region Impression Metrics +export function addImpressionMetricsToGuidedSearchToken(objectGraph, token, type, metricsOptions) { + const tokenIndex = currentPosition(metricsOptions.locationTracker); + /** + * ID for parent is index. This should be explicit but is not in how we do metrics in JS today (sequential IDs are added later) + */ + const impressionFields = { + impressionIndex: tokenIndex, + id: tokenIndex.toString(), + idType: "sequential", + name: token.displayName, + impressionType: type, + parentId: "search-revisions", + }; + token.impressionMetrics = new ImpressionMetrics(impressionFields); +} +/** + * Add imaginary parent container for search results + * Guided Search: Tech Debt: Trim parent impression tech debt off SearchResults (JS Compatibility Breaking) + */ +export function addSearchResultParentImpressionMetrics(objectGraph, searchResultsPage, metricsOptions) { + const parentIndex = currentPosition(metricsOptions.locationTracker); + searchResultsPage.resultsParentImpressionMetrics = new ImpressionMetrics({ + impressionIndex: parentIndex, + impressionType: "SearchResults", + idType: "relationship", + id: "search-results", + name: "Search Results", + }); +} +/** + * Add an imaginary parent container for guided search tokens + * Guided Search: Tech Debt: Trim parent impression tech debt off SearchResults (JS Compatibility Breaking) + */ +export function addGuidedSearchParentImpressionMetrics(objectGraph, searchResultsPage, metricsOptions) { + const parentIndex = currentPosition(metricsOptions.locationTracker); + searchResultsPage.guidedSearchTokensParentImpressionMetrics = new ImpressionMetrics({ + impressionIndex: parentIndex, + impressionType: "SearchRevisions", + idType: "relationship", + id: "search-revisions", + name: "Search Revisions", + }); +} +// endregion +//# sourceMappingURL=guided-search-metrics.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js new file mode 100644 index 0000000..3be4ae9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js @@ -0,0 +1,105 @@ +/** + * Model Builder for Guided Search (STUB) + */ +import { GuidedSearchQuery, GuidedSearchToken, SearchAction, searchEntitySystemImage, } from "../../../api/models"; +import { GuidedSearchTokenToggleAction, SearchEntityChangeAction, } from "../../../api/models/search/guided-search-actions"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { unreachable } from "../../../foundation/util/errors"; +import { addEventsToGuidedSearchTokenEntityChangeAction, addEventsToGuidedSearchTokenAction, addImpressionMetricsToGuidedSearchToken, } from "./guided-search-metrics"; +// region exports +/** + * Create a Guided search token from facet data. + * @param objectGraph The App Store object graph. + * @param selectionBehavior The behavior for click action, e.g. query rewrite versus toggling tokens. + * @param requestDescriptor The request descriptor for search request that returned this data. + * @param facetData The facet data to build with. + * @param metricsOptions The metrics options. + */ +export function createGuidedSearchToken(objectGraph, selectionBehavior, requestDescriptor, facetData, metricsOptions) { + if (isNullOrEmpty(facetData)) { + return null; + } + // Create click and search metrics for token action. + const origin = "guidedToken"; + const searchTerm = requestDescriptor.term; + const targetToken = facetData.displayLabel; + const clickAction = selectionBehavior === "rewrite" + ? new SearchAction(targetToken, facetData.finalTerm, null, origin) + : new GuidedSearchTokenToggleAction(targetToken, origin); + addEventsToGuidedSearchTokenAction(objectGraph, clickAction, targetToken, searchTerm, metricsOptions); + // Create the token with associated click action and metrics.. + const token = new GuidedSearchToken(targetToken, facetData.isSelected, undefined, targetToken, clickAction); + addImpressionMetricsToGuidedSearchToken(objectGraph, token, "guidedLabel", metricsOptions); + return token; +} +/** + * Create the guided search queries from facet data. + * These are stored across a guided search session so client can send down precomputed combinations of search term and guided search facets as an optimization. + * @param requestDescriptor The request descriptor for search request that returned this data. + * @param facetData The data to generate `GuidedSearchQuery` for. + */ +export function createGuidedSearchQueries(objectGraph, requestDescriptor, facetData) { + var _a; + if (isNullOrEmpty(facetData)) { + return null; + } + // request parameters that returned this `facetData` + const requestTerm = requestDescriptor.term; + const requestFacets = (_a = requestDescriptor.guidedSearchTokens) !== null && _a !== void 0 ? _a : []; + const queries = []; + for (const data of facetData) { + /** + * For each facet data, project the facet selection if that facet was selected / deselected w.r.t. request's facets + */ + const facetValue = data.displayLabel; + const lookaheadFacets = Array.from(requestFacets); + if (data.isSelected) { + const facetIndex = lookaheadFacets.indexOf(facetValue); + if (facetIndex !== -1) { + lookaheadFacets.splice(facetIndex, 1); + } + } + else { + lookaheadFacets.push(facetValue); + } + const query = new GuidedSearchQuery(requestTerm, lookaheadFacets, data.finalTerm); + queries.push(query); + } + return queries; +} +/** + * Create an action for **reversing** a selected entity hint. This is so user can reverse an selected entity filter for search. + * e.g. If user tapped "Search for Games in Apple Watch Apps", user can deselect the "Apple Watch Apps" entity filter from the Guided Search UI + */ +export function createGuidedSearchTokenClearingEntityFilter(objectGraph, requestDescriptor, metricsOptions) { + var _a; + const selectedEntityFilter = requestDescriptor.searchEntity; + if (!selectedEntityFilter) { + return null; + } + const deselectEntityAction = new SearchEntityChangeAction(null, "guidedToken"); + addEventsToGuidedSearchTokenEntityChangeAction(objectGraph, deselectEntityAction, requestDescriptor.term, selectedEntityFilter, metricsOptions); + let displayText; + switch (selectedEntityFilter) { + case "arcade": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_ARCADE"); + break; + case "developer": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_DEVELOPERS"); + break; + case "story": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_STORIES"); + break; + case "watch": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_APPLEWATCH"); + break; + default: + unreachable(selectedEntityFilter); + break; + } + const token = new GuidedSearchToken(displayText, true, (_a = searchEntitySystemImage(selectedEntityFilter)) !== null && _a !== void 0 ? _a : "magnifyingglass", displayText, deselectEntityAction); + addImpressionMetricsToGuidedSearchToken(objectGraph, token, "hintsEntity", metricsOptions); + return token; +} +// endregion +//# sourceMappingURL=guided-search.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js new file mode 100644 index 0000000..6baf947 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js @@ -0,0 +1,84 @@ +/** + * Manages storing Cohort IDs for Search Landing Page (SLP) + * + * # What is Cohort ID? + * Cohort IDs (a.k.a. cluster IDs) are used to bucket users into different categories, e.g. a gamer. + * This ID can be used to specify a SLP that is suited to that cateogry of users, e.g. a page featuring more games. + * + * # SLP Endpoint Constraints + * Today, SLPs are not personalized, and rely heavily on CDN caching. + * We cannot fire a single request to SLP endpoint to have it adapt to a user's cohort based on cookies, etc. + * + * As a workaround we: + * 1. Store the cohort ID for user if we ever load a personalized endpoint, e.g. Today. + * 2. Send stored cohort ID as a query param on the SLP endpoint, if we have any. + */ +"use strict"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +// region exports +/** + * Should be called whenever we recieve a MAPI response from a personalized endpoint. + * Persists the cohort id that may be present in given MAPI respose for specified user. + * @param dsid The user's dsid. + * @param metaDataProviding MAPI response that may contain cohort id for current user. + */ +export function storeCohortIdForUserFromResponse(objectGraph, dsid, metaDataProviding) { + if (serverData.isNullOrEmpty(dsid)) { + return; + } + const cohortId = cohortIdFromResponse(metaDataProviding); + if (serverData.isNullOrEmpty(cohortId)) { + return; + } + setCohortIdForDSID(objectGraph, dsid, cohortId); +} +/** + * Return the stored cohort id for given dsid. + * @param dsid The DSID of user to fetch cohort id for. + */ +export function cohortIdForUser(objectGraph, dsid) { + if (serverData.isNullOrEmpty(dsid)) { + return null; + } + return getCohortIdForDSID(objectGraph, dsid); +} +/** + * Deletes cohort id for given user. For testing. + */ +export function deleteCohortIdForUser(objectGraph, dsid) { + setCohortIdForDSID(objectGraph, dsid, undefined); +} +// endregion +// region Internals +/** + * Given a top-level MAPI response `metaDataProviding`, returns the cohort ID, if any. + * @param metaDataProviding A MAPI Response that may contain a `meta.metrics` blob with `clusterId` + */ +function cohortIdFromResponse(metaDataProviding) { + return serverData.asString(metaDataProviding, "meta.metrics.clusterId"); +} +/** + * Converts a DSID into a dictionary key for storing cohort ID. + * @param dsid DSID to generate storage key for. + */ +function cohortIDStorageKeyForDSID(dsid) { + return dsid + "-cohort-id"; +} +/** + * Set the stored cohort id for given user (by dsid). + * @param dsid DSID to associate cohortId with + * @param cohortId The cohort id for user. + */ +function setCohortIdForDSID(objectGraph, dsid, cohortId) { + const cohortForDSIDKey = cohortIDStorageKeyForDSID(dsid); + objectGraph.storage.storeString(cohortForDSIDKey, cohortId); +} +/** + * Gets the stored cohort id for given user by dsid. + * @param dsid DSID to get cohort id for. + */ +function getCohortIdForDSID(objectGraph, dsid) { + const cohortForDSIDKey = cohortIDStorageKeyForDSID(dsid); + return objectGraph.storage.retrieveString(cohortForDSIDKey); +} +//# sourceMappingURL=search-landing-cohort.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js new file mode 100644 index 0000000..808384b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js @@ -0,0 +1,836 @@ +import { isNothing, isSome, unwrapOptional } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as filtering from "../../filtering"; +import * as adLockups from "../../lockups/ad-lockups"; +import * as lockups from "../../lockups/lockups"; +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 mediaRelationships from "../../../foundation/media/relationships"; +import { searchLandingPageAdShelfIdentifier, searchLandingPagePositionInfo } from "../../ads/on-device-ad-stitch"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import * as content from "../../content/content"; +import * as metricsHelpersPage from "../../metrics/helpers/page"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { createChartsCategoryShelf } from "../content/search-categories"; +import { MediumAdLockupWithScreenshotsBackground, } from "../../../api/models"; +import * as adStitch from "../../ads/ad-stitcher"; +import * as adIncidents from "../../ads/ad-incident-recorder"; +import * as searchShelves from "../content/search-shelves"; +import { CollectionShelfDisplayStyle } from "../../editorial-pages/editorial-page-types"; +import { buildBrick } from "../../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-brick-collection-shelf-builder"; +import { iconFromData } from "../../content/content"; +import * as searchHistoryShelf from "../shelves/search-history-shelf"; +import { adLogger } from "../search-ads"; +import { asString } from "../../../foundation/json-parsing/server-data"; +import { addImpressionsFieldsToAd } from "../../metrics/helpers/impressions"; +import { makeSearchResultsPageIntent } from "../../../api/intents/search-results-page-intent"; +import { getLocale } from "../../locale"; +import { actionFor } from "../../../foundation/runtime/action-provider"; +import * as impressionDemotion from "../../personalization/on-device-impression-demotion"; +import { applySearchAdMissedOpportunityToShelvesIfNeeded } from "../../ads/ad-common"; +// #region Shelf Creation +export function firstShelfMarkerMatchingUseCase(dataContainer, searchLandingPageContext, onDevicePersonalizationUseCase) { + const shelfData = dataContainer.data; + if (serverData.isNullOrEmpty(shelfData)) { + return null; + } + for (const dataItem of shelfData) { + if (serverData.isNullOrEmpty(dataItem)) { + continue; + } + /// Skip shelf if not valid for context page type. + const shelfMetadata = serverData.asDictionary(dataItem, "meta"); + const shelfPageType = pageTypeFromShelfMetaCategory(shelfMetadata === null || shelfMetadata === void 0 ? void 0 : shelfMetadata.category); + if (shelfPageType !== searchLandingPageContext.pageType) { + continue; + } + if (onDevicePersonalizationUseCase === + mediaAttributes.attributeAsString(dataItem, "onDevicePersonalizationUseCase")) { + return dataItem; + } + } + return null; +} +/** + * Inserts the shelf made from the data container into the grouping parse context + * @param objectGraph The App Store dependency graph + * @param dataContainer The response data + * @param searchLandingPageContext The context for the search page, e.g. landing or focus + */ +export function insertShelvesIntoSearchPageContext(objectGraph, dataContainer, searchLandingPageContext) { + var _a; + const shelfData = dataContainer.data; + if (serverData.isNullOrEmpty(shelfData)) { + return; + } + // index to compare adPositionInfo + let builtShelves = 0; + const supportsFocus = objectGraph.bag.mediaAPISearchFocusEnabled && isSome(searchLandingPageContext.pageType); + for (const dataItem of shelfData) { + if (serverData.isNullOrEmpty(dataItem)) { + continue; + } + /// Skip shelf if not valid for context page type. + if (supportsFocus) { + const shelfMetadata = serverData.asDictionary(dataItem, "meta"); + const shelfPageType = pageTypeFromShelfMetaCategory(shelfMetadata === null || shelfMetadata === void 0 ? void 0 : shelfMetadata.category); + if (isSome(shelfPageType) && shelfPageType !== searchLandingPageContext.pageType) { + continue; + } + } + const adMeta = dataContainer.meta || null; + const adUnitShelf = shelfFromAdStitcher(objectGraph, searchLandingPageContext, builtShelves, adMeta); + if (isSome(adUnitShelf)) { + searchLandingPageContext.shelves.push(adUnitShelf); + metricsHelpersLocation.nextPosition(searchLandingPageContext.metricsLocationTracker); + } + /// Get the necessary metadata for the shelf + const shelfKind = mediaAttributes.attributeAsString(dataItem, "contentKind"); + const shelfAttributes = searchShelves.shelfAttributesFromData(objectGraph, dataItem, shelfKind); + const shelfContext = searchLandingPageShelfContext(objectGraph, dataItem, shelfAttributes, searchLandingPageContext, shelfKind); + /// Push the shelf content location so each shelf has the correct parent and starting index + metricsHelpersLocation.pushContentLocation(objectGraph, shelfContext.metricsOptions, (_a = shelfAttributes.title) !== null && _a !== void 0 ? _a : ""); + /// Create the shelf + const shelf = createShelf(objectGraph, dataItem, searchLandingPageContext, shelfContext, shelfAttributes, shelfKind); + /// Pop the shelf location + metricsHelpersLocation.popLocation(searchLandingPageContext.metricsLocationTracker); + /// If the shelf is empty, skip it + if (serverData.isNullOrEmpty(shelf)) { + continue; + } + /// Add impressions for the shelf + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfContext.metricsOptions); + applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, [shelf], "searchLanding", shelfContext.metricsOptions.id, searchLandingPageContext.metricsPageInformation); + /// Add the shelf to the page context for processing later + searchLandingPageContext.shelves.push(shelf); + builtShelves += 1; + /// Make sure each shelf is represented by its own impressions index position + metricsHelpersLocation.nextPosition(searchLandingPageContext.metricsLocationTracker); + } +} +function pageTypeFromShelfMetaCategory(category) { + switch (category) { + case "search-landing": + return searchShelves.SearchPageType.Landing; + case "search-focus": + return searchShelves.SearchPageType.Focus; + default: + return undefined; + } +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + * @param shelfContext The context for the shelf + * @param shelfAttributes The attributes for the shelf + * @param shelfKind The content kind for the shelf + * @returns A shelf if supported, null otherwise + */ +function createShelf(objectGraph, data, pageContext, shelfContext, shelfAttributes, shelfKind) { + switch (shelfKind) { + case models.SearchLandingPageContentKind.Suggestion: + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + return createSuggestedLinksShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + else if (pageContext.pageType === searchShelves.SearchPageType.Focus) { + return createSuggestedSearchesShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + else { + return createSuggestedLinkActionsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + case models.SearchLandingPageContentKind.CategoriesAndCharts: + return createChartsCategoryShelf(objectGraph, data, false, shelfAttributes, pageContext, shelfContext); + case models.SearchLandingPageContentKind.Apps: + return createLockupsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + case models.SearchLandingPageContentKind.EditorialCollection: + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + return buildBrickShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + else { + return null; + } + default: + return createShelfFromMarker(objectGraph, data, pageContext, shelfContext, shelfAttributes); + } +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + * @param shelfContext The context for the shelf + * @param shelfAttributes The attributes for the shelf + * @returns A shelf if supported, null otherwise + */ +function createShelfFromMarker(objectGraph, data, pageContext, shelfContext, shelfAttributes) { + if (data.type !== "search-recommendations-marker") { + return null; + } + switch (mediaAttributes.attributeAsString(data, "onDevicePersonalizationUseCase")) { + case "recentSearches": + return searchHistoryShelf.createShelfWithContext(objectGraph, pageContext, shelfAttributes); + default: + return null; + } +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + */ +function createMediumAdLockupWithScreenshotsBackgroundShelf(objectGraph, data, pageContext) { + var _a, _b, _c; + const shelf = new models.Shelf("mediumAdLockupWithScreenshotsBackground"); + shelf.isHorizontal = false; + const offerEnvironment = "dark"; + const offerStyle = "white"; + const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, asString(data.attributes.name), { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + targetType: "card", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + }); + metricsOptions.kind = "adItem"; + // Set up iAdInfo + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, data); + const lockupOptions = { + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + metricsOptions: { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + disableFastImpressionsForAds: true, + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "mediumAdLockupWithScreenshotsBackground"), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mediumAdLockupWithScreenshotsBackground"), + }; + const videoConfiguration = { + canPlayFullScreen: false, + playbackControls: {}, + }; + let lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); + const platformScreenshots = lockup.screenshots[0]; + const templateString = adLockups.getTemplateTypeForMediumAdFromLockupWithScreenshots(platformScreenshots); + pageContext.metricsPageInformation.iAdInfo.setTemplateType(templateString); + const iconData = iconFromData(objectGraph, data, { + useCase: 0 /* ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + overrideTextColorKey: "textColor2", + }); + // Update the lockup value after setting the template type so that the value gets added to the lockup. + lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.setTemplateType(templateString); + } + else { + (_b = lockup.searchAd) === null || _b === void 0 ? void 0 : _b.setTemplateType(templateString); + } + const backgroundColor = iconData.backgroundColor; + const secondaryTextColor = iconData.textColor; + const mediumAd = new MediumAdLockupWithScreenshotsBackground(lockup, [platformScreenshots], true, secondaryTextColor, backgroundColor, (_c = objectGraph.bag.todayAdMediumLockupScreenshotsRiverSpeed) !== null && _c !== void 0 ? _c : 8); + addImpressionsFieldsToAd(objectGraph, mediumAd, metricsOptions, metricsOptions.pageInformation.iAdInfo); + mediumAd.clickAction = lockups.actionFromData(objectGraph, data, metricsOptions, null); + shelf.items = [mediumAd]; + return shelf; +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + */ +function createCondensedAdLockupWithIconBackgroundShelf(objectGraph, data, pageContext) { + var _a, _b, _c; + const shelf = new models.Shelf("condensedAdLockupWithIconBackground"); + shelf.isHorizontal = false; + const offerEnvironment = "dark"; + const offerStyle = "white"; + const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, asString(data.attributes.name), { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + targetType: "card", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + }); + metricsOptions.kind = "adItem"; + // Set up iAdInfo + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, data); + const lockupOptions = { + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + metricsOptions: { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + disableFastImpressionsForAds: true, + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "condensedAdLockupWithIconBackground"), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "condensedAdLockupWithIconBackground"), + }; + (_a = pageContext.metricsPageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setTemplateType("APPLOCKUP"); + const videoConfiguration = { + canPlayFullScreen: false, + playbackControls: {}, + }; + const lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType("APPLOCKUP"); + } + else { + (_c = lockup.searchAd) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); + } + const condensedAd = new models.CondensedAdLockupWithIconBackground(lockup, lockup.icon); + addImpressionsFieldsToAd(objectGraph, condensedAd, metricsOptions, metricsOptions.pageInformation.iAdInfo); + shelf.items = [condensedAd]; + return shelf; +} +// #endregion +// #region Suggested Links/Actions Shelves +/** + * Creates the suggested links shelf + * @param objectGraph The App Store dependency graph + * @param data The content items for the shelf + * @param pageContext The context for the shelf's page + * @param shelfAttributes The attributes for the shelf and its contents + * @returns A shelf for the suggested links, or null if the shelf would be empty + */ +function createSuggestedLinkActionsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a; + const linkData = mediaRelationships.relationshipCollection(data, "contents"); + const items = []; + const shelf = new models.Shelf("action"); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + for (const [linkIndex, link] of linkData.entries()) { + const metricsBase = { + targetType: "link", + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + }; + const searchAdAction = trendingSearchLinkActionFromData(objectGraph, link, pageContext, shelfAttributes); + if (serverData.isNull(searchAdAction) || serverData.isNull(searchAdAction.action)) { + continue; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAdAction.action, { + ...metricsBase, + kind: "link", + softwareType: null, + title: searchAdAction.action.title, + id: `${linkIndex}`, + idType: "sequential", + }); + items.push(searchAdAction); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + shelf.items = items; + if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: shelfAttributes.displayStyle.layoutSize, + }; + } + else if (objectGraph.client.isPhone || objectGraph.client.isPad) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: items.length >= 6 ? 2 : 1, + }; + } + return shelf; +} +/** + * Creates the suggested searches shelf for the focus page. + * @param objectGraph The App Store dependency graph + * @param data The content items for the shelf + * @param pageContext The context for the shelf's page + * @param shelfAttributes The attributes for the shelf and its contents + * @returns A shelf for the suggested links, or null if the shelf would be empty + */ +function createSuggestedSearchesShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a, _b, _c; + const linkData = mediaRelationships.relationshipCollection(data, "contents"); + if (isNothing(linkData)) { + return null; + } + const items = []; + const shelf = new models.Shelf("singleColumnList"); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + for (const [linkIndex, link] of linkData.entries()) { + const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); + if (isNothing(searchTerm) || searchTerm.length === 0) { + continue; // search term is required + } + const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; + const searchAction = createFocusPageSearchAction(objectGraph, displayTerm !== null && displayTerm !== void 0 ? displayTerm : "", searchTerm !== null && searchTerm !== void 0 ? searchTerm : "", undefined, pageContext.metricsLocationTracker, "suggested", undefined, /// MAINTAINER'S NOTE: In the future, we could use this to attribute the suggestion source to the search result fetch. + pageContext.metricsPageInformation, (_b = shelfAttributes.searchLandingItemDisplayStyle) !== null && _b !== void 0 ? _b : undefined); + if (isNothing(searchAction) || serverData.isNullOrEmpty(searchAction)) { + continue; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAction, { + targetType: "link", + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + kind: "link", + softwareType: null, + title: (_c = searchAction.title) !== null && _c !== void 0 ? _c : "", + id: `${linkIndex}`, + idType: "sequential", + }); + items.push(searchAction); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + shelf.items = items; + return shelf; +} +/** + * Creates the suggested links shelf + * @param objectGraph The App Store dependency graph + * @param data The content items for the shelf + * @param pageContext The context for the shelf's page + * @param shelfAttributes The attributes for the shelf and its contents + * @returns A shelf for the suggested links, or null if the shelf would be empty + */ +function createSuggestedLinksShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a; + const linkData = mediaRelationships.relationshipCollection(data, "contents"); + const links = []; + const shelf = new models.Shelf("searchLink"); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfContext.metricsOptions); + for (const [linkIndex, link] of linkData.entries()) { + const metricsBase = { + targetType: "link", + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + }; + const searchLink = trendingSearchLinkFromData(objectGraph, link, pageContext, shelfAttributes); + metricsHelpersImpressions.addImpressionFields(objectGraph, searchLink, { + ...metricsBase, + kind: "link", + softwareType: null, + title: searchLink.clickAction.title, + id: `${linkIndex}`, + idType: "sequential", + }); + if (serverData.isNullOrEmpty(searchLink)) { + continue; + } + links.push(searchLink); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(links)) { + return null; + } + shelf.items = links; + if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: shelfAttributes.displayStyle.layoutSize, + }; + } + else if (objectGraph.client.isPhone) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: links.length >= 6 ? 2 : 1, + }; + } + return shelf; +} +/** + * Creates a suggestion link action for the link data + * NOTE: This is legacy and the newer SLP protocol uses `trendingSearchLinkFromData` + * @param objectGraph The App Store dependency graph + * @param link The data for an individual suggestion link + * @param pageContext The page context for the search link + * @param shelfAttributes The attributes for the shelf and its contents + * @returns An action representing the suggestion link + */ +function trendingSearchLinkActionFromData(objectGraph, link, pageContext, shelfAttributes) { + var _a, _b; + /// We should always have search term, but not necessarily displayTerm. + const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); + if (isNothing(searchTerm) || searchTerm.length === 0) { + return null; + } + const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; + /// MAINTAINER'S NOTE: In the future, we could use this to attribute the suggestion source to the search result fetch. + const searchAction = new models.SearchAction(displayTerm, searchTerm, null, "suggested", undefined, undefined); + searchAction.artwork = createArtworkForSearchAction((_b = shelfAttributes.searchLandingItemDisplayStyle) !== null && _b !== void 0 ? _b : undefined, objectGraph); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", pageContext.metricsLocationTracker); + return isSome(searchAction) ? new models.SearchAdAction(searchAction) : null; +} +/** + * Creates a search action + * @param objectGraph The App Store dependency graph + * @param searchTerm The term to search for + * @param entity The entity to scope the search to, e.g. apps, stories, arcade + * @param metricsLocationTracker The metrics location tracker + * @param origin The source the search was fired from + * @param isFocusPage Whether the origin is in context of the focus page + * @param displayStyle The style to display the action item + * @returns An action representing a search + */ +export function createFocusPageSearchAction(objectGraph, title, searchTerm, entity, metricsLocationTracker, origin, source, metricsPageInformation = undefined, displayStyle) { + if (serverData.isNullOrEmpty(searchTerm)) { + return null; + } + // For Search Focus Page, text uses primary color and icon uses secondary color (instead of tintColor). + const searchAction = new models.SearchAction(title, searchTerm, null, origin, entity !== null && entity !== void 0 ? entity : undefined, source, []); + searchAction.artwork = createArtworkForSearchAction(displayStyle, objectGraph); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", metricsLocationTracker, metricsPageInformation); + return searchAction; +} +function createArtworkForSearchAction(displayStyle, objectGraph) { + var _a; + if ((displayStyle === null || displayStyle === void 0 ? void 0 : displayStyle.iconKind) === models.SearchLandingPageShelfItemIconKind.Symbol && ((_a = displayStyle === null || displayStyle === void 0 ? void 0 : displayStyle.iconKind) === null || _a === void 0 ? void 0 : _a.length)) { + return createArtworkForResource(objectGraph, `systemimage://${displayStyle.iconSymbol}`); + } + else if (objectGraph.client.isPhone || objectGraph.client.isVision) { + return createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + } + return undefined; +} +/** + * Creates a suggestion link action for the link data + * @param objectGraph The App Store dependency graph + * @param link The data for an individual suggestion link + * @param pageContext The page context for the search link + * @param shelfAttributes The attributes for the shelf and its contents + * @returns An action representing the suggestion link + */ +function trendingSearchLinkFromData(objectGraph, link, pageContext, shelfAttributes) { + var _a, _b, _c; + const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); + if (isNothing(searchTerm) || searchTerm.length === 0) { + return null; + } + const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; /// we should always have search term, but not necessarily displayTerm + let searchAction; + if (objectGraph.client.isWeb) { + const intent = makeSearchResultsPageIntent({ + ...getLocale(objectGraph), + origin: "suggested", + term: displayTerm, + platform: (_b = objectGraph.activeIntent) === null || _b === void 0 ? void 0 : _b.previewPlatform, + }); + searchAction = unwrapOptional(actionFor(intent, objectGraph)); + } + else { + searchAction = new models.SearchAction(displayTerm, displayTerm, null, "suggested"); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", pageContext.metricsLocationTracker); + } + const artwork = createArtworkForSearchAction((_c = shelfAttributes.searchLandingItemDisplayStyle) !== null && _c !== void 0 ? _c : undefined, objectGraph); + return new models.SearchLink(displayTerm, searchAction, artwork, null); +} +// #endregion +// #region Discover Lockups Shelves +/** + * Creates a shelf composed of lockups + * @param objectGraph The App Store Object Graph + * @param data The data representing this lockups shelf + * @param pageContext The page context for the shelf + * @param shelfAttributes The attributes for the shelf + * @param shelfContext The context for the shelf + * @returns A lockups shelf if any lockups could be made + */ +function createLockupsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a, _b, _c, _d, _e; + const filterType = 80894 /* filtering.Filter.All */; + const items = []; + let hasAdLockup = false; + let shelfData = initialShelfContentsFromData(data); + // Set metadata + const shelf = new models.Shelf(shelfContext.shelfStyle); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + if (objectGraph.client.isVision) { + shelf.shouldFilterApps = !mediaAttributes.attributeAsBooleanOrFalse(data, "doNotFilter"); + } + else { + shelf.shouldFilterApps = false; + } + shelf.filteringExcludedItems = shelfContext.filteringExcludedItems; + // Stitch First Position Ad (only if lockup array is nonempty) + if (serverData.isDefinedNonNullNonEmpty(shelfData)) { + const adLockup = lockupFromAdStitcher(objectGraph, pageContext, shelfContext); + if (adLockup && adLockup instanceof models.Lockup) { + hasAdLockup = true; + items.push(adLockup); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + shelfData = shelfData.filter((shelfItem) => shelfItem.id !== adLockup.adamId); // Filter dupe + } + } + // Determines whether all apps are displayed on SLP suggested apps shelf. + const hasDisplayCount = isSome(shelfAttributes.displayCount); + // If on device personalization is available, we need to personalize the data items. This reorders the shelf. + if (serverData.isDefinedNonNullNonEmpty(shelfData)) { + shelfData = impressionDemotion.personalizeDataItems(shelfData, (_a = pageContext.recoImpressionData) !== null && _a !== void 0 ? _a : {}, (_c = (_b = shelfContext.metricsOptions) === null || _b === void 0 ? void 0 : _b.recoMetricsData) !== null && _c !== void 0 ? _c : {}); + } + // Build Lockups + for (const lockupData of shelfData) { + // If we encounter a type of app-events, this means they have been incorrectly programmed, + // and we should throw the shelf away. + if (lockupData.type === "app-events") { + return null; + } + if (serverData.isNull(lockupData.attributes)) { + continue; + } + // Filter out unwanted content + if (filtering.shouldFilter(objectGraph, lockupData, filterType)) { + continue; + } + const lockup = lockupFromData(objectGraph, lockupData, pageContext, shelfContext); + if (lockup) { + items.push(lockup); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + } + if (hasDisplayCount) { + // number of apps displayed on SLP suggested shelf + const displayCount = shelfAttributes.displayCount; + shelf.items = items.slice(0, displayCount); + } + else { + shelf.items = items; + } + if (hasDisplayCount) { + const seeAllShelf = new models.Shelf(shelfContext.shelfStyle); + if (hasAdLockup) { + // Ad is the first item in array so we are dropping it here. + seeAllShelf.items = items.splice(1, items.length - 1); + } + else { + seeAllShelf.items = items; + } + // Setup Page + const seeAllPage = new models.GenericPage([seeAllShelf]); + seeAllPage.title = shelf.title; + // Setup action + const seeAllAction = new models.FlowAction("page"); + seeAllAction.pageUrl = shelfAttributes.seeAllLink; + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = seeAllPage; + // Connect action + shelf.seeAllAction = seeAllAction; + // Metrics + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, seeAllAction.pageUrl, { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + }); + const seeAllPageInformation = metricsHelpersPage.pageInformationForRoom(objectGraph, data.id); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, seeAllPage, seeAllPageInformation); + } + // Honor A/B treatment for using horizontal shelf for suggested apps, which may have an optional rowsPerColumn defined. + // NOTE: Client will choose an appropriate `rowsPerColumn` at display time, if not provided by server. + if (((_d = shelfAttributes.displayStyle) === null || _d === void 0 ? void 0 : _d.layout) === "horizontal" /* models.GenericSearchPageShelfDisplayStyleLayout.Horizontal */) { + shelf.isHorizontal = true; + shelf.rowsPerColumn = (_e = shelfAttributes.displayStyle) === null || _e === void 0 ? void 0 : _e.layoutSize; + } + return shelf; +} +/** + * Create a lockup for shelfContents to display within a grouping shelf + * @param objectGraph + * @param lockupData shelfContents to create lockup for. + * @param pageContext The page context for the lockup + * @param shelfContext The shelf context for the lockup + */ +function lockupFromData(objectGraph, lockupData, pageContext, shelfContext) { + if (serverData.isNullOrEmpty(lockupData)) { + return null; + } + if (shelfContext.shelfStyle !== "smallLockup") { + return null; + } + let offerStyle = null; + if (serverData.isDefinedNonNull(shelfContext.shelfBackground) && + (shelfContext.shelfBackground.type === "color" || shelfContext.shelfBackground.type === "interactive")) { + offerStyle = "white"; + } + // Create the lockup + const lockupOptions = { + metricsOptions: { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(lockupData), + isAdvert: adLockups.isAdvert(objectGraph, lockupData), + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfContext.shelfStyle), + offerStyle: offerStyle, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfContext.shelfStyle), + isContainedInPreorderExclusiveShelf: false, + shouldHideArcadeHeader: false, + }; + const lockup = lockups.lockupFromData(objectGraph, lockupData, lockupOptions); + if (serverData.isNull(lockup) || !lockup.isValid()) { + return null; + } + return lockup; +} +/** + * Performs `lockupFromData`, but additional with Ad stitch related side-effects. + * @param objectGraph The AppStore dependency graph + * @param pageContext The page context for the ad lockup + * @param shelfContext The shelf context for the ad lockup + */ +function lockupFromAdStitcher(objectGraph, pageContext, shelfContext) { + const task = adStitch.consumeTask(pageContext.adStitcher, shelfContext.adPositionInfo); + if (serverData.isNull(task)) { + return null; // no task for position + } + // Try to create lockup + const lockupData = task.data; + try { + const lockup = lockupFromData(objectGraph, lockupData, pageContext, shelfContext); + if (serverData.isDefinedNonNull(lockup)) { + shelfContext.filteringExcludedItems = [lockupData.id]; + } + else { + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, lockupData); + } + return lockup; + } + catch (error) { + adLogger(objectGraph, `Failed to create SLP ad lockup: ${error}`); + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, lockupData); + return null; + } +} +// #endregion +// #region Brick Shelves +function buildBrickShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + const items = []; + const shelf = new models.Shelf("brick"); + shelf.isHorizontal = mediaAttributes.attributeAsString(data, "layoutDirection") === "Horizontal"; + const shelfContents = mediaRelationships.relationshipCollection(data, "contents"); + for (const itemData of shelfContents) { + const metricsOptions = { + ...shelfContext.metricsOptions, + targetType: "brickMedium", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + // Using the location tracker from the context will cause positions to be unintentionally incremented + // by the lockup builder. Since we're only extracting icons we won't use the lockup metrics, so we can + // pass in a new, unused, location tracker instead. + const lockupMetricsOptions = { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsHelpersLocation.newLocationTracker(), + }; + const brick = buildBrick(objectGraph, itemData, CollectionShelfDisplayStyle.BrickMedium, metricsOptions, lockupMetricsOptions); + brick.clickAction = createPrimaryActionForComponentFromData(objectGraph, itemData, shelfContext); + if (!brick.isValid()) { + continue; + } + items.push(brick); + metricsHelpersLocation.nextPosition(shelfContext.metricsOptions.locationTracker); + } + shelf.title = shelfAttributes.title; + shelf.items = items; + return shelf; +} +export function createPrimaryActionForComponentFromData(objectGraph, data, shelfContext) { + const clickOptions = createBrickClickOptionsFromData(objectGraph, data, shelfContext); + const primaryAction = lockups.actionFromData(objectGraph, data, clickOptions, null); + return primaryAction; +} +function createBrickClickOptionsFromData(objectGraph, data, shelfContext) { + const clickOptions = { + ...shelfContext.metricsOptions, + id: data.id, + targetType: "brickMedium", + }; + return clickOptions; +} +// #endregion +// #region Shelf Data Extraction +/** + * Gets the initial raw shelf contents from the MAPI data object + * @param mediaApiData The raw MAPI data + * @returns A collection of data objects representing a shelf's contents + */ +function initialShelfContentsFromData(mediaApiData) { + const shelfContents = mediaRelationships.relationship(mediaApiData, "contents"); + return shelfContents === null || shelfContents === void 0 ? void 0 : shelfContents.data; +} +// #endregion +// #region Context Generators +/** + * Performs either `createMediumAdLockupWithScreenshotsBackgroundShelf` or `createCondensedAdLockupWithIconBackgroundShelf + * depending on the format defined in adDisplayStyle, but with Ad stitch related side-effects. + * @param objectGraph The AppStore dependency graph + * @param pageContext The page context for the ad lockup + * @param builtShelves The index of the current shelf + * @param adMeta The SearchLandingPageAdMeta which includes adDisplayStyle + */ +function shelfFromAdStitcher(objectGraph, pageContext, builtShelves, adMeta) { + var _a; + const task = adStitch.consumeTask(pageContext.adStitcher, { + shelfIdentifier: searchLandingPageAdShelfIdentifier, + slot: builtShelves, + }); + if (serverData.isNull(task)) { + return null; // no task for position + } + const adData = task.data; + try { + switch ((_a = adMeta === null || adMeta === void 0 ? void 0 : adMeta.adDisplayStyle) === null || _a === void 0 ? void 0 : _a.format) { + case "medium": + return createMediumAdLockupWithScreenshotsBackgroundShelf(objectGraph, adData, pageContext); + case "condensed": + return createCondensedAdLockupWithIconBackgroundShelf(objectGraph, adData, pageContext); + default: + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, adData); + return null; + } + } + catch (error) { + adLogger(objectGraph, `Failed to create SLP ad shelf: ${error}`); + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, adData); + return null; + } +} +/** + * Generates a shelf context for a search landing page shelf + * @param objectGraph The App Store Object Graph + * @param data The shelf's data object + * @param shelfAttributes The shelf's attribtues + * @param pageContext The context for the page containing the shelf + * @param shelfContentKind The type of content the shelf contains + * @returns A shelf context for a search landing page shelf + */ +function searchLandingPageShelfContext(objectGraph, data, shelfAttributes, pageContext, shelfContentKind = null) { + const baseShelfContext = searchShelves.baseShelfContext(objectGraph, data, shelfAttributes, pageContext); + switch (shelfContentKind) { + case models.SearchLandingPageContentKind.Apps: + return { + ...baseShelfContext, + shelfStyle: "smallLockup", + adPositionInfo: searchLandingPagePositionInfo, + }; + default: + return baseShelfContext; + } +} +// #endregion +//# sourceMappingURL=search-landing-shelf-controller.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js new file mode 100644 index 0000000..70c47c8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js @@ -0,0 +1,38 @@ +import { isSome } from "@jet/environment"; +import { MetadataRibbonItem } from "../../../api/models"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { categoryArtworkData } from "../../categories"; +import * as content from "../../content/content"; +import { categoryFromData } from "../../lockups/lockups"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const artworkData = categoryArtworkData(objectGraph, data, true); + const hasArtwork = isSome(artworkData); + const labelText = categoryFromData(objectGraph, data); + if (isNullOrEmpty(labelText)) { + return null; + } + if (labelText != null) { + if (dedupeSet.has(labelText)) { + return null; + } + else { + dedupeSet.add(labelText); + } + } + const viewType = hasArtwork ? "imageWithLabel" : "textLabel"; + const categoryItem = new MetadataRibbonItem(viewType); + categoryItem.moduleType = "genreDisplayName"; + categoryItem.labelText = labelText; + if (hasArtwork) { + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 20 /* content.ArtworkUseCase.CategoryIcon */, + }); + artwork.crop = "sr"; + categoryItem.artwork = artwork; + } + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "genreDisplayName", categoryItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, categoryItem, impressionOptions); + return [categoryItem]; +} +//# sourceMappingURL=category-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js new file mode 100644 index 0000000..26f815c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js @@ -0,0 +1,75 @@ +import { isSome, isNothing } from "@jet/environment/types/optional"; +import { MetadataRibbonItem } from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { contentAttributeAsDictionary } from "../../content/attributes"; +import { badgeChartKeyForClientIdentifier } from "../../content/content"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a, _b; + const chartData = chartFromData(objectGraph, data); + if (serverData.isNullOrEmpty(chartData)) { + return null; + } + const position = serverData.asNumber(chartData, "position"); + /// Per product, if the app isn't charting in the top 50, we don't want to use the chart module + if (isNothing(position) || position > 50) { + return null; + } + const genreName = (_a = serverData.asString(chartData, "genreShortName")) !== null && _a !== void 0 ? _a : serverData.asString(chartData, "genreName"); + if (genreName != null) { + if (dedupeSet.has(genreName)) { + return null; + } + else { + dedupeSet.add(genreName); + } + } + let chartItem; + if (objectGraph.bag.isLLMSearchTagsEnabled) { + chartItem = new MetadataRibbonItem("highlightedText"); + } + else { + chartItem = new MetadataRibbonItem("borderedTextLabel"); + } + chartItem.moduleType = "chartPositions"; + // Only use an ad override locale if this is an ad. + const adsOverrideLanguage = isSome((_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.searchAd) || isSome(lockup.searchAd) + ? objectGraph.bag.adsOverrideLanguage + : null; + const useAdsLocale = isSome(adsOverrideLanguage); + const loc = useAdsLocale ? objectGraph.adsLoc : objectGraph.loc; + // MAINTAINER'S NOTE: This was previously guarded by the iOS only `search_tags` feature flag that has been enabled by default on iOS only. + if (objectGraph.client.isiOS) { + const chartPositionText = loc + .string("MetadataRibbon.ChartPosition") + .replace("@@chartPosition@@", objectGraph.loc.formattedCountForPreferredLocale(objectGraph, position, adsOverrideLanguage)); + if (objectGraph.bag.isLLMSearchTagsEnabled) { + chartItem.highlightedText = chartPositionText; + chartItem.labelText = loc + .string("MetadataRibbon.ChartPositionAndCategory.Tags") + .replace("@@chartPosition@@", objectGraph.loc.formattedCountForPreferredLocale(objectGraph, position, adsOverrideLanguage)) + .replace("@@category@@", genreName); + } + else { + chartItem.labelText = genreName; + chartItem.borderedText = chartPositionText; + } + } + chartItem.secondaryViewPlacement = "leading"; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "chartPosition", chartItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, chartItem, impressionOptions); + return [chartItem]; +} +export function chartFromData(objectGraph, data) { + const chartPositionsByStore = contentAttributeAsDictionary(objectGraph, data, "chartPositions"); + if (serverData.isNullOrEmpty(chartPositionsByStore)) { + return null; + } + const storeChartKey = badgeChartKeyForClientIdentifier(objectGraph, objectGraph.host.clientIdentifier); + if (serverData.isNullOrEmpty(storeChartKey)) { + return null; + } + const chartData = serverData.asDictionary(chartPositionsByStore, storeChartKey); + return chartData; +} +//# sourceMappingURL=chart-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js new file mode 100644 index 0000000..3a451a2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js @@ -0,0 +1,37 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import { attributeAsString } from "../../../foundation/media/attributes"; +import * as contentArtwork from "../../content/artwork/artwork"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a; + let developerName = lockup.developerName; + if (isNothing(developerName)) { + developerName = (_a = attributeAsString(data, "artistName")) !== null && _a !== void 0 ? _a : attributeAsString(data, "developerName"); + } + if (developerName != null) { + if (dedupeSet.has(developerName)) { + return null; + } + else { + dedupeSet.add(developerName); + } + } + if (isSome(developerName) && developerName.length > 0) { + const developerItem = new models.MetadataRibbonItem("imageWithLabel"); + developerItem.moduleType = "developerInfo"; + developerItem.labelText = developerName; + developerItem.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://person.crop.square"); + const characterCountThreshold = 6; + developerItem.maxCharacterCount = 16; + developerItem.truncationLegibilityCharacterCountThreshold = Math.min(characterCountThreshold, developerName.length); + developerItem.allowsTruncation = developerName.length >= characterCountThreshold; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "developerInfo", developerItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, developerItem, impressionOptions); + return [developerItem]; + } + else { + return null; + } +} +//# sourceMappingURL=developer-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js new file mode 100644 index 0000000..91f5a5f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js @@ -0,0 +1,8 @@ +import { MetadataRibbonItem } from "../../../api/models"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const dividerItem = new MetadataRibbonItem("divider"); + dividerItem.moduleType = "divider"; + dividerItem.labelText = "|"; + return [dividerItem]; +} +//# sourceMappingURL=divider-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js new file mode 100644 index 0000000..764624e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js @@ -0,0 +1,21 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a; + if (lockup.isEditorsChoice) { + const editorsChoiceItem = new models.MetadataRibbonItem("editorsChoice"); + editorsChoiceItem.moduleType = "editorialBadgeInfo"; + // Only use an ad override locale if this is an ad. + editorsChoiceItem.useAdsLocale = + (isSome((_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.searchAd) || isSome(lockup.searchAd)) && + isSome(objectGraph.bag.adsOverrideLanguage); + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "editorialBadgeInfo", "Editors Choice", "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, editorsChoiceItem, impressionOptions); + return [editorsChoiceItem]; + } + else { + return null; + } +} +//# sourceMappingURL=editors-choice-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js new file mode 100644 index 0000000..40d18c0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js @@ -0,0 +1,36 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as contentArtwork from "../../content/artwork/artwork"; +import * as contentAttributes from "../../content/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a; + let isGameControllerSupported = false; + switch (contentAttributes.contentAttributeAsString(objectGraph, data, "remoteControllerRequirement")) { + case "CONTROLLER_REQUIRED": + case "CONTROLLER_OPTIONAL": + isGameControllerSupported = true; + break; + default: + break; + } + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsGameController")) { + isGameControllerSupported = true; + } + if (!isGameControllerSupported) { + return null; + } + const gameControllerItem = new models.MetadataRibbonItem("imageWithLabel"); + gameControllerItem.moduleType = "supportsGameController"; + // Only use an ad override locale if this is an ad. + const useAdsLocale = (isSome(lockup.searchAd) || isSome((_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.searchAd)) && + isSome(objectGraph.bag.adsOverrideLanguage); + gameControllerItem.labelText = useAdsLocale + ? objectGraph.adsLoc.string("BADGE_MFI_SUPPORTED") + : objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + gameControllerItem.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "supportsGameController", "Supports Game Controller", "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, gameControllerItem, impressionOptions); + return [gameControllerItem]; +} +//# sourceMappingURL=game-controller-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js new file mode 100644 index 0000000..5c6f3a6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js @@ -0,0 +1,24 @@ +import * as categoryMetadataRibbonItem from "./category-metadata-ribbon-item"; +import * as chartMetadataRibbonItem from "./chart-metadata-ribbon-item"; +import * as developerMetadataRibbonItem from "./developer-metadata-ribbon-item"; +import * as dividerMetadataRibbonItem from "./divider-metadata-ribbon-item"; +import * as editorsChoiceMetadataRibbonItem from "./editors-choice-metadata-ribbon-item"; +import * as gameControllerMetadataRibbonItem from "./game-controller-metadata-ribbon-item"; +import * as secondaryShortCategoriesMetadataRibbonItem from "./secondary-short-categories-metadata-ribbon-item"; +import * as shortCategoryMetadataRibbonItem from "./short-category-metadata-ribbon-item"; +import * as starRatingMetadataRibbonItem from "./star-rating-metadata-ribbon-item"; +import * as tagMetadataRibbonItem from "./tag-metadata-ribbon-item"; +export const standardList = { + // every key in Key must be present + chartPositions: chartMetadataRibbonItem.createMetadataRibbonItems, + genreDisplayName: categoryMetadataRibbonItem.createMetadataRibbonItems, + genreShortDisplayName: shortCategoryMetadataRibbonItem.createMetadataRibbonItems, + secondaryGenreShortDisplayNames: secondaryShortCategoriesMetadataRibbonItem.createMetadataRibbonItems, + developerInfo: developerMetadataRibbonItem.createMetadataRibbonItems, + editorialBadgeInfo: editorsChoiceMetadataRibbonItem.createMetadataRibbonItems, + userRating: starRatingMetadataRibbonItem.createMetadataRibbonItems, + supportsGameController: gameControllerMetadataRibbonItem.createMetadataRibbonItems, + tag: tagMetadataRibbonItem.createMetadataRibbonItems, + divider: dividerMetadataRibbonItem.createMetadataRibbonItems, +}; +//# sourceMappingURL=metadata-ribbon-item-factory.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js new file mode 100644 index 0000000..1b487cb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js @@ -0,0 +1,27 @@ +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { standardList } from "./metadata-ribbon-item-factory"; +export function createMetadataRibbonItemsForLockup(objectGraph, data, lockup, itemTypes, options, metadataRibbonItemFactory = standardList) { + if (serverData.isNullOrEmpty(itemTypes)) { + return []; + } + const metadataRibbonItems = []; + const dedupeSet = new Set(); + for (const itemSlot of itemTypes) { + if (serverData.isNullOrEmpty(itemSlot)) { + continue; + } + for (const itemType of itemSlot) { + const metadataRibbonFactory = metadataRibbonItemFactory[itemType]; + if (serverData.isNull(metadataRibbonFactory)) { + continue; + } + const results = metadataRibbonFactory(objectGraph, data, lockup, dedupeSet, options.metricsOptions); + if (serverData.isDefinedNonNull(results)) { + metadataRibbonItems.push(...results); + break; + } + } + } + return metadataRibbonItems; +} +//# sourceMappingURL=metadata-ribbon.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js new file mode 100644 index 0000000..217876d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js @@ -0,0 +1,22 @@ +import { MetadataRibbonItem } from "../../../api/models"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import { isNothing, isSome } from "@jet/environment"; +/** + * Creates a metadata ribbon item for ranked secondary category metadata ribbon type. + * This particular function takes in the type string since we grab it from the data in the search-tags-ribbon. + */ +export function createMetadataRibbonItemsForRankedSecondaryCategory(objectGraph, data, lockup, dedupeSet, metricsOptions) { + if (isNothing(data) || data.length === 0 || dedupeSet.has(data)) { + return null; + } + const tagItem = new MetadataRibbonItem("textLabel"); + tagItem.moduleType = "rankedSecondaryGenre"; + if (isSome(data)) { + tagItem.labelText = data; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "rankedSecondaryGenre", tagItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, tagItem, impressionOptions); + } + dedupeSet.add(data); + return [tagItem]; +} +//# sourceMappingURL=ranked-secondary-category-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js new file mode 100644 index 0000000..3901446 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js @@ -0,0 +1,72 @@ +import { isSome } from "@jet/environment"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { standardList } from "./metadata-ribbon-item-factory"; +import * as rankedSecondaryCategoryMetadataRibbonItem from "./ranked-secondary-category-metadata-ribbon-item"; +export function createSearchTagsRibbonItemsForLockup(objectGraph, data, lockup, itemSlots, options, metadataRibbonItemFactory = standardList) { + if (serverData.isNullOrEmpty(itemSlots)) { + return []; + } + const tagData = serverData.asArrayOrEmpty(data.meta, "associations.tags.data"); + const metadataRibbonItems = []; + // We need to keep track of how many tags we have so we can assign the tag properly + let tagIndex = 0; + let rankedSecondaryGenreIndex = 0; + // We are going to pass in a set of strings for items we have already added to the ribbon so we never duplicate items + const dedupeSet = new Set(); + for (const itemSlot of itemSlots) { + const itemTypes = Array.isArray(itemSlot) ? itemSlot : [itemSlot]; + if (serverData.isNullOrEmpty(itemTypes)) { + continue; + } + for (const itemType of itemTypes) { + // If we find a tag, we pass in the tag data specifically + // If we find a rankedSecondaryGenre, we want to call the factory function specifically. + const isTag = itemType === "tag"; + const isRankedSecondaryGenre = itemType === "rankedSecondaryGenre"; + let results; + let metadataItemData = data; + let metadataItemString = ""; + if (isRankedSecondaryGenre) { + const searchExperimentDataForLockup = serverData.asDictionary(data, "meta"); + if (isSome(searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.rankedSecondaryGenreShortDisplayNames)) { + metadataItemString = + searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.rankedSecondaryGenreShortDisplayNames[rankedSecondaryGenreIndex]; + } + if (isSome(metadataItemString)) { + results = + rankedSecondaryCategoryMetadataRibbonItem.createMetadataRibbonItemsForRankedSecondaryCategory(objectGraph, metadataItemString, lockup, dedupeSet, options.metricsOptions); + rankedSecondaryGenreIndex = rankedSecondaryGenreIndex + 1; + } + else { + results = []; + } + } + else { + const metadataRibbonFactory = metadataRibbonItemFactory[itemType]; + if (serverData.isNull(metadataRibbonFactory)) { + continue; + } + if (isTag) { + metadataItemData = tagData[tagIndex]; + } + else { + metadataItemData = data; + } + results = metadataRibbonFactory(objectGraph, metadataItemData, lockup, dedupeSet, options.metricsOptions); + tagIndex = isTag ? tagIndex + 1 : tagIndex; + } + if (serverData.isDefinedNonNull(results)) { + metadataRibbonItems.push(...results); + for (const result of results) { + if (isSome(result.impressionMetrics)) { + metricsHelpersLocation.nextPosition(options.metricsOptions.locationTracker); + } + } + break; + } + } + } + return metadataRibbonItems; +} +//# sourceMappingURL=search-tags-ribbon.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js new file mode 100644 index 0000000..c5469c7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js @@ -0,0 +1,27 @@ +import { MetadataRibbonItem } from "../../../api/models"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsArrayOrEmpty } from "../../../foundation/media/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const secondaryGenres = attributeAsArrayOrEmpty(data, "secondaryGenreShortDisplayNames"); + if (isNullOrEmpty(secondaryGenres)) { + return null; + } + const secondaryCategoryItems = secondaryGenres.map((secondaryGenre) => { + const categoryItem = new MetadataRibbonItem("textLabel"); + // Workaround for changing the moduleType to secondaryGenreShortDisplayNames from secondaryGenreShortDisplayName + // otherwise native doesnt layout the secondary genres correctly + // will be fixed with rdar://127458403 (Allow unknown metadataribbon items) + categoryItem.moduleType = "genreShortDisplayName"; + categoryItem.labelText = secondaryGenre; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "genreDisplayName", categoryItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, categoryItem, impressionOptions); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + return categoryItem; + }); + return secondaryCategoryItems.filter((category) => { + return category.labelText != null && !dedupeSet.has(category.labelText); + }); +} +//# sourceMappingURL=secondary-short-categories-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js new file mode 100644 index 0000000..16a148a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js @@ -0,0 +1,36 @@ +import { isNothing, isSome } from "@jet/environment"; +import { MetadataRibbonItem } from "../../../api/models"; +import { attributeAsString } from "../../../foundation/media/attributes"; +import { categoryArtworkData } from "../../categories"; +import { artworkFromApiArtwork } from "../../content/content"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const artworkData = categoryArtworkData(objectGraph, data, true); + const hasArtwork = isSome(artworkData); + const shortGenre = attributeAsString(data, "genreShortDisplayName"); + if (shortGenre != null) { + if (dedupeSet.has(shortGenre)) { + return null; + } + else { + dedupeSet.add(shortGenre); + } + } + if (isNothing(shortGenre) || shortGenre.length === 0) { + return null; + } + const viewType = hasArtwork ? "imageWithLabel" : "textLabel"; + const shortCategoryItem = new MetadataRibbonItem(viewType); + shortCategoryItem.moduleType = "genreShortDisplayName"; + shortCategoryItem.labelText = shortGenre; + if (hasArtwork) { + shortCategoryItem.artwork = artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 20 /* ArtworkUseCase.CategoryIcon */, + cropCode: "sr", + }); + } + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "genreDisplayName", shortCategoryItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, shortCategoryItem, impressionOptions); + return [shortCategoryItem]; +} +//# sourceMappingURL=short-category-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js new file mode 100644 index 0000000..81587f8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js @@ -0,0 +1,20 @@ +import { MetadataRibbonItem } from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { contentAttributeAsBooleanOrFalse } from "../../content/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const isPreorder = contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder"); + if (serverData.isDefinedNonNull(lockup.ratingCount) && serverData.isDefinedNonNull(lockup.rating) && !isPreorder) { + const starRatingItem = new MetadataRibbonItem("starRating"); + starRatingItem.moduleType = "userRating"; + starRatingItem.starRating = lockup.rating; + starRatingItem.labelText = lockup.ratingCount; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "userRating", "User Rating", "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, starRatingItem, impressionOptions); + return [starRatingItem]; + } + else { + return null; + } +} +//# sourceMappingURL=star-rating-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js new file mode 100644 index 0000000..37877c3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js @@ -0,0 +1,18 @@ +import { isNothing } from "@jet/environment"; +import { MetadataRibbonItem } from "../../../api/models"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const tagData = data; + const tagItem = new MetadataRibbonItem("textLabel"); + tagItem.moduleType = "tag"; + tagItem.labelText = mediaAttributes.attributeAsString(tagData, "name"); + if (isNothing(tagItem.labelText) || tagItem.labelText.length === 0 || dedupeSet.has(tagItem.labelText)) { + return null; + } + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, tagData.id, tagItem.labelText, "tag_id"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, tagItem, impressionOptions); + dedupeSet.add(tagItem.labelText); + return [tagItem]; +} +//# sourceMappingURL=tag-metadata-ribbon-item.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js new file mode 100644 index 0000000..aedcef1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js @@ -0,0 +1,87 @@ +/** + * Implements the ODML Treatment (On-device machine learning) for Sponsored search. + */ +"use strict"; +import { adLogger } from "../../common/search/search-ads"; +import { isNull, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { shallowCopyOf } from "../../foundation/util/objects"; +import { decorateAdInstanceIdOnData } from "../ads/ad-common"; +import { productVariantDataForData, productVariantIDForVariantData } from "../product-page/product-page-variants"; +// region exports +/** + * Merge the contents of the raw response with the data in `advertData` from `SearchAds.framework` + * + * @param rawAdverts The adverts returned in the network response. + * @param nativeAdvertData The advert data that was returned from native. + */ +export function applyNativeAdvertData(objectGraph, rawAdverts, nativeAdvertData) { + if (isNull(nativeAdvertData)) { + return rawAdverts; // even if `nativeAdvertData` may contain error, we need to perform apply to merge instance ids. + } + const updated = []; + const rawAdvertsMap = rawAdverts.reduce((acc, data) => ({ ...acc, [data.id]: data }), {}); + for (const nativeAdData of nativeAdvertData.adverts) { + const rawAd = rawAdvertsMap[nativeAdData.adamId]; + if (isNullOrEmpty(rawAd)) { + adLogger(objectGraph, `[${nativeAdData.adamId}] skipped - Data was not part of original response`); + continue; + } + if (isNullOrEmpty(rawAd.attributes)) { + adLogger(objectGraph, `[${rawAd.id}] skipped - Data is missing attributes`); + continue; + } + const newAd = createCopyWithNativeData(objectGraph, rawAd, nativeAdData); + updated.push(newAd); + } + if (!preprocessor.PRODUCTION_BUILD) { + const rawOrder = rawAdverts.map((ad) => ad.id).join(" "); + const updatedOrder = updated.map((ad) => ad.id).join(" "); + adLogger(objectGraph, `applyNativeAdvertData: [${rawOrder}] => [${updatedOrder}]`); + } + return updated; +} +/** + * Returns whether or not ODML treatment was successful + */ +export function wasODMLSuccessful(objectGraph, nativeAdvertData) { + return nativeAdvertData && nativeAdvertData.odmlSuccess; +} +// endregion +// region internals +/** + * Create a copy of `ad` with the `adData` replacing the "iad" attribute. + * @param ad The raw ad to copy + * @param adData The ad blob to overrwite wth. + */ +function createCopyWithNativeData(objectGraph, ad, nativeAdData) { + const copy = shallowCopyOf(ad); + const attributes = shallowCopyOf(ad.attributes); + attributes["iads"] = nativeAdData.adData; + copy.attributes = attributes; + overrideCustomProductPageIdIfRequired(objectGraph, copy, nativeAdData); + decorateAdInstanceIdOnData(objectGraph, copy, nativeAdData.instanceId); + return copy; +} +/** + * Modifies an `ad` by overriding the `ppid` value if native ODML has dictated a new selection. + * Note: This may intentionally replace the `ppid` value with `null` if CPP is disabled for Search Ads. + * If we pass native all `null` values for ODML processing (because the feature is disabled in the bag), + * we expect to receive `null` back and insert `null` into the `ad` here. + * @param ad The ad to modify. + * @param adData The ad blob to overrwite with. + */ +function overrideCustomProductPageIdIfRequired(objectGraph, ad, nativeAdData) { + var _a; + const productVariantData = productVariantDataForData(objectGraph, ad); + const productVariantId = productVariantIDForVariantData(productVariantData); + // If there is a "selected" cppId, and it's different to the `serverCppId`, native has made a new selection. + // Confirm we have a cppData of meta to modify + if (nativeAdData.selectedCppId === productVariantId || isNullOrEmpty((_a = ad === null || ad === void 0 ? void 0 : ad.meta) === null || _a === void 0 ? void 0 : _a.cppData)) { + return; + } + // Modify the meta data with the new selection. + const meta = shallowCopyOf(ad.meta); + meta.cppData["ppid"] = nativeAdData.selectedCppId; + ad.meta = meta; +} +//# sourceMappingURL=search-ads-odml.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js new file mode 100644 index 0000000..e8377d4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js @@ -0,0 +1,1047 @@ +// +// search-ads.ts +// AppStoreKit +// +// Created by Joel Parsons on 24/October/2019 +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isSome } from "@jet/environment"; +import * as models from "../../api/models"; +import { ads } from "../../api/typings/constants"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import { shallowCopyOf } from "../../foundation/util/objects"; +import { isAdLocalizationValid } from "../ads/ad-common"; +import * as client from "../../foundation/wrappers/client"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as filtering from "../filtering"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import { customCreativeArtworkFromData, customCreativeVideoFromData } from "./custom-creative"; +import { platformAttributeAsDictionary } from "../../foundation/media/platform-attributes"; +import { searchResultWillUseAppEventDisplay } from "./content/search-results"; +/** + * Data passed from native for requesting Sponsored Search + */ +export class SponsoredSearchRequestData { + constructor(data, appStoreClientRequestId) { + if (!data) { + return; + } + this.appStoreClientRequestId = appStoreClientRequestId; + this.iAdId = data["iAdId"]; + this.sponsoredSearchRequestData = data["dataBlob"]; + this.routingInfo = data["iAdRoutingInfo"]; + this.canary = data["canary"]; + } + validAdRequest() { + const hasRequestData = this.sponsoredSearchRequestData && this.sponsoredSearchRequestData.length > 0; + const hasRoutingInfo = this.routingInfo && this.routingInfo.length > 0; + return hasRequestData && hasRoutingInfo; + } +} +const searchVideoConfiguration = { + canPlayFullScreen: false, + playbackControls: {}, +}; +export function adsResultFromSearchResults(objectGraph, advertDatum, resultsDatum, requestMetadata, metricsOptions, installedStates, appStates, searchExperimentsData, personalizationDataContainer) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + const advertsSearchResult = new models.AdvertsSearchResult(); + const isNetworkConstrained = (_a = requestMetadata.requestDescriptor.isNetworkConstrained) !== null && _a !== void 0 ? _a : false; + const advertMetricsOptions = { + id: "ad_container", + kind: "iosSoftware", + softwareType: null, + targetType: null, + title: "ad_container", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + idType: "sequential", + }; + /** + * # Why do we push ad_container? + * We push an identifier for element that doesn't exist (`ad_container`) to prevent impression indices of organic search results + * being affected by having multiple ads built for ad-rotation. + * + * It is expected that: + * - `ad_container` will not impresss, since it doesn't exist + * - `parentImpressionId` will not be set expected. + */ + metricsHelpersLocation.pushContentLocation(objectGraph, advertMetricsOptions, "ad_container"); + if (serverData.isNullOrEmpty(advertDatum)) { + return { + result: advertsSearchResult, + }; + } + const firstSearchResult = resultsDatum[0]; + let firstAdComputedStyle; + const adIdsString = advertDatum + .filter(serverData.isDefinedNonNull) + .map((ad) => `[${ad.id}]`) + .join(", "); + const adString = `Adverts received from ad server: ${adIdsString}`; + adLogger(objectGraph, adString); + let isFirstAd = true; + for (const ad of advertDatum) { + if (serverData.isNull(ad)) { + continue; + } + if (filtering.shouldFilter(objectGraph, ad)) { + adLogger(objectGraph, `[${ad.id}] filtered by shouldFilter() - app probably not supported on current os or device`); + continue; + } + const isDupe = adIsDupe(ad.id, firstSearchResult === null || firstSearchResult === void 0 ? void 0 : firstSearchResult.id, installedStates); + // Extract the iAd data dictionary based on the first organic search result. + const adDataType = iAdDataTypeForAdvert(firstSearchResult, isDupe); + ad.attributes["iad"] = iadAttributesForType(ad, adDataType); + if (serverData.isNullOrEmpty(ad.attributes["iad"])) { + adLogger(objectGraph, `[${ad.id}] filtered because no appropriate iAd dictionary was found. (Probably a server issue if hitting this)`); + continue; + } + const adLockupOptions = { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "card", + isAdvert: true, + }, + hideZeroRatings: true, + artworkUseCase: 8 /* content.ArtworkUseCase.SearchIcon */, + isNetworkConstrained: isNetworkConstrained, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mixedMediaLockup"), + }; + const iAdData = contentAttributes.contentAttributeAsDictionary(objectGraph, ad, "iad"); + const iAdAllowsMedia = serverData.asBooleanOrFalse(iAdData, "format.images"); + let creativeHasArtworkToDisplay = false; + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const customCreativeData = platformAttributeAsDictionary(ad, contentAttributes.bestAttributePlatformFromData(objectGraph, ad), "creativeAttributes"); + const customCreativeArtwork = customCreativeArtworkFromData(objectGraph, ad, customCreativeData); + const customCreativeVideo = customCreativeVideoFromData(objectGraph, ad, customCreativeData, searchVideoConfiguration); + creativeHasArtworkToDisplay = isSome(customCreativeArtwork) || isSome(customCreativeVideo); + } + const noPreviousAdStyle = serverData.isNullOrEmpty(firstAdComputedStyle); + const temporaryAdLockup = lockups.mixedMediaAdLockupFromData(objectGraph, ad, adLockupOptions, searchVideoConfiguration, searchExperimentsData); + const platformLockupMedia = platformMediaForLockup(temporaryAdLockup); + // Adverts should only rotate between the same display style. The first advert dictates the display style + // for the rest of the adverts. Once we have an ad display style locked in we try to create the next ads in + // a style compatible with the first. If it can't satisfy any compatible styles, it gets thrown away. + const screenshotsDisplayStyle = creativeHasArtworkToDisplay + ? "four-screenshots" + : (_b = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _b === void 0 ? void 0 : _b.screenshots; + const computedAdDisplayStyle = resolvedAdDisplayStyleForMedia(objectGraph, platformLockupMedia, ad.id, adDataType, iAdAllowsMedia, firstAdComputedStyle, screenshotsDisplayStyle, firstSearchResult, installedStates, appStates, metricsOptions, personalizationDataContainer); + if (serverData.isNull(computedAdDisplayStyle)) { + adLogger(objectGraph, `[${ad.id}] will not be displayed because we could not create an ad style compatible with ${debugDescriptionForStyle(firstAdComputedStyle)}`); + continue; + } + // Check the localization is valid for the ad with the selected display style. + // We do this _before_ storing the style (if this is the first ad) to avoid setting + // a style based on an ad that can't be shown. + if (!isAdLocalizationValid(objectGraph, ad, null, computedAdDisplayStyle.style)) { + adLogger(objectGraph, `[${ad.id}] filtered because localization is not available`); + continue; + } + if (noPreviousAdStyle) { + // For the first advert we calculate a display style based on whether it is a dupe of the first organic result + // and what media is available for display for the app + adLogger(objectGraph, `[${ad.id}] first ad dictates ad display style of: ${debugDescriptionForStyle(computedAdDisplayStyle)}`); + firstAdComputedStyle = computedAdDisplayStyle; + } + else { + adLogger(objectGraph, `[${ad.id}] will be displayed because it is compatible with the display style of: ${debugDescriptionForStyle(computedAdDisplayStyle)}, which is the same height as display style: ${debugDescriptionForStyle(firstAdComputedStyle)}`); + } + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, ad); + // Set the template type before creating the lockup to ensure the click event has the right data. + (_c = metricsOptions.pageInformation.iAdInfo) === null || _c === void 0 ? void 0 : _c.setTemplateType(computedAdDisplayStyle.style); + let adResultLockup = lockups.mixedMediaAdLockupFromData(objectGraph, ad, adLockupOptions, searchVideoConfiguration, searchExperimentsData); + adResultLockup = modifyLockupToMatchAdDisplayStyle(adResultLockup, computedAdDisplayStyle, isDupe, isFirstAd); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_d = adResultLockup.searchAdOpportunity) === null || _d === void 0 ? void 0 : _d.setTemplateType(computedAdDisplayStyle.style); + } + else { + (_e = adResultLockup.searchAd) === null || _e === void 0 ? void 0 : _e.setTemplateType(computedAdDisplayStyle.style); + } + if (computedAdDisplayStyle.style === "TEXT" /* SearchAdDisplayStyle.TEXT */) { + const iAdTextKey = mediaAttributes.attributeAsString(ad, "iad.format.text"); + if (iAdTextKey !== "none") { + let advertisingText; + if (iAdTextKey === "description") { + advertisingText = contentAttributes.contentAttributeAsString(objectGraph, ad, "description.standard"); + } + else { + advertisingText = contentAttributes.contentAttributeAsString(objectGraph, ad, iAdTextKey); + } + const searchAd = (_f = adResultLockup.searchAd) !== null && _f !== void 0 ? _f : (_g = adResultLockup.searchAdOpportunity) === null || _g === void 0 ? void 0 : _g.searchAd; + if (serverData.isDefinedNonNull(searchAd) && serverData.isDefinedNonNull(advertisingText)) { + searchAd.advertisingText = advertisingText; + } + } + advertsSearchResult.displaysScreenshots = false; + } + if (serverData.isDefinedNonNullNonEmpty(adResultLockup)) { + const duplicatePosition = findOrganicLockupPosition(resultsDatum, adResultLockup.adamId); + if (isSome(duplicatePosition) && !creativeHasArtworkToDisplay) { + if (objectGraph.props.enabled("advertSlotReporting")) { + (_h = adResultLockup.searchAdOpportunity) === null || _h === void 0 ? void 0 : _h.setDuplicatePosition(duplicatePosition); + } + else { + (_j = adResultLockup.searchAd) === null || _j === void 0 ? void 0 : _j.setDuplicatePosition(duplicatePosition); + } + } + advertsSearchResult.lockups.push(adResultLockup); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + isFirstAd = false; + } + } + // Always pop the `ad_container` location. + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + // There are situations where all ads are filtered out - check the results have at least one item prior to modifying metrics data. + if (serverData.isDefinedNonNullNonEmpty(advertsSearchResult.lockups)) { + // Once all the lockups are built re-set the page information to use the iAd Info of the first advert + // This (probably) happens to ensure that the page event accurately reflects what's being presented + // to the user at first view. + const firstAd = advertDatum[0]; + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, firstAd); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + if (firstAdComputedStyle) { + (_k = metricsOptions.pageInformation.iAdInfo) === null || _k === void 0 ? void 0 : _k.setTemplateType(firstAdComputedStyle.style); + } + else { + (_l = metricsOptions.pageInformation.iAdInfo) === null || _l === void 0 ? void 0 : _l.setTemplateType(null); + } + advertsSearchResult.condensedBehavior = "never"; + return { + result: advertsSearchResult, + displayStyle: firstAdComputedStyle === null || firstAdComputedStyle === void 0 ? void 0 : firstAdComputedStyle.style, + }; +} +/** + * Determines whether or not the ad is considered a dupe of the first organic result. + * @param adID The adaimID of the ad result + * @param firstResultID The adamID of the first organic result + * @param installedState A mapping of adamIDs to device app install state to determine if the ad/first result is on the user's device + * @returns whether the ad and result are dupes of the same app and that the first result will not condensed as a condensed first result + * will have no bearing on dupe status or display. + */ +function adIsDupe(adID, firstResultID, installedState) { + const isResultInstalled = installedState && installedState[firstResultID]; + const isDupeResult = adID && firstResultID && adID === firstResultID; + return isDupeResult && !isResultInstalled; +} +// endregion +// region Ad Display Styles +/** + * Resolves a SearchAdDisplayStyle for the given ad media and the search results context (the first organic search result). + * If the ad is a dupe of the first organic search result, we attempt to create two lockups with "full creative" to ensure there is enough + * media to show both. + * @param media the media from which to calculate the style. + * @param adId the id of the ad, for logging purposes. + * @param adDataType the ad type, based on the first search result. + * @param firstAdComputedStyle a previously computed `SearchAdDisplayStyleContainer`, if any, to maintain compatibility. + * @param firstSearchResult The first organic search result which we informs how to handle DUP logic + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past + * @param metricsOptions Metrics options for built models. + * @param personalizationDataContainer The data container to use for personalizing the data. + * @returns a `SearchAdDisplayStyleContainer` with a style compatible with the first search result, if one can be found. + */ +function resolvedAdDisplayStyleForMedia(objectGraph, media, adId, adDataType, iAdAllowsMedia, firstAdComputedStyle, screenshotsDisplayStyle, firstSearchResult, installStates, appStates, metricsOptions, personalizationDataContainer) { + const isFirstAd = serverData.isNullOrEmpty(firstAdComputedStyle); + const adDisplayStyle = getAdDisplayStyleForMedia(objectGraph, media, adId, iAdAllowsMedia, firstAdComputedStyle, screenshotsDisplayStyle); + if (serverData.isNull(adDisplayStyle)) { + return null; + } + const searchAdDisplayContainer = { + platform: media.mediaPlatformUsedForDisplayStyle, + style: adDisplayStyle, + }; + adLogger(objectGraph, `[${adId}] tentatively resolved to: ${debugDescriptionForStyle(searchAdDisplayContainer)}`); + if (adDataType === "DUP" /* iAdDataType.DUPE_AD */) { + removeUsedMediaForAdDisplayStyle(adDisplayStyle, media); + const organicSearchResultDisplayStyle = getAdDisplayStyleForMedia(objectGraph, media, adId, iAdAllowsMedia, null, screenshotsDisplayStyle); + const organicHasFullCreative = isDisplayStyleFullCreativeForOrganic(objectGraph, organicSearchResultDisplayStyle, screenshotsDisplayStyle); + const organicWillUseAppEvent = searchResultWillUseAppEventDisplay(objectGraph, firstSearchResult, installStates, appStates, metricsOptions, personalizationDataContainer); + if ((organicHasFullCreative || organicWillUseAppEvent) && isFirstAd) { + adLogger(objectGraph, `[${adId}] Organic Dupe would be full creative as ${organicSearchResultDisplayStyle} so choosing tentative style for ad`); + return searchAdDisplayContainer; + } + else if (organicHasFullCreative && + !isFirstAd && + isProposedAdStyleCompatible(adDisplayStyle, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] Organic Dupe would be a full creative, but ad is not the first so returning compatible style with first ${adDisplayStyle}`); + return searchAdDisplayContainer; + } + else if (isProposedAdStyleCompatible("UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result so returning UNRESTRICTED_IMAGE_ONE_ASSET`); + return { + style: "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */, + }; + } + else if (isProposedAdStyleCompatible("UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result so returning UNRESTRICTED_VIDEO_ONE_ASSET`); + return { + style: "UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */, + }; + } + else if (isProposedAdStyleCompatible("TEXT" /* SearchAdDisplayStyle.TEXT */, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result so returning TEXT`); + return { + style: "TEXT" /* SearchAdDisplayStyle.TEXT */, + }; + } + else { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result and first style is not compatible with TEXT so skipping ad`); + return null; + } + } + else if (serverData.isDefinedNonNull(firstAdComputedStyle) && + firstAdComputedStyle.style === "TEXT" /* SearchAdDisplayStyle.TEXT */) { + adLogger(objectGraph, `[${adId}] tentative style would be filtered since the first ad has style: ${debugDescriptionForStyle(firstAdComputedStyle)}, so returning TEXT`); + return { + style: "TEXT" /* SearchAdDisplayStyle.TEXT */, + }; + } + return searchAdDisplayContainer; +} +function allowsFourScreenshots(screenshotsDisplayStyle) { + if (!serverData.isDefinedNonNull(screenshotsDisplayStyle)) { + return false; + } + return screenshotsDisplayStyle === "four-screenshots"; +} +/** + * Finds the position of a specific lockup within the organic search results + * @param searchResults List of search results + * @param adResult The adamId of the app to search for + * @returns The 0-indexed position in the list that contains the lockup. If the organic results doesn't contain the lockup, this instead returns null. + */ +function findOrganicLockupPosition(searchResults, adamId) { + const index = searchResults.findIndex((datum) => datum.id === adamId); + return index === -1 ? null : index; +} +/** + * Returns a suitable style for a given set of lockup media. + * If a firstComputedAdStyle is provided, we check to confirm the proposed style is compatible before selecting that new style. + * @param media a set of media from which to calculate a style. + * @param adId the id of the ad, for logging purposes. + * @param iAdAllowsMedia whether the iAd Data has enabled media for the given ad. + * @param firstComputedAdStyle whether the iAd Data has enabled media for the given ad. + * @returns the preferred display style from the provided set of media. + */ +function getAdDisplayStyleForMedia(objectGraph, media, adId, iAdAllowsMedia, firstComputedAdStyle, screenshotsDisplayStyle) { + if (!iAdAllowsMedia) { + adLogger(objectGraph, `[${adId}] is not allowed to display media because of iAd configuration.`); + return "TEXT" /* SearchAdDisplayStyle.TEXT */; + } + if (media.mediaPlatformUsedForDisplayStyle && + firstComputedAdStyle && + firstComputedAdStyle.mediaPlatform && + !media.mediaPlatformUsedForDisplayStyle.isEqualTo(firstComputedAdStyle.mediaPlatform)) { + adLogger(objectGraph, `[${adId}] filtered because media is derived from: ${media.mediaPlatformUsedForDisplayStyle.mediaType}, but first ad media is derived from: ${firstComputedAdStyle.mediaPlatform.mediaType}`); + return null; + } + let displayStyle; + let firstVideoPreview = null; + if (serverData.isDefinedNonNullNonEmpty(media.videos)) { + const firstVideo = media.videos[0]; + firstVideoPreview = firstVideo.preview; + } + if (isSome(media.alignedRegionArtwork) && + isProposedAdStyleCompatible("UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */, firstComputedAdStyle)) { + displayStyle = "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */; + } + else if (isSome(media.alignedRegionVideo) && + isProposedAdStyleCompatible("UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */, firstComputedAdStyle)) { + displayStyle = "UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */; + } + else if (serverData.isDefinedNonNullNonEmpty(firstVideoPreview) && + firstVideoPreview.isLandscape() && + isProposedAdStyleCompatible("LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */, firstComputedAdStyle)) { + // If first trailer's preview is landscape the lockup will render as landscape video + displayStyle = "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */; + } + else if (serverData.isDefinedNonNullNonEmpty(firstVideoPreview) && + firstVideoPreview.isPortrait() && + allowsFourScreenshots(screenshotsDisplayStyle) && + isProposedAdStyleCompatible("PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + // If first trailer's preview is portrait the lockup will render as portrait with portrait images filled in + if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && media.portraitScreenshots.length >= 3) { + displayStyle = "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && + media.portraitScreenshots.length >= 2) { + displayStyle = "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots)) { + displayStyle = "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */; + } + else { + displayStyle = "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */; + } + } + else if (serverData.isDefinedNonNullNonEmpty(firstVideoPreview) && + firstVideoPreview.isPortrait() && + isProposedAdStyleCompatible("PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + // If first trailer's preview is portrait the lockup will render as portrait with portrait images filled in + if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && media.portraitScreenshots.length >= 2) { + displayStyle = "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots)) { + displayStyle = "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */; + } + else { + displayStyle = "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */; + } + } + else if (serverData.isDefinedNonNullNonEmpty(media.landscapeScreenshots) && + isProposedAdStyleCompatible("LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */, firstComputedAdStyle)) { + displayStyle = "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && + allowsFourScreenshots(screenshotsDisplayStyle) && + isProposedAdStyleCompatible("PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + if (media.portraitScreenshots.length >= 4) { + displayStyle = "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */; + } + else if (media.portraitScreenshots.length >= 3) { + displayStyle = "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */; + } + else if (media.portraitScreenshots.length >= 2) { + displayStyle = "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */; + } + else { + displayStyle = "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */; + } + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && + isProposedAdStyleCompatible("PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + if (media.portraitScreenshots.length >= 3) { + displayStyle = "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */; + } + else if (media.portraitScreenshots.length >= 2) { + displayStyle = "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */; + } + else { + displayStyle = "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */; + } + } + else if (isProposedAdStyleCompatible("TEXT" /* SearchAdDisplayStyle.TEXT */, firstComputedAdStyle)) { + displayStyle = "TEXT" /* SearchAdDisplayStyle.TEXT */; + } + else { + adLogger(objectGraph, `[${adId}] filtered because we could not create a compatible style for the first style of: ${debugDescriptionForStyle(firstComputedAdStyle)}`); + return null; + } + if (completePortraitMediaCount(objectGraph, screenshotsDisplayStyle) === 2) { + if (displayStyle === "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */) { + displayStyle = "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */; + } + else if (displayStyle === "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */) { + displayStyle = "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */; + } + } + return displayStyle; +} +/** + * Removes media used by the nominated displayStyle. + * Used in the case of a dupe where we need to confirm we have "full creative" for a subsequent presentation of a lockup. + * @param lockup the lockup to modify + * @param displayStyle the display style previously calculated as suitable for the lockup + */ +function removeUsedMediaForAdDisplayStyle(displayStyle, media) { + // In the case of any video assets being used, as per the implementation in `getAdDisplayStyleForMedia`, + // we always pick the first video, so whether it's portrait or landscape just remove the first. + switch (displayStyle) { + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + // We don't want to splice here as we might need these for reuse on dupe ads + if (media.portraitScreenshots.length <= 5) { + media.portraitScreenshots.splice(0, 4); + } + break; + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + media.portraitScreenshots.splice(0, 3); + break; + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + media.portraitScreenshots.splice(0, 2); + break; + case "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */: + media.portraitScreenshots.splice(0, 1); + break; + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + // We will keep the video splice as the video is always first, so it will never be reused between the ad and organic + media.videos.splice(0, 1); + // We don't want to splice here as we might need these for reuse on dupe ads + if (media.portraitScreenshots.length <= 4) { + media.portraitScreenshots.splice(0, 3); + } + break; + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + media.videos.splice(0, 1); + media.portraitScreenshots.splice(0, 2); + break; + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + media.videos.splice(0, 1); + media.portraitScreenshots.splice(0, 1); + break; + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + media.landscapeScreenshots.splice(0, 1); + break; + case "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */: + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + media.videos.splice(0, 1); + break; + default: + break; + } +} +/** + * A function to determine whether a calculated organic search display style is considered "full creative". + * @param displayStyle the display style calculated for an organic dupe result to validate. + * @returns whether the given display style is considered "full creative" for an organic result. + */ +function isDisplayStyleFullCreativeForOrganic(objectGraph, displayStyle, screenshotsDisplayStyle) { + switch (displayStyle) { + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + return true; + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + return completePortraitMediaCount(objectGraph, screenshotsDisplayStyle) === 3; + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + return true; + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + return completePortraitMediaCount(objectGraph) === 2; + default: + return false; + } +} +/** + * Indicates whether the proposed ad style is compatible with a previously computed ad style. + * If there is no previously computed style, we return true as it's assumed the new style is compatible. + * @param proposedAdStyle + * @param firstAdComputedStyle + * @returns a boolean indicating whether the styles are compatible. + */ +function isProposedAdStyleCompatible(proposedAdStyle, firstComputedAdStyle) { + if (serverData.isNull(firstComputedAdStyle)) { + return true; + } + let areStylesCompatible = true; + switch (proposedAdStyle) { + case "TEXT" /* SearchAdDisplayStyle.TEXT */: + areStylesCompatible = firstComputedAdStyle.style === "TEXT" /* SearchAdDisplayStyle.TEXT */; + break; + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + areStylesCompatible = + firstComputedAdStyle.style === "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */ || + firstComputedAdStyle.style === "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */; + break; + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + case "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */: + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + case "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */: + areStylesCompatible = + firstComputedAdStyle.style === "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */ || + firstComputedAdStyle.style === "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */ || + firstComputedAdStyle.style === "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */ || + firstComputedAdStyle.style === "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */ || + firstComputedAdStyle.style === "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */ || + firstComputedAdStyle.style === "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */ || + firstComputedAdStyle.style === "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */ || + firstComputedAdStyle.style === "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */; + break; + case "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */: + areStylesCompatible = firstComputedAdStyle.style === "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */; + break; + default: + areStylesCompatible = false; + } + return areStylesCompatible; +} +/** + * Extract the best media for the platform from the lockup. + * @param lockup a lockup created from which we should extract the media. + * @returns the best media for the platform. + */ +function platformMediaForLockup(lockup) { + let mediaPlatformUsedForDisplayStyle = null; + // This works on the assumption that when a lockup is created screenshots and trailers are sorted + // and the first in the array of each is what we attempt to display. + const firstTrailer = lockup.trailers[0]; + let videos = null; + if (serverData.isDefinedNonNullNonEmpty(firstTrailer)) { + firstTrailer.videos.sort((a, b) => { + return artworkSortingFunction(a.preview, b.preview); + }); + videos = firstTrailer.videos; + mediaPlatformUsedForDisplayStyle = firstTrailer.mediaPlatform; + } + // Grab the first array of platform screenshots, the one the search ad will display. + // Additional elements in this array might be for other platforms we wouldn't + // display (e.g. if current device is an iPhone then [1] might be iPad screenshots) + const platformScreenshots = lockup.screenshots[0]; + // Split the screenshots into portrait and landscape arrays. + // Landscape is preferred, so we use those first before portrait images, but we also need to know + // of any portrait screenshots so we can fill in any space left after a portrait video, which is + // preferred over a landscape screenshot. + const portraitScreenshots = []; + const landscapeScreenshots = []; + if (serverData.isDefinedNonNullNonEmpty(platformScreenshots)) { + platformScreenshots.artwork.forEach((artwork) => { + if (artwork.isPortrait()) { + portraitScreenshots.push(artwork); + } + else { + landscapeScreenshots.push(artwork); + } + }); + mediaPlatformUsedForDisplayStyle = platformScreenshots.mediaPlatform; + } + return { + portraitScreenshots: portraitScreenshots, + landscapeScreenshots: landscapeScreenshots, + alignedRegionArtwork: lockup.alignedRegionArtwork, + alignedRegionVideo: lockup.alignedRegionVideo, + videos: videos, + mediaPlatformUsedForDisplayStyle: mediaPlatformUsedForDisplayStyle, + }; +} +// endregion +function iadAttributesForType(data, type) { + let iAdDictionaryToUse = null; + const iAdOptions = mediaAttributes.attributeAsDictionary(data, "iads"); + const iAdJSONStringToUse = serverData.asString(iAdOptions, type); + if (iAdJSONStringToUse && iAdJSONStringToUse.length) { + iAdDictionaryToUse = JSON.parse(iAdJSONStringToUse); + } + return iAdDictionaryToUse; +} +function iAdDataTypeForAdvert(firstSearchResult, isDupe) { + if (serverData.isNullOrEmpty(firstSearchResult)) { + return "NOORGANIC" /* iAdDataType.NO_ORGANIC_RESULTS */; + } + if (isDupe) { + return "DUP" /* iAdDataType.DUPE_AD */; + } + return "NORMAL" /* iAdDataType.NORMAL */; +} +/** + * Removes unused media from the provided ad lockup, matching the selected ad display style. + * @param ad the ad lockup from which media should be removed. + * @param searchAdDisplayStyle the selected `SearchAdDisplayStyle` to match. + * @param isDupe if the given ad is a dupe of the first organic search result. + * @param isFirstAd if the given ad is the first search ad. + * @returns + */ +function modifyLockupToMatchAdDisplayStyle(ad, searchAdDisplayStyle, isDupe, isFirstAd) { + var _a, _b; + // Derive whether the ad lockup has a CPP (custom product page/ppid) that's being used for it's assets. + const hasCPP = serverData.isDefinedNonNullNonEmpty((_b = (_a = ad.impressionMetrics) === null || _a === void 0 ? void 0 : _a.fields) === null || _b === void 0 ? void 0 : _b.pageCustomId); + // If this ad lockup is a duplicate of the first organic search result and is not the first received ad, + // the organic search result gets the "first choice" of media. This flag indicates whether we should keep + // the "first set" or "second set" of media for this ad lockup. + // This logic does *not* apply if the ad is using a CPP. This is because the CPP from an ad only gets applied + // to an organic dupe result where the matching ad is in the first position. If not, the ad and organic are + // using a different set of assets, so there's no need to prefer one with the "first" or "second" set of media. + const wantsSecondSetOfMedia = isDupe && !isFirstAd && !hasCPP; + if (serverData.isDefinedNonNullNonEmpty(ad.trailers)) { + const firstTrailers = ad.trailers.shift(); + firstTrailers.videos.sort((a, b) => { + return artworkSortingFunction(a.preview, b.preview); + }); + ad.trailers.unshift(firstTrailers); + } + // Split the screenshots into portrait and landscape arrays. + // When removing screenshots from the array below, we can't just assume the first ones match the orientation + // we're using. For example, we only use portrait images with a portait video and some landscape images could + // be mixed in. + let mediaPlatformUsedForDisplayStyle; + let portraitScreenshots = []; + let landscapeScreenshots = []; + if (serverData.isDefinedNonNullNonEmpty(ad.screenshots)) { + const firstScreenshots = ad.screenshots.shift(); + firstScreenshots.artwork.forEach((artwork) => { + if (artwork.isPortrait()) { + portraitScreenshots.push(artwork); + } + else { + landscapeScreenshots.push(artwork); + } + }); + mediaPlatformUsedForDisplayStyle = firstScreenshots.mediaPlatform; + } + switch (searchAdDisplayStyle.style) { + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + // With 4 portrait assets, we need to make sure that the ads and organic can be full by borrowing some assets from the organic. + // However, per product, if the app has 5 or less portrait assets, then it will go to the text ad as today. + // So we need to check for 5 assets as the lower bound (exclusive) and 8 (exclusive) as the upper for + // the extra work to reuse assets. + ad.trailers = null; + landscapeScreenshots = null; + ad.screenshotsDisplayStyle = "four-screenshots"; + if (wantsSecondSetOfMedia) { + if (portraitScreenshots.length > 5 && portraitScreenshots.length < 8) { + const usedAssets = portraitScreenshots.splice(0, 4); + const reuseCount = 4 - portraitScreenshots.length; + const reuseAssets = usedAssets.splice(usedAssets.length - reuseCount); + portraitScreenshots.unshift(...reuseAssets); + } + else { + portraitScreenshots.splice(0, 4); + } + } + else { + portraitScreenshots.splice(4); + } + break; + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + ad.trailers = null; + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + portraitScreenshots.splice(0, 3); + } + else { + portraitScreenshots.splice(3); + } + break; + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + ad.trailers = null; + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + portraitScreenshots.splice(0, 2); + } + else { + portraitScreenshots.splice(2); + } + break; + case "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + ad.trailers = null; + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + portraitScreenshots.splice(0, 1); + } + else { + portraitScreenshots.splice(1); + } + break; + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + // We've previously determined advert can support this type. Remove videos and portrait images so + // advert only displays landscape images. + ad.trailers = null; + portraitScreenshots = null; + if (wantsSecondSetOfMedia) { + landscapeScreenshots.splice(0, 1); + } + else { + landscapeScreenshots.splice(1); + } + break; + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + // We've previously determined advert can support this type. Remove landscape images so + // advert only displays portrait assets. + // With 4 portrait assets, we need to make sure that the ads and organic can be full by borrowing some assets from the organic. + // However, per product, if the app has 5 or less portrait assets, then it will go to the text ad as today. + // Since this style includes a portrait video, we only need to check for 4 assets (exclusive) as the lower bound and 7 (exclusive) as the upper for + // the extra work to reuse assets + landscapeScreenshots = null; + ad.screenshotsDisplayStyle = "four-screenshots"; + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + if (portraitScreenshots.length > 4 && portraitScreenshots.length < 7) { + const usedAssets = portraitScreenshots.splice(0, 3); + const reuseCount = 3 - portraitScreenshots.length; + const reuseAssets = usedAssets.splice(usedAssets.length - reuseCount); + portraitScreenshots.unshift(...reuseAssets); + } + else { + portraitScreenshots.splice(0, 3); + } + } + else { + ad.trailers[0].videos.splice(1); + portraitScreenshots.splice(3); + } + break; + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + // We've previously determined advert can support this type. Remove landscape images so + // advert only displays portrait assets. + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + portraitScreenshots.splice(0, 2); + } + else { + ad.trailers[0].videos.splice(1); + portraitScreenshots.splice(2); + } + break; + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + // We've previously determined advert can support this type. Remove landscape images so + // advert only displays portrait assets. + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + portraitScreenshots.splice(0, 1); + } + else { + ad.trailers[0].videos.splice(1); + portraitScreenshots.splice(1); + } + break; + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + case "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */: + // We've determined advert can support this type. Remove images so only the single video displays + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + } + else { + ad.trailers[0].videos.splice(1); + } + landscapeScreenshots = null; + portraitScreenshots = null; + break; + case "TEXT" /* SearchAdDisplayStyle.TEXT */: + // All adverts can support text only, remove all screenshots and trailers so they display in this style + ad.trailers = null; + landscapeScreenshots = null; + portraitScreenshots = null; + break; + default: + break; + } + if (serverData.isDefinedNonNullNonEmpty(ad.trailers)) { + const firstTrailers = ad.trailers.shift(); + ad.trailers = [firstTrailers]; + } + // Combine the remaining landscape and portrait screenshots, and set them back on the ad as the only values. + const allScreenshots = [].concat(...[landscapeScreenshots, portraitScreenshots].filter(serverData.isDefinedNonNull)); + if (serverData.isDefinedNonNullNonEmpty(allScreenshots)) { + const screenshots = new models.Screenshots(allScreenshots, mediaPlatformUsedForDisplayStyle); + ad.screenshots = [screenshots]; + } + else { + ad.screenshots = null; + } + return ad; +} +function completePortraitMediaCount(objectGraph, searchExperimentsData = null) { + if (objectGraph.client.isPhone) { + return allowsFourScreenshots(searchExperimentsData) ? 4 : 3; + } + else { + return 2; + } +} +function debugDescriptionForStyle(styleContainer) { + if (serverData.isNullOrEmpty(styleContainer)) { + return ""; + } + let mediaTypeString = ""; + if (styleContainer && styleContainer.mediaPlatform) { + mediaTypeString = ` derived from ${styleContainer.mediaPlatform.mediaType} media`; + } + return `${styleContainer.style}${mediaTypeString}`; +} +/** + * Stores a value representing the current setting of the native ad debug logging. + * This starts as `null`, and the first time we log we ask native to provide the current value. + * Requires a re-bootstrap to update, so we avoid sending a message back to native on every call + * if logging is disabled. + */ +let isNativeAdLoggingEnabled = null; +export function adLogger(objectGraph, message) { + objectGraph.console.log(`[Ads] ${message}`); + if (objectGraph.client.buildType === "internal" && + objectGraph.isAvailable(ads) && + serverData.isDefinedNonNull(objectGraph.ads.debugLog)) { + // If we haven't asked native for whether the debug setting for ad logging is enabled, do it now. + if (serverData.isNull(isNativeAdLoggingEnabled) && + serverData.isDefinedNonNull(objectGraph.ads.isNativeAdLoggingEnabled)) { + isNativeAdLoggingEnabled = objectGraph.ads.isNativeAdLoggingEnabled(); + } + if (isNativeAdLoggingEnabled) { + objectGraph.ads.debugLog(message); + } + } +} +const artworkSortingFunction = (a, b) => { + const aLandscape = a.isLandscape(); + const bLandscape = b.isLandscape(); + if (aLandscape === bLandscape) { + return 0; + } + if (aLandscape) { + return -1; + } + return 1; +}; +/** + * A wrapper around the ad search result to include the ad's displayStyle it will present with + */ +export class SearchAdsDisplayStyleResultContainer { +} +/** + * Removes any media used in a provided ad search result from the organic search result. + * @param objectGraph the Object Graph + * @param adResult the ad search result + * @param searchResult the organic search result + * @param searchExperimentsData the metadata for search result experiemnts + * @param searchAdDisplayStyle the display style for the ad that is matching with the organic + */ +export function dedupeAdMediaFromMatchingResult(objectGraph, adResult, searchResult, searchExperimentsData, searchAdDisplayStyle) { + var _a; + // Run de-duping on `AppSearchResult`s and `AppEventSearchResult`s. + // We specifically run this for `AppEventSearchResult`s because they can be presented in the search results + // as either a regular search result (ie. MixedMediaLockup) (if the app isn't installed), or as an IAE search + // result (if it is installed). This means we need to run de-duping in case it appears as a regular result, + // matching behaviour of the `AppSearchResult`. + if (!(searchResult instanceof models.AppSearchResult || searchResult instanceof models.AppEventSearchResult)) { + return; + } + const searchLockup = searchResult.lockup; + const adLockup = adResult.lockups[0]; + if (adLockup.adamId !== searchLockup.adamId) { + return; + } + const usedTemplateUrls = new Set(); + if (serverData.isDefinedNonNullNonEmpty(adLockup.screenshots)) { + for (const screenshot of adLockup.screenshots[0].artwork) { + usedTemplateUrls.add(screenshot.template); + } + } + if (serverData.isDefinedNonNullNonEmpty(adLockup.trailers)) { + for (const video of adLockup.trailers[0].videos) { + usedTemplateUrls.add(video.preview.template); + } + } + if (serverData.isDefinedNonNullNonEmpty(searchLockup.screenshots)) { + const filteredArtwork = searchLockup.screenshots[0].artwork.filter((artwork) => { + return !usedTemplateUrls.has(artwork.template); + }); + searchLockup.screenshots[0] = new models.Screenshots(filteredArtwork, searchLockup.screenshots[0].mediaPlatform); + } + if (serverData.isDefinedNonNullNonEmpty(searchLockup.trailers)) { + const filteredVideos = searchLockup.trailers[0].videos.filter((video) => { + return !usedTemplateUrls.has(video.preview.template); + }); + searchLockup.trailers[0] = new models.Trailers(filteredVideos, searchLockup.trailers[0].mediaPlatform); + } + /// Exit early if we don't need to do anything special for the 4 screenshots case + if (((_a = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _a === void 0 ? void 0 : _a.screenshots) !== "four-screenshots") { + return; + } + // We need to possibly reuse some of the screenshots from the ad to make sure the organic has enough screenshots to show; + // This is only if we will show 4 portrait assets + const padSearchLockupScreenshotsToPreferredSize = (totalRequiredArtwork) => { + const currentScreenshots = searchLockup.screenshots[0].artwork; + if (currentScreenshots.length >= totalRequiredArtwork) { + return; + } + let screenshotsStillNeededCount = totalRequiredArtwork - currentScreenshots.length; + const adArtworksToReuse = adLockup.screenshots[0].artwork.slice().reverse(); + for (const artwork of adArtworksToReuse) { + if (screenshotsStillNeededCount <= 0) { + return; + } + searchLockup.screenshots[0].artwork.unshift(artwork); + screenshotsStillNeededCount -= 1; + } + }; + switch (searchAdDisplayStyle) { + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + padSearchLockupScreenshotsToPreferredSize(4); + break; + default: + break; + } +} +/** + * As part of the CPP implementation for Search Results ads, if the first ad result has a CPP applied, and there is a dupe scenario + * (ie. the first ad matches the first organic result) the CPP must also be applied to the first organic result. + * Here we update the CPP data on the organic result to match the first ad result, if applicable. + * @param objectGraph The object graph + * @param adData The raw ad data used to build the ad results. + * @param adResult The built ad result. + * @param searchResultData The data for the first organic search result. + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past + * @param metricsOptions Metrics options for built models. + * @param personalizationDataContainer The data container to use for personalizing the data. + */ +export function updateDupeOrganicResultCPPData(objectGraph, adData, adResult, searchResultData, installStates, appStates, metricsOptions, personalizationDataContainer) { + var _a, _b; + const adLockup = adResult.lockups[0]; + if (adLockup.adamId !== searchResultData.id) { + return; + } + // Get the data for the first ad lockup. This *should* just be the first, but + // it's possible the first data got dropped when building the ad lockups. + const dataForAdLockup = adData.find((data) => data.id === adLockup.adamId); + const shouldSkipUpdatingOrganicCPP = searchResultWillUseAppEventDisplay(objectGraph, searchResultData, installStates, appStates, metricsOptions, personalizationDataContainer); + if (shouldSkipUpdatingOrganicCPP) { + return; + } + // The first ad lockup matches the first organic result. Apply the Ad cppId to matching organic result. + updatePPIDInData((_b = (_a = dataForAdLockup === null || dataForAdLockup === void 0 ? void 0 : dataForAdLockup.meta) === null || _a === void 0 ? void 0 : _a.cppData) === null || _b === void 0 ? void 0 : _b["ppid"], searchResultData); +} +/** + * Update the ppid for the given data. + * If provided `ppid` is `null`, the field will be deleted. + * If `cppData` does not already exist on `meta`, it will be created. + * @param ppid The ppid to update in the data. Can be null, which will delete the field. + * @param data The data object to update. + */ +function updatePPIDInData(ppid, data) { + var _a; + let meta = shallowCopyOf(data.meta); + if (serverData.isNull(ppid)) { + (_a = meta === null || meta === void 0 ? void 0 : meta.cppData) === null || _a === void 0 ? true : delete _a["ppid"]; + } + else { + if (serverData.isNull(meta)) { + meta = {}; + } + if (serverData.isNull(meta.cppData)) { + meta.cppData = {}; + } + meta.cppData["ppid"] = ppid; + } + data.meta = meta; +} +/** + * Whether or not platform supports adverts. + */ +export function platformSupportsAdverts(objectGraph) { + return ((clientIdentifierSupportsAdverts(objectGraph) && objectGraph.host.isiOS) || + objectGraph.host.platform === "unknown"); +} +function clientIdentifierSupportsAdverts(objectGraph) { + return (objectGraph.host.clientIdentifier === client.appStoreIdentifier || + objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier); +} +//# sourceMappingURL=search-ads.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js new file mode 100644 index 0000000..94e7438 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js @@ -0,0 +1,59 @@ +/** + * Common operations for builders in search tab. + */ +// region Search Term State +/** + * Build the `SearchTermContext` for a given response triggered by a search for `term`, possibly originating from `originatingTerm` + * @param requestDescriptor The options that describe fetch request. + * @param searchResponse The sequential response that was returned. + */ +export function createTermContextForSpellcheckedSequentialResponse(objectGraph, requestDescriptor, searchResponse) { + var _a, _b, _c, _d, _e, _f, _g; + return { + term: requestDescriptor.term, + suggestedTerm: (_b = (_a = searchResponse.results) === null || _a === void 0 ? void 0 : _a.spellCheck) === null || _b === void 0 ? void 0 : _b.suggestedTerm, + correctedTerm: (_d = (_c = searchResponse.results) === null || _c === void 0 ? void 0 : _c.spellCheck) === null || _d === void 0 ? void 0 : _d.correctedTerm, + resultsTerm: (_g = (_f = (_e = searchResponse.results) === null || _e === void 0 ? void 0 : _e.spellCheck) === null || _f === void 0 ? void 0 : _f.correctedTerm) !== null && _g !== void 0 ? _g : requestDescriptor.term, + originatingTerm: requestDescriptor.originatingTerm, + }; +} +/** + * Create a search term context for the segmented search results completed fetch + * @param objectGraph The app store object graph + * @param requestDescriptor The search request descriptor + * @param searchResponse The response for the segmented search results page + * @returns The search term context for the segmented search results fetch + */ +export function createTermContextForSpellcheckedGroupedResponse(objectGraph, requestDescriptor, searchResponse) { + var _a, _b, _c, _d, _e, _f, _g; + return { + term: requestDescriptor.term, + suggestedTerm: (_b = (_a = searchResponse.results) === null || _a === void 0 ? void 0 : _a.spellCheck) === null || _b === void 0 ? void 0 : _b.suggestedTerm, + correctedTerm: (_d = (_c = searchResponse.results) === null || _c === void 0 ? void 0 : _c.spellCheck) === null || _d === void 0 ? void 0 : _d.correctedTerm, + resultsTerm: (_g = (_f = (_e = searchResponse.results) === null || _e === void 0 ? void 0 : _e.spellCheck) === null || _f === void 0 ? void 0 : _f.correctedTerm) !== null && _g !== void 0 ? _g : requestDescriptor.term, + originatingTerm: requestDescriptor.originatingTerm, + }; +} +/** + * Build the `SearchTermContext` purely from `requestDescriptor` for requests that don't have spellchecking. + * @param requestDescriptor The options that describe fetch request. + */ +export function createTermContextForNonspellcheckRequest(objectGraph, requestDescriptor) { + return { + term: requestDescriptor.term, + resultsTerm: requestDescriptor.term, + originatingTerm: requestDescriptor.originatingTerm, + }; +} +// endregion +// region Constants +/** + * The field name desired within the `meta.metrics` in search response. + */ +export const searchMetricsDataSetID = "data.search.dataSetId"; +/** + * The actual field name within the `meta.metrics` in search response. + */ +export const legacySearchMetricsDataSetID = "dataSetId"; +// endregion +//# sourceMappingURL=search-common.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js new file mode 100644 index 0000000..dd0ccf4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js @@ -0,0 +1,146 @@ +/** + * Build methods for Search Facets. + */ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { categoryListFromApiResponse } from "../categories"; +import * as metricsClickHelpers from "../metrics/helpers/clicks"; +/** + * Create Search Facets. Theres platform specific variations here. + * @param requestFacets Facets in current request. + * @param categoryFacetsData Additional + */ +export function createSearchFacets(objectGraph, requestFacets, categoryFacetsData) { + const selectedFacets = requestFacets || {}; + const facets = []; + // Device Type + if (objectGraph.client.deviceType !== "mac") { + facets.push(new models.SearchFacetSet("targetPlatform", [ + new models.SearchFacetValue(objectGraph.loc.string("Search.Facets.iPadAndIPhone"), null, selectedFacets["targetPlatform"]), + new models.SearchFacetValue(objectGraph.loc.string("Search.Facets.iPhoneOnly"), "iphone", selectedFacets["targetPlatform"]), + ])); + } + // Price + facets.push(new models.SearchFacetSet("price", [ + new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_ANY_PRICE", "Any"), null, selectedFacets["price"]), + new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_FREE", "Any"), "free", selectedFacets["price"]), + ])); + // Categories + const categoryList = categoryListFromApiResponse(objectGraph, categoryFacetsData, false); + if (categoryList) { + const serverCategories = categoryList.categories; + if (serverCategories.length) { + const genreFacetValues = serverCategories + .filter((category) => { + return serverData.isDefinedNonNull(category.genreId); + }) + .map((category) => { + return new models.SearchFacetValue(category.name, category.genreId, selectedFacets["genre"]); + }); + genreFacetValues.unshift(new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_ANY_CATEGORY", "Any"), null, selectedFacets["genre"])); + facets.push(new models.SearchFacetSet("genre", genreFacetValues)); + } + } + const searchSortOptions = objectGraph.bag.searchSortOptions; + // Sorts + const sortFacetValues = []; + sortFacetValues.push(new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_RELEVANCE"), null, selectedFacets["sort"])); + for (const sortValue of searchSortOptions) { + sortFacetValues.push(new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_" + sortValue), sortValue, selectedFacets["sort"])); + } + if (sortFacetValues.length > 1) { + facets.push(new models.SearchFacetSet("sort", sortFacetValues)); + } + const serverAgeBands = objectGraph.bag.ageBands; + const ageBandFacetValues = serverAgeBands.map((ageBand) => { + return new models.SearchFacetValue(serverData.asString(ageBand, "name"), serverData.asString(ageBand, "ageBandId"), selectedFacets["ages"]); + }); + if (ageBandFacetValues.length > 0 && objectGraph.client.deviceType !== "mac") { + facets.push(new models.SearchFacetSet("ages", ageBandFacetValues)); + } + return facets; +} +/** + * Create Search Facets. Theres platform specific variations here. + * @param categoryFacetsData Additional + */ +export function createSearchPageFacets(objectGraph, categoryFacetsData) { + let categoryFacet = null; + let sortsFacet = null; + let ageBandsFacet = null; + // Platform + const deviceTypeFacet = new models.PageFacetsFacet("targetPlatform", "targetPlatform", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_DEVICE_TYPE"), "singleSelection", [ + new models.PageFacetOption(objectGraph.loc.string("Search.Facets.iPadAndIPhone"), null), + new models.PageFacetOption(objectGraph.loc.string("Search.Facets.iPhoneOnly"), "iphone"), + ], null, null, pageFacetChangeAction(objectGraph, "targetPlatform")); + // Price + const priceFacet = new models.PageFacetsFacet("filter[price]", "filter[price]", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_PRICE"), "singleSelection", [ + new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_PRICE", "Any"), null), + new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_FREE", "Any"), "free"), + ], null, null, pageFacetChangeAction(objectGraph, "price")); + // Categories + const categoryList = categoryListFromApiResponse(objectGraph, categoryFacetsData, false); + if (categoryList) { + const serverCategories = categoryList.categories; + if (serverCategories.length) { + const categories = serverCategories.filter((category) => { + return serverData.isDefinedNonNull(category.genreId); + }); + categoryFacet = new models.PageFacetsFacet("filter[genre]", "filter[genre]", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_CATEGORY"), "singleSelection", [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_CATEGORY", "Any"), null)], null, null, pageFacetChangeAction(objectGraph, "genre")); + for (const category of categories) { + categoryFacet.options.push(new models.PageFacetOption(category.name, category.genreId)); + } + } + } + // Sorts + const searchSortOptions = objectGraph.bag.searchSortOptions; + sortsFacet = new models.PageFacetsFacet("sort", "sort", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_SORT"), "singleSelection", [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_RELEVANCE"), null)], null, null, pageFacetChangeAction(objectGraph, "sort")); + for (const sortValue of searchSortOptions) { + sortsFacet.options.push(new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_" + sortValue), sortValue)); + } + // Age Bands + const serverAgeBands = objectGraph.bag.ageBands; + const ageBandFacetOptions = serverAgeBands.map((ageBand) => { + return new models.PageFacetOption(serverData.asString(ageBand, "name"), serverData.asString(ageBand, "ageBandId")); + }); + if (ageBandFacetOptions.length > 0 && objectGraph.client.deviceType !== "mac") { + ageBandsFacet = new models.PageFacetsFacet("filter[ages]", "filter[ages]", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_AGE_BAND"), "singleSelection", ageBandFacetOptions, null, null, pageFacetChangeAction(objectGraph, "ages")); + } + const pageFacets = new models.PageFacets([], false, null); + if (objectGraph.client.isMac) { + pageFacets.facetGroups.push(new models.PageFacetsGroup([priceFacet])); + if (serverData.isDefinedNonNull(categoryFacet)) { + pageFacets.facetGroups.push(new models.PageFacetsGroup([categoryFacet])); + } + pageFacets.facetGroups.push(new models.PageFacetsGroup([sortsFacet])); + } + else { + const facets = [deviceTypeFacet, priceFacet]; + if (serverData.isDefinedNonNull(categoryFacet)) { + facets.push(categoryFacet); + } + facets.push(sortsFacet); + if (serverData.isDefinedNonNull(ageBandsFacet)) { + facets.push(ageBandsFacet); + } + for (const facet of facets) { + facet.showsSelectedOptions = true; + } + pageFacets.facetGroups.push(new models.PageFacetsGroup(facets)); + } + return pageFacets; +} +export function createDefaultSelectedFacetOptions(objectGraph) { + return { + "targetPlatform": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_IPAD_ONLY"), null)], + "filter[price]": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_PRICE", "Any"), null)], + "sort": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_RELEVANCE"), null)], + "filter[genre]": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_CATEGORY", "Any"), null)], + }; +} +function pageFacetChangeAction(objectGraph, facetParameter) { + const action = new models.BlankAction(); + metricsClickHelpers.addClickEventToPageFacetsChangeAction(objectGraph, action, facetParameter); + return action; +} +//# sourceMappingURL=search-facets.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js new file mode 100644 index 0000000..f26b78b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js @@ -0,0 +1,386 @@ +import * as validation from "@jet/environment/json/validation"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { PageRefreshPolicy, SearchAction, SearchFocusPage, SearchLandingPage, Shelf } from "../../api/models"; +import * as mediaRequestUtils from "../../common/builders/url-mapping-utils"; +import * as appStoreExperiments from "../../foundation/experimentation/app-store-experiments"; +import { ExperimentAreaId } from "../../foundation/experimentation/experiment-area-id"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; +import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; +import { iadInfoFromOnDeviceAdResponse, isAdPlacementEnabled } from "../ads/ad-common"; +import * as adIncidents from "../ads/ad-incident-recorder"; +import { fetchAds as landingAdFetchFetchAds } from "../ads/on-device-ad-fetch"; +import * as landingAdStitch from "../ads/on-device-ad-stitch"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as groupingCommon from "../grouping/grouping-common"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as metricsHelpersUtil from "../metrics/helpers/util"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import * as productPageVariants from "../product-page/product-page-variants"; +import { areAppTagsEnabled } from "../util/app-tags-util"; +import { isFeatureEnabledForCurrentUser } from "../util/lottery"; +import { SearchPageType } from "./content/search-shelves"; +import * as searchLandingCohort from "./landing/search-landing-cohort"; +import * as searchLandingShelfController from "./landing/search-landing-shelf-controller"; +/** + * Determines whether or not the user will use the legacy Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @returns Whether or not the user will use the legacy Search Landing Page protocol + */ +function shouldUseProtocolV1(objectGraph) { + if (objectGraph.client.isVision) { + return false; // visionOS always uses the modern V2 protocol. + } + if (objectGraph.client.isWeb) { + return false; // the "web" expects to use the V2 protocol as well + } + if (!objectGraph.bag.supportsSearchLandingPageV2) { + return true; // Use V1 protocol when V2 is unsupported + } + // Use V1 protocol based on the bags rollout rate for V2 protocol. + return !isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.searchLandingPageV2RolloutRate); +} +async function fetchSearchLandingPage(objectGraph, fetchAds) { + if (shouldUseProtocolV1(objectGraph)) { + return await fetchSearchLandingPageV1(objectGraph, fetchAds); + } + if (objectGraph.bag.mediaAPISearchFocusEnabled) { + return await fetchSearchLandingPageV2WithFocusPage(objectGraph, fetchAds); + } + return await fetchSearchLandingPageV2(objectGraph, fetchAds); +} +async function fetchSearchLandingPageV1(objectGraph, fetchAds) { + const searchLandingRequest = new mediaDataFetching.Request(objectGraph) + .forType("landing") + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); // for `extend=customArtwork` + searchLandingRequest.targetResourceType = "groupings"; + const cohortIdOrNil = searchLandingCohort.cohortIdForUser(objectGraph, objectGraph.user.dsid); + if ((cohortIdOrNil === null || cohortIdOrNil === void 0 ? void 0 : cohortIdOrNil.length) > 0) { + searchLandingRequest.addingQuery("clusterId", cohortIdOrNil); + } + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + return await Promise.all([fetchSearchLanding, fetchAds]).then(([responseData, adResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + return landingPageFromResponseV1(modifiedObjectGraph, responseData, adResponse); + }); + }); +} +/** + * Creates `SearchLandingPage` model from the V1 Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the fetch + * @param adResponse The response from the ad fetch + * @returns A `SearchLandingPage` model from the V1 Search Landing Page protocol + */ +function landingPageFromResponseV1(objectGraph, landingPageResponse, adResponse) { + const mediaApiGroupingDataArray = serverData.asArrayOrEmpty(landingPageResponse, "results.contents"); + const mediaApiGroupingData = mediaApiGroupingDataArray[0]; + if (serverData.isNullOrEmpty(mediaApiGroupingData)) { + return null; + } + if (!mediaRelationship.hasRelationship(mediaApiGroupingData, "tabs")) { + return null; + } + const groupingGenreAdamId = mediaAttributes.attributeAsString(mediaApiGroupingData, "id"); + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "Genre", mediaApiGroupingData.id, landingPageResponse); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + const groupingParseContext = { + shelves: [], + metricsPageInformation: pageInformation, + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + pageGenreAdamId: groupingGenreAdamId, + pageGenreId: mediaAttributes.attributeAsNumber(mediaApiGroupingData, "genre"), + hasAuthenticatedUser: serverData.isDefinedNonNull(objectGraph.user.dsid), + isSearchLandingPage: true, + adStitcher: landingAdStitch.adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse), + adIncidentRecorder: adIncidentRecorder, + }; + const flattenedGrouping = groupingCommon.flattenMediaApiGroupingData(objectGraph, mediaApiGroupingData); + groupingCommon.insertInitialShelvesIntoGroupingParseContext(objectGraph, flattenedGrouping, groupingParseContext); + const page = new SearchLandingPage(groupingParseContext.shelves); + // Page refresh + const refreshPolicy = new PageRefreshPolicy("timeSinceOnScreen", objectGraph.bag.searchLandingPageRefreshUpdateDelayInterval, objectGraph.bag.searchLandingPageOffscreenRefreshInterval, null); + page.pageRefreshPolicy = refreshPolicy; + // Ad Incidents + page.adIncidents = adIncidents.recordedIncidents(objectGraph, groupingParseContext.adIncidentRecorder); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, groupingParseContext.metricsPageInformation); + return page; +} +function makeSearchLandingRequestV2(objectGraph, fetchAds) { + const searchLandingRequest = new mediaDataFetching.Request(objectGraph) + .forType("landing:new-protocol") + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) // for `extend=customArtwork` + .includingScopedRelationships("search-recommendations", ["contents"]) + .addingQuery("name", "search-landing"); + if (areAppTagsEnabled(objectGraph, "slp")) { + mediaRequestUtils.configureTagsForMediaRequest(searchLandingRequest); + } + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + searchLandingRequest.includingScopedAttributes("editorial-items", ["editorialClientParams"]); + } + const cohortIdOrNil = searchLandingCohort.cohortIdForUser(objectGraph, objectGraph.user.dsid); + if ((cohortIdOrNil === null || cohortIdOrNil === void 0 ? void 0 : cohortIdOrNil.length) > 0) { + searchLandingRequest.addingQuery("clusterId", cohortIdOrNil); + } + if (objectGraph.client.isiOS) { + searchLandingRequest.addingQuery("meta", "adDisplayStyle"); + } + return searchLandingRequest; +} +async function fetchSearchLandingPageV2(objectGraph, fetchAds) { + const searchLandingRequest = makeSearchLandingRequestV2(objectGraph, fetchAds); + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + const amsEngagement = objectGraph.amsEngagement; + let amdPromise; + if (amsEngagement && objectGraph.bag.enableRecoOnDeviceReordering) { + const request = { + timeout: 500, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "search", + }; + amdPromise = amsEngagement.performRequest(request); + } + return await Promise.all([fetchSearchLanding, fetchAds, amdPromise]).then(([responseData, adResponse, amdResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + return landingPageFromResponseV2(modifiedObjectGraph, responseData, adResponse, amdResponse); + }); + }); +} +/** + * Creates `SearchLandingPage` model from the V2 Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the fetch + * @param adResponse The response from the ad fetch + * @returns A `SearchLandingPage` model from the V2 Search Landing Page protocol + */ +function landingPageFromResponseV2(objectGraph, landingPageResponse, adResponse, impressionData) { + if (serverData.isNullOrEmpty(landingPageResponse.data)) { + return null; + } + // Creates the page info + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "SearchLanding", "SearchLanding", landingPageResponse); + // Decorate page info with personalization metrics + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + // Creates Search Landing Page Context + const landingPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + adStitcher: landingAdStitch.adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse, landingPageResponse), + adIncidentRecorder: adIncidentRecorder, + pageType: SearchPageType.Landing, + recoImpressionData: impressionDemotion.impressionEventsFromData(objectGraph, impressionData), + }; + // Create the shelves for the page + searchLandingShelfController.insertShelvesIntoSearchPageContext(objectGraph, landingPageResponse, landingPageContext); + // Add Unified Messaging placement to top of page for NLS BT. + const bubbleTipShelf = createNaturalLanguageSearchBubbleTipShelf(objectGraph); + if (bubbleTipShelf) { + landingPageContext.shelves.unshift(bubbleTipShelf); + } + const landingPage = new SearchLandingPage(landingPageContext.shelves); + // Page refresh + landingPage.pageRefreshPolicy = new PageRefreshPolicy("timeSinceOnScreen", objectGraph.bag.searchLandingPageRefreshUpdateDelayInterval, objectGraph.bag.searchLandingPageOffscreenRefreshInterval, null); + // Ad Incidents + landingPage.adIncidents = adIncidents.recordedIncidents(objectGraph, landingPageContext.adIncidentRecorder); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, landingPage, landingPageContext.metricsPageInformation); + return landingPage; +} +/** + * Creates the NLS BT shelf for SLP if enabled. + * @param objectGraph The app store object graph. + * @returns The shelf for the NLS BT shown on SLP, or undefined if bag has feature disabled. + */ +export function createNaturalLanguageSearchBubbleTipShelf(objectGraph) { + var _a; + if (!objectGraph.bag.isNaturalLanguageSearchEnabled && !objectGraph.bag.isNaturalLanguageSearchResultsEnabled) { + return undefined; // feature not enabled in the bag + } + const context = { + signal: { + lastNLSQueryDate: objectGraph.storage.retrieveString("lastNLSQueryDate"), + treatmentId: (_a = appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.SearchLandingPage)) !== null && _a !== void 0 ? _a : null, + }, + }; + const shelf = groupingCommon.shelfForUnifiedMessage(objectGraph, "searchFocusHeader", context, "pullOnly"); + shelf.refreshUrl = `${Protocol.internal}:/${Path.searchLandingPage}/${Path.shelf}/?${Parameters.isSearchFocusHeaderShelf}=true`; + return shelf; +} +async function fetchSearchLandingPageV2WithFocusPage(objectGraph, fetchAds) { + const searchLandingRequest = makeSearchLandingRequestV2(objectGraph, fetchAds).enablingFeature("search-focus-suggestions"); + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + const amsEngagement = objectGraph.amsEngagement; + let amdPromise = null; + if (amsEngagement && objectGraph.bag.enableRecoOnDeviceReordering) { + const request = { + timeout: 500, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "search", + }; + amdPromise = amsEngagement.performRequest(request); + } + return await Promise.all([fetchSearchLanding, fetchAds, amdPromise]).then(async ([responseData, adResponse, amdResponse]) => { + return await fetchTimingMetricsBuilder.measureModelConstructionAsync(async () => await landingPageFromResponseV2WithFocusPage(modifiedObjectGraph, responseData, adResponse, amdResponse)); + }); +} +/** + * Creates `SearchLandingPage` model from the V2 Search Landing Page protocol, but with search-focus feature enabled. + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the landing fetch + * @param landingAdResponse The response from the landingAd fetch + * @returns A `SearchLandingPage` model created from server response. + */ +async function landingPageFromResponseV2WithFocusPage(objectGraph, landingPageResponse, landingAdResponse, impressionData) { + // MAINTAINER'S NOTE: V3 protocol does not change any existing SLP v2 fields and is purely additives for focus page support + const landingPage = landingPageFromResponseV2(objectGraph, landingPageResponse, landingAdResponse, impressionData); + // Create Search Focus Page + return await fetchFocusPageUsingLandingPageResponse(objectGraph, landingPageResponse).then((focusPage) => { + landingPage.searchFocusPage = focusPage; + return landingPage; + }); +} +/** + * Creates `SearchFocusPage` model from the V3 Search Landing Page protocol, fetching search history if needed. + * @param objectGraph The App Store Object Graph + * @param focusPageResponse The response from the fetch + * @returns A `SearchFocusPage` model from the V3 Search Landing Page protocol + */ +async function fetchFocusPageUsingLandingPageResponse(objectGraph, landingPageResponse) { + var _a; + if (serverData.isNullOrEmpty(landingPageResponse.data)) { + return null; + } + // Creates the page info + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "SearchFocus", "Focus", landingPageResponse, " "); + // Decorate page info with personalization metrics + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + // Creates Search Focus Page Context + const focusPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + pageType: SearchPageType.Focus, + }; + const searchHistoryShelfMarker = searchLandingShelfController.firstShelfMarkerMatchingUseCase(landingPageResponse, focusPageContext, "recentSearches"); + // Skip fetching search history if there isn't a marker for it in SLP response. + if (isNothing(searchHistoryShelfMarker)) { + return createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext); + } + const searchHistoryDisplayCount = (_a = mediaAttributes.attributeAsNumber(searchHistoryShelfMarker, "displayCount")) !== null && _a !== void 0 ? _a : 0; + const fetchSearchHistory = objectGraph.onDeviceSearchHistoryManager.fetchRecentsWithLimit(searchHistoryDisplayCount); + return await fetchSearchHistory.then((searchHistory) => { + focusPageContext.searchHistory = searchHistory; + return createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext); + }); +} +function createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext) { + // Create the shelves for the focus page using same logic as landing page, but + // includes search history in page context to support `search-recommendations-marker`. + searchLandingShelfController.insertShelvesIntoSearchPageContext(objectGraph, landingPageResponse, focusPageContext); + const focusPage = new SearchFocusPage(focusPageContext.shelves); + if (serverData.isNullOrEmpty(focusPage.shelves)) { + return null; + } + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, focusPage, focusPageContext.metricsPageInformation); + return focusPage; +} +async function fetchTrendingSearchesFallbackPage(objectGraph, fetchAds) { + const fetchRequest = { + url: objectGraph.bag.trendingSearchesURL, + }; + const trendingSearchesPromise = objectGraph.network.fetch(fetchRequest).then((response) => { + if (!response.ok) { + throw Error(`Bad Status code ${response.status} for ${fetchRequest.url}`); + } + return JSON.parse(response.body); + }); + return await Promise.all([trendingSearchesPromise, fetchAds]).then(([trendingSearchesData, adResponse]) => { + var _a; + const page = new SearchLandingPage(trendingSearchesShelvesForResponse(objectGraph, trendingSearchesData)); + const pageInformation = metricsHelpersPage.fakeMetricsPageInformation(objectGraph, "SearchLanding", "trending", ""); // old trending endpoint doesn't have metrics meta + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "SLPLOAD", "searchLanding"); // trending fallback never displays ad, so is always missed opportunity. + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + return page; + }); +} +/** + * Creates a trending searches shelves from the given JSON response. + * @param objectGraph The App Store Object Graph. + * @param response The API response JSON data. + * @return {Shelf[]} Trending searches shelves created from response. + */ +function trendingSearchesShelvesForResponse(objectGraph, response) { + return validation.context("trendingSearchesShelfForResponse", () => { + const locationTracker = metricsHelpersLocation.newLocationTracker(); + const searches = serverData.asArrayOrEmpty(response, "trendingSearches").map((rawSearch) => { + const term = serverData.asString(rawSearch, "label"); + const searchAction = new SearchAction(term, term, serverData.asString(rawSearch, "url"), "trending"); + if (objectGraph.client.isPhone) { + searchAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + } + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", locationTracker); + metricsHelpersLocation.nextPosition(locationTracker); + return searchAction; + }); + let maxNumberOfSearches = 0; + switch (objectGraph.client.deviceType) { + case "pad": + maxNumberOfSearches = 10; + break; + case "phone": + maxNumberOfSearches = 7; + break; + default: + break; + } + const shelf = new Shelf("action"); + shelf.title = searches.length > 0 ? serverData.asString(response, "header.label") : null; + shelf.isHorizontal = false; + shelf.items = searches.slice(0, maxNumberOfSearches); + return [shelf]; + }); +} +export async function fetchPage(objectGraph) { + const fetchAds = isAdPlacementEnabled(objectGraph, "searchLanding") + ? landingAdFetchFetchAds(objectGraph, "searchLanding").catch(() => null) + : null; + return await fetchSearchLandingPage(objectGraph, fetchAds).catch(async (e) => { + // If the client has provided a `trendingSearchesURL`, we can fallback to that search + // mechanism if the search landing request fails. If `trendingSearchesURL` is not + // provided, we re-throw the original landing page error for the client to handle. + if (isSome(objectGraph.bag.trendingSearchesURL)) { + return await fetchTrendingSearchesFallbackPage(objectGraph, fetchAds); + } + else { + throw e; + } + }); +} +//# sourceMappingURL=search-landing-page-utils.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js new file mode 100644 index 0000000..9462f94 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js @@ -0,0 +1,5 @@ +import { generateRoutes } from "../util/generate-routes"; +import { makeSearchResultsPageIntentFromURLParams } from "../../api/intents/search-results-page-intent"; +const { routes: searchResultsPageRoutes, makeCanonicalUrl: makeCanonicalSearchResultsPageUrl } = generateRoutes(makeSearchResultsPageIntentFromURLParams, "/{platform}/search", ["term"]); +export { searchResultsPageRoutes, makeCanonicalSearchResultsPageUrl }; +//# sourceMappingURL=search-page-url.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js new file mode 100644 index 0000000..f04a8e1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js @@ -0,0 +1,496 @@ +/** + * Data Fetching for Search Results for: + * - Initial search requests + * - Content pagination requests + */ +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import { asBooleanOrFalse, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; +import { defaultAdditionalPlatformsForClient, Request } from "../../foundation/media/data-fetching"; +import { metricsFromMediaApiObject, } from "../../foundation/media/data-structure"; +import { fetchData } from "../../foundation/media/network"; +import { buildURLFromRequest } from "../../foundation/media/url-builder"; +import * as constants from "../../foundation/util/constants"; +import * as client from "../../foundation/wrappers/client"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import * as categories from "../categories"; +import { addVariantParametersToRequestForItems, shouldFetchCustomAttributes, } from "../product-page/product-page-variants"; +import { setPreviewPlatform } from "../preview-platform"; +import { SponsoredSearchRequestData } from "./search-ads"; +import { searchMetricsDataSetID } from "./search-common"; +import { fetchSponsoredSearchNativeAdvertData } from "./sponsored-search-fetching"; +import { dateFlooredToHour } from "../../foundation/util/date-util"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +export async function fetchSegmentedSearchResults(objectGraph, options) { + var _a; + // Primary search request + const searchRequest = (_a = createSearchRequest(objectGraph, options)) === null || _a === void 0 ? void 0 : _a.addingQuery("groupBy[search]", "platform"); + if (isNothing(searchRequest)) { + return null; + } + const fetchSearchResults = fetchData(objectGraph, searchRequest, undefined); + return await fetchSearchResults.then((catalogResponse) => { + renameDataSetIdKey(catalogResponse); + const data = { + catalogResponse: catalogResponse, + requestMetadata: { + requestDescriptor: options, + searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url + }, + categoriesFilterData: null, + sponsoredSearchRequestData: null, + sponsoredSearchAdvertData: null, + }; + return data; + }); +} +/** + * Fetch a collection of data for displaying a single, sequential set of search results. + * @param options Parameters for the search being performed. + * @returns `Promise` for aggregate search result data, or `null` if `options` was invalid. + */ +export async function fetchSequentialSearchResultsData(objectGraph, options) { + // Advert targeting data from client + const sponsoredSearchRequestData = new SponsoredSearchRequestData(options.targetingData, objectGraph.random.nextUUID()); + // Primary search request + const searchRequest = createSearchRequest(objectGraph, options); + if (searchRequest === null) { + return null; + } + const fetchSearchResults = fetchData(objectGraph, searchRequest, createSearchFetchOptions(objectGraph, sponsoredSearchRequestData)); + // Search History + if (objectGraph.bag.mediaAPISearchFocusEnabled) { + const historyItem = { + term: options.term.trim(), + entity: options.searchEntity, + }; + // ** MAINTAINER'S NOTE ** + // Fire and forget this request to reduce delay showing search results on persisting recent searches that are not visible at this time. + objectGraph.onDeviceSearchHistoryManager.saveRecentSearchWithLimit(historyItem, 30); + } + // Optional requests + const fetchSponsoredSearchAds = fetchSponsoredSearchNativeAdvertData(objectGraph, sponsoredSearchRequestData, options.term, fetchSearchResults); + const fetchCategoriesOrNull = fetchCategoryFiltersDataIfNeeded(objectGraph); + return await Promise.all([fetchSearchResults, fetchSponsoredSearchAds, fetchCategoriesOrNull]).then(([catalogResponse, sponsoredSearchAdvertData, categoriesFilterData]) => { + var _a, _b, _c, _d; + renameDataSetIdKey(catalogResponse); + // Remember the last time a natural language search was performed. + if ((_c = (_b = (_a = catalogResponse.meta) === null || _a === void 0 ? void 0 : _a.results) === null || _b === void 0 ? void 0 : _b.search) === null || _c === void 0 ? void 0 : _c.naturalLanguage) { + const previousNLSQueryDate = objectGraph.storage.retrieveString("lastNLSQueryDate"); + const lastNLSQueryDate = dateFlooredToHour(new Date()).getTime().toString(); + objectGraph.storage.storeString("lastNLSQueryDate", lastNLSQueryDate); + // Notify ODJ on change using AMSEngagement. + (_d = objectGraph.amsEngagement) === null || _d === void 0 ? void 0 : _d.enqueueData({ + eventType: "lastNLSQueryDateChange", + app: "com.apple.AppStore", + oldState: previousNLSQueryDate, + newState: lastNLSQueryDate, + }); + } + const data = { + catalogResponse: catalogResponse, + categoriesFilterData: categoriesFilterData, + sponsoredSearchRequestData: sponsoredSearchRequestData, + sponsoredSearchAdvertData: sponsoredSearchAdvertData, + requestMetadata: { + requestDescriptor: options, + searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url + }, + }; + return data; + }); +} +/** + * Fetch a collection of data for displaying several, grouped set of search results. + * @param options Parameters for the search being performed. + * @returns `Promise` for aggregate search result data, or `Promise` of `null` if `options` was invalid. + */ +export async function fetchPlatformGroupedSearchResultsData(objectGraph, options) { + // Primary search request + const searchRequest = createPlatformGroupedSearchRequest(objectGraph, options); + if (isNothing(searchRequest)) { + return null; + } + const fetchSearchResults = fetchData(objectGraph, searchRequest, createSearchFetchOptions(objectGraph)); + // Optional requests + const fetchCategoriesOrNull = fetchCategoryFiltersDataIfNeeded(objectGraph); + return await Promise.all([fetchSearchResults, fetchCategoriesOrNull]).then(([catalogResponse, categoriesFilterData]) => { + renameDataSetIdKey(catalogResponse); + const data = { + catalogResponse: catalogResponse, + categoriesFilterData: categoriesFilterData, + sponsoredSearchRequestData: null, + sponsoredSearchAdvertData: null, + requestMetadata: { + requestDescriptor: options, + searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url + }, + }; + return data; + }); +} +/** + * Fetch a set of items for appearing in search results + * @param items Items to fetch. + */ +export async function fetchSearchResultItems(objectGraph, items) { + const request = createCatalogRequestForItems(objectGraph, items); + return await fetchData(objectGraph, request); +} +// endregion +// region Request Header +/** + * Create fetch options, annotating with `SponsoredSearchRequestData` header fields. + * @param adData Advert data to include in header. + */ +function createSearchFetchOptions(objectGraph, sponsoredSearchRequestData) { + const searchRequestHeader = {}; + if (sponsoredSearchRequestData && sponsoredSearchRequestData.validAdRequest()) { + searchRequestHeader[constants.appStoreClientRequestIdName] = sponsoredSearchRequestData.appStoreClientRequestId; + searchRequestHeader[constants.iAdRequestDataHeaderName] = sponsoredSearchRequestData.sponsoredSearchRequestData; + searchRequestHeader[constants.iAdRoutingInfoHeaderName] = sponsoredSearchRequestData.routingInfo; + } + return { + headers: searchRequestHeader, + }; +} +// endregion +// region Search Results Request +/** + * Set of relationships fetched as relation for search request / catalog requests + */ +const searchIncludeRelationships = ["apps", "top-apps"]; +/** + * Create the `Request` object to execute a search described by `options` + * @param options Options for search being executed. + * @returns `Request` configured for `options`, or `null` if `options` was invalid. + */ +export function createSearchRequest(objectGraph, options) { + var _a; + const term = (_a = options.term) === null || _a === void 0 ? void 0 : _a.trim(); + if (isNullOrEmpty(term)) { + return null; + } + const origin = options.origin; + const source = options.source; + const searchEntityOrNull = options.searchEntity; + const facetsOrNull = options.facets; + const selectedFacetOptionsOrNull = options.selectedFacetOptions; + const spellCheckEnabled = options.spellCheckEnabled; + const excludedTermsOrNull = options.excludedTerms; + const clientIdentifier = objectGraph.host.clientIdentifier; + const searchRequest = new Request(objectGraph) + .withSearchTerm(term) + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(includeAttributesForSearchResults(objectGraph)) + .includingScopedAttributes("editorial-items", ["showLabelInSearch"]) + .includingRelationshipsForUpsell(true) + .includingMacOSCompatibleIOSAppsWhenSupported() + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + setPreviewPlatform(objectGraph, searchRequest); + if (!objectGraph.client.isWatch && !objectGraph.client.isVision) { + searchRequest.includingRelationships(searchIncludeRelationships); + } + if (objectGraph.client.isVision) { + // Temporarily increase the sparseLimit on Vision to workaround lack of filtering for bincompat. + searchRequest.addingQuery("sparseLimit[developers:top-apps]", "12"); + } + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + searchRequest.includingAssociateKeys("apps", ["app-events"]); + searchRequest.includingScopedAttributes("app-events", AppEventsAttributes); + searchRequest.includingScopedRelationships("editorial-items", ["primary-content"]); + } + /** + * Tinker Filter + */ + if (asBooleanOrFalse(objectGraph.client.isTinkerWatch)) { + searchRequest.withFilter("contexts", "tinker"); + } + /** + * Guided Search + */ + if (objectGraph.host.isiOS) { + // Feature enablers + searchRequest.enablingFeature("guidedSearch"); + searchRequest.enablingFeature("midScrollGuidedSearch"); + // Selected facets, if any. + if (isDefinedNonNullNonEmpty(options.guidedSearchTokens)) { + searchRequest.addingQuery("selectedFacets", options.guidedSearchTokens.join(",")); + } + // Optimization term, if any were on request. + if (isDefinedNonNullNonEmpty(options.guidedSearchOptimizationTerm)) { + searchRequest.addingQuery("finalTerm", options.guidedSearchOptimizationTerm); + } + if (objectGraph.bag.isLLMSearchTagsEnabled) { + searchRequest.includingAssociateKeys("results:apps", ["tags"]); + } + } + if (objectGraph.featureFlags.isEnabled("voyager_bundles_2025A")) { + searchRequest.includingScopedAttributes("apps", ["screenshotsByType"]); + } + /** + * Entities in results + */ + if (searchEntityOrNull === "story") { + searchRequest.searchingOverTypes(["editorial-items"]); + } + else if (searchEntityOrNull === "developer") { + searchRequest.searchingOverTypes(["developers"]); + } + else if (searchEntityOrNull === "watch" || searchEntityOrNull === "arcade") { + searchRequest.searchingOverTypes(["apps"]).withFilter("contexts", searchEntityOrNull); + } + else if (objectGraph.client.isTV) { + searchRequest.searchingOverTypes(["apps", "developers", "groupings", "editorial-items"]); + } + else if (objectGraph.client.isVision) { + searchRequest.searchingOverTypes([ + "apps", + "developers", + "editorial-items", + "app-bundles", + "in-apps", + "editorial-pages", + ]); + } + else { + searchRequest.searchingOverTypes([ + "apps", + "developers", + "groupings", + "editorial-items", + "app-bundles", + "in-apps", + ]); + } + /** + * Signal Rosetta unavailability. + */ + if (objectGraph.appleSilicon.isSupportEnabled && !objectGraph.appleSilicon.isRosettaAvailable) { + searchRequest.addingQuery("restrict", "!requiresRosetta"); + } + /** + * Facets / filters + */ + if (facetsOrNull) { + for (const key of Object.keys(facetsOrNull)) { + searchRequest.addingQuery(key, facetsOrNull[key]); + } + } + if (selectedFacetOptionsOrNull) { + for (const key of Object.keys(selectedFacetOptionsOrNull)) { + const requestValues = models.PageFacets.requestValuesForSelectedFacetOptions(selectedFacetOptionsOrNull[key]); + if (isDefinedNonNullNonEmpty(requestValues)) { + searchRequest.addingQuery(key, requestValues.value); + for (const additionalKey of Object.keys(requestValues.additionalKeyValuePairs)) { + searchRequest.addingQuery(additionalKey, requestValues.additionalKeyValuePairs[additionalKey]); + } + } + } + } + /** + * natural language search availability + */ + const isNaturalLanguageSearchResultsEnabled = objectGraph.bag.isNaturalLanguageSearchEnabled || objectGraph.bag.isNaturalLanguageSearchResultsEnabled; + /** + * source attribution + */ + const sourceKey = isNaturalLanguageSearchResultsEnabled ? "source" : "src"; + if (origin === "hints") { + const hintSource = isNaturalLanguageSearchResultsEnabled && (source === null || source === void 0 ? void 0 : source.length) ? "hint:".concat(source) : "hint"; + searchRequest.addingQuery(sourceKey, hintSource); + } + else if (origin === "recents") { + searchRequest.addingQuery(sourceKey, "recent"); + } + else if (origin === "trending") { + searchRequest.addingQuery(sourceKey, "trending"); + } + else if (origin === "undoSpellCorrection") { + searchRequest.addingQuery(sourceKey, "searchInstead"); + } + else if (origin === "applySpellCorrection") { + searchRequest.addingQuery(sourceKey, "didYouMean"); + } + else if (origin === "guidedToken") { + searchRequest.addingQuery(sourceKey, "facet"); + } + /** + * contexts + */ + switch (clientIdentifier) { + case client.watchIdentifier: + searchRequest.addingContext("watch"); + break; + case client.messagesIdentifier: + searchRequest.addingContext("messages"); + break; + case client.arcadeIdentifier: + searchRequest.addingContext("arcade"); + break; + default: + break; + } + /** + * Advert limit + */ + searchRequest.addingQuery("limit[ads-result]", objectGraph.bag.mediaAdvertRequestLimit.toString()); + /** + * Ads locale metadata. + */ + if (isDefinedNonNullNonEmpty(objectGraph.bag.adsOverrideLanguage)) { + searchRequest.enablingFeature("adsLocaleMetadata"); + } + /** + * Feature: Spellcheck + */ + if (spellCheckEnabled) { + searchRequest.enablingFeature("spellCheck"); + } + /** + * Feature: Natural Language + */ + if (isNaturalLanguageSearchResultsEnabled) { + searchRequest.enablingFeature("naturalLanguage"); + } + /** + * Feature: Organic CPPs + */ + if (objectGraph.host.isiOS) { + searchRequest.enablingFeature("searchResultCpps"); + } + /** + * Excluded terms + */ + if (excludedTermsOrNull) { + searchRequest.addingQuery("excludeTerms", excludedTermsOrNull.join(",")); + } + return searchRequest; +} +/** + * Create the `Request` object to execute a search described by `options`, with results grouped by platform + * @param options Options for search being executed. + * @returns `Request` configured for `options`, or `null` if `options` was invalid. + */ +export function createPlatformGroupedSearchRequest(objectGraph, options) { + var _a; + return (_a = createSearchRequest(objectGraph, options)) === null || _a === void 0 ? void 0 : _a.addingQuery("groupBy[search]", "platform").includingMacOSCompatibleIOSAppsWhenSupported(); +} +/** + * Create a request for fetching set of `items` for appearing in search tab. Used for pagination. + * @param items Items to fetch. + */ +function createCatalogRequestForItems(objectGraph, items) { + const shouldUseMixedCatalogRequest = objectGraph.bag.isLLMSearchTagsEnabled || + asBooleanOrFalse(objectGraph.bag.supportedMixedMediaRequestUsecases["search"]); + const searchRequest = new Request(objectGraph, items, shouldUseMixedCatalogRequest, ["tags"]) + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingScopedAttributes("editorial-items", ["showLabelInSearch"]) + .includingAttributes(includeAttributesForSearchResults(objectGraph)) + .includingRelationshipsForUpsell(true) + .includingMacOSCompatibleIOSAppsWhenSupported() + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + addVariantParametersToRequestForItems(objectGraph, searchRequest, items); + if (!objectGraph.client.isWatch) { + searchRequest.includingRelationships(searchIncludeRelationships); + } + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + searchRequest.includingAssociateKeys("apps", ["app-events"]); + searchRequest.includingScopedAttributes("app-events", AppEventsAttributes); + searchRequest.includingScopedRelationships("editorial-items", ["primary-content"]); + } + return searchRequest; +} +/** + * Returns the include attributes used for: + * 1. Initial search request + * 2. Subsequent catalog request for pagination + */ +function includeAttributesForSearchResults(objectGraph) { + const attributes = [ + "screenshotsByType", + "messagesScreenshots", + "videoPreviewsByType", + "requiredCapabilities", + "editorialBadgeInfo", + "supportsFunCamera", + "minimumOSVersion", + "customScreenshotsByTypeForAd", + "customVideoPreviewsByTypeForAd", + "secondaryGenreShortDisplayNames", + "genreShortDisplayName", + "editorialVideo", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + /// We don't want to unnecessarily ask for clients that don't have metadata ribbon capability + if (objectGraph.host.isOSAtLeast(15, 5, 0)) { + attributes.push("remoteControllerRequirement"); + } + // Keeping custom creatives out of prod. + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + attributes.push("adCreativeArtwork"); + attributes.push("adCreativeVideo"); + } + } + return attributes; +} +// endregion +// region Categories Filter Request +/** + * Fetches data to for category filters on devices that need them. + */ +async function fetchCategoryFiltersDataIfNeeded(objectGraph) { + const deviceType = objectGraph.client.deviceType; + if (deviceTypeSupportsCategoryFilters(deviceType)) { + const categoriesRequest = categories.createRequest(objectGraph, null, null, defaultAdditionalPlatformsForClient(objectGraph)); + if (categoriesRequest) { + // Failable request. + return await fetchData(objectGraph, categoriesRequest).catch(() => null); + } + } + return null; +} +function deviceTypeSupportsCategoryFilters(deviceType) { + switch (deviceType) { + case "pad": + case "mac": + return true; + default: + return false; + } +} +// endregion +// region Search Result Modification +/** + * The search dataset id is set to the key `dataSetId` on `meta.metrics`, but needs to have `data.search.dataSetId` key. + * Metrics: metrics blob in search response has incorrect field name + */ +function renameDataSetIdKey(searchResponse) { + var _a; + const searchMetrics = metricsFromMediaApiObject(searchResponse); + const oldDataSetId = "dataSetId"; + if (isDefinedNonNull(searchMetrics) && + isDefinedNonNull(searchResponse.meta) && + isDefinedNonNull((_a = searchResponse.meta) === null || _a === void 0 ? void 0 : _a.metrics)) { + searchResponse.meta.metrics[searchMetricsDataSetID] = asString(searchMetrics, oldDataSetId); + delete searchResponse.meta.metrics[oldDataSetId]; + } +} +// endregion +//# sourceMappingURL=search-results-fetching.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js new file mode 100644 index 0000000..8c2e741 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js @@ -0,0 +1,46 @@ +/** + * Build methods for Search Transparency. + * Search transparency provides additional disclosure message about search ranking required in certain storefronts. + */ +import { isNothing } from "@jet/environment"; +import { FlowAction, LinkableText, StyledText } from "../../api/models"; +import { addClickEventToAction } from "../metrics/helpers/clicks"; +import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils"; +import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent"; +import { getPlatform } from "../preview-platform"; +import { getLocale } from "../locale"; +export function createSearchResultsLearnMoreNoticeLinkableText(objectGraph, metricsOptions) { + const editorialItemId = objectGraph.bag.searchResultsLearnMoreEditorialId; + if (isNothing(editorialItemId) || (editorialItemId === null || editorialItemId === void 0 ? void 0 : editorialItemId.length) === 0) { + return undefined; + } + const clickOptions = { + kind: "editorialItem", + softwareType: null, + title: objectGraph.loc.string("SEARCH_TRANSPARENCY_LINK"), + id: editorialItemId, + targetType: "link", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }; + const text = objectGraph.loc.string("SEARCH_TRANSPARENCY_TEXT"); + const learnMoreAction = new FlowAction("article"); + learnMoreAction.title = text; + learnMoreAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + if (objectGraph.client.isWeb) { + const destination = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: editorialItemId, + }); + const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination); + learnMoreAction.pageUrl = pageUrlString; + learnMoreAction.destination = destination; + } + addClickEventToAction(objectGraph, learnMoreAction, clickOptions); + const linkSubstrings = {}; + linkSubstrings[`${objectGraph.loc.string("SEARCH_TRANSPARENCY_LINK")}`] = learnMoreAction; + const styledText = new StyledText(text, "text/plain"); + return new LinkableText(styledText, linkSubstrings); +} +//# sourceMappingURL=search-results-learn-more-notice.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js new file mode 100644 index 0000000..f6b64c1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js @@ -0,0 +1,248 @@ +/** + * Builder methods for building a collection of search results models for a series of search result data. + */ +import * as validation from "@jet/environment/json/validation"; +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isDataHydrated } from "../../foundation/media/data-structure"; +import { shouldFilter } from "../filtering"; +import { nextPosition, pushBasicLocation, popLocation } from "../metrics/helpers/location"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import { searchResultFromData } from "./content/search-results"; +import * as searchAds from "./search-ads"; +import * as guidedSearch from "./guided-search/guided-search"; +import { addImpressionFields } from "../metrics/helpers/impressions"; +import { searchAdMissedOpportunityFromId } from "../lockups/ad-lockups"; +/** + * Determines where to display guided search, e.g. as mid-scroll module or pinned to top. + * @param meta The metadata from the MAPI response for search catalog request. + * @param objectGraph The App Store object graph. + * @returns The position of the mid-scroll guided search module in search results, or `undefined` for the pinned to top experience. + */ +export function guidedSearchPositionFromSearchResponseMeta(meta, objectGraph) { + var _a, _b, _c; + // Client debug override + const guidedSearchPositionOverride = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.integer("GuidedSearchOverrides.position"); + if (serverData.isNumber(guidedSearchPositionOverride) && guidedSearchPositionOverride > 1) { + return guidedSearchPositionOverride; + } + // Server controlled by default + return (_c = (_b = meta === null || meta === void 0 ? void 0 : meta.displayStyle) === null || _b === void 0 ? void 0 : _b.guidedSearch) === null || _c === void 0 ? void 0 : _c.position; +} +/** + * Create a `SearchResultsBuildResult` containing set of built and deferred results from the top-level search data container. + * This method supports prepending adverts. + * @param objectGraph + * @param requestMetadata Request metadata for search being performed. + * @param searchResponseMetadata Response metadata for search that was performed. + * @param metricsOptions Metrics options for built models. + * @param resultsData Array of search results data. + * @param advertData Data container with advert results. + * @param facetData Array of guided search token response data. + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past + */ +export async function createSearchResults(objectGraph, requestMetadata, searchResponseMetadata, metricsOptions, resultsData, advertData = undefined, facetData = undefined, installedStates = undefined, appStates = undefined) { + var _a, _b, _c, _d, _e, _f; + /// Built models + const builtResults = []; + // Unhydrated items that will be fetched in pagination. + const deferredResults = []; + // Search Experiments Data + const searchExperimentsData = searchResponseMetadata || null; + // Generate the personalization data + const appIds = resultsData + .filter((resultData) => { + return resultData.type === "apps"; + }) + .map((resultData) => { + return resultData.id; + }); + const personalizationDataContainer = onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, new Set(appIds)); + // Build Adverts + let advertsSearchResult; + let advertsDisplayStyle; + if (searchAds.platformSupportsAdverts(objectGraph) && serverData.isDefinedNonNullNonEmpty(advertData)) { + const adsResultAndDisplayStyle = searchAds.adsResultFromSearchResults(objectGraph, advertData, resultsData, requestMetadata, metricsOptions, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, searchExperimentsData, personalizationDataContainer); + advertsSearchResult = adsResultAndDisplayStyle.result; + advertsDisplayStyle = adsResultAndDisplayStyle.displayStyle; + if (serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) { + advertsSearchResult.searchAdOpportunity = advertsSearchResult.lockups[0].searchAdOpportunity; + builtResults.push(advertsSearchResult); + } + } + // Flag for Ad Media Deduping + let isFirstResult = true; + const guidedSearchPosition = guidedSearchPositionFromSearchResponseMeta(searchResponseMetadata, objectGraph); + for (const [index, resultData] of resultsData.entries()) { + // Inject the mid-scroll guided search module if we've reached the desired position. + if (index === guidedSearchPosition) { + const tokens = createGuidedSearchTokens(objectGraph, requestMetadata.requestDescriptor, facetData, metricsOptions); + if (tokens.length > 0) { + const title = (_c = (_b = (_a = searchResponseMetadata === null || searchResponseMetadata === void 0 ? void 0 : searchResponseMetadata.displayStyle) === null || _a === void 0 ? void 0 : _a.guidedSearch) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : objectGraph.loc.string("Search.Guided.Title.ExploreMore"); // static fallback query context + const guidedSearchResult = new models.GuidedSearchResult(title, tokens); + const impressionOptions = { + ...metricsOptions, + id: "midScrollGuidedSearch", + kind: "grouping", + targetType: "module", + title: title, + softwareType: null, + }; + addImpressionFields(objectGraph, guidedSearchResult, impressionOptions); + builtResults.push(guidedSearchResult); + nextPosition(metricsOptions.locationTracker); + } + } + // Deferred items for subsequent pagination. + if (!isDataHydrated(resultData)) { + // On the first unhydrated item, attach the rest of the queue to `deferredResults` to preserve ordering. + deferredResults.push(...resultsData.slice(index)); + break; + } + // Filter + if (shouldFilter(objectGraph, resultData, 10750 /* Filter.Search */)) { + continue; + } + // Advert: Update CPP data on first organic result. + // We must do this *after* the ad results are built, because we need to ensure we're picking the first lockup that will appear, + // not just the first data (that may be filtered somehow). + if (isFirstResult && serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) { + searchAds.updateDupeOrganicResultCPPData(objectGraph, advertData !== null && advertData !== void 0 ? advertData : [], advertsSearchResult, resultData, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, metricsOptions, personalizationDataContainer); + } + // Build model + const searchResult = searchResultFromData(objectGraph, resultData, searchResponseMetadata, personalizationDataContainer, metricsOptions, requestMetadata.requestDescriptor.isNetworkConstrained, requestMetadata.requestDescriptor.searchEntity, searchExperimentsData); + if (!searchResult || !platformSupportsResultType(objectGraph, searchResult)) { + continue; + } + /** + * Advert: When first advert and result matches, modify media. + */ + if (isFirstResult && + serverData.isDefinedNonNullNonEmpty(advertsSearchResult) && + serverData.isDefinedNonNullNonEmpty(advertsSearchResult.lockups)) { + searchAds.dedupeAdMediaFromMatchingResult(objectGraph, advertsSearchResult, searchResult, searchExperimentsData, advertsDisplayStyle); + } + /** + * Advert: When advert isn't available, mark the organic as a missed opportunity slot + */ + if (isFirstResult && + searchAds.platformSupportsAdverts(objectGraph) && + serverData.isDefinedNonNull((_d = metricsOptions.pageInformation) === null || _d === void 0 ? void 0 : _d.iAdInfo) && + (serverData.isNull(advertsSearchResult) || serverData.isNullOrEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups))) { + searchResult.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, metricsOptions.pageInformation); + (_e = searchResult.searchAdOpportunity) === null || _e === void 0 ? void 0 : _e.setMissedOpportunityReason("NOAD"); + (_f = searchResult.searchAdOpportunity) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP"); + } + builtResults.push(searchResult); + isFirstResult = false; + nextPosition(metricsOptions.locationTracker); + } + return await applyClientFilteringToIAPs(objectGraph, builtResults).then((builtResultsFiltered) => { + return { + builtSearchResults: builtResultsFiltered, + deferredSearchResults: deferredResults, + }; + }); +} +// endregion +// region Internals +/** + * Create guided search tokens for the given search request descriptor and facet MAPI response data. + * @param objectGraph The App Store object graph. + * @param requestDescriptor The search request descriptor. + * @param facetData The media API response for guided search facets. + * @param metricsOptions The metrics options. + * @returns The guided search tokens to display. + */ +function createGuidedSearchTokens(objectGraph, requestDescriptor, facetData, metricsOptions) { + if (!objectGraph.host.isiOS || isNothing(facetData) || facetData.length === 0) { + return []; + } + pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchRevisions", + }, ""); + // Tokens from facet data + const tokens = []; + for (const data of facetData) { + const token = guidedSearch.createGuidedSearchToken(objectGraph, "rewrite", requestDescriptor, data, metricsOptions); + if (token) { + tokens.push(token); + nextPosition(metricsOptions.locationTracker); + } + } + popLocation(metricsOptions.locationTracker); + return tokens; +} +/** + * Apply client-side iAP filtering to set of results. + * @param objectGraph + * @param resultsToFilter Results to apply iAP filtering on. + */ +async function applyClientFilteringToIAPs(objectGraph, resultsToFilter) { + return await validation.context("applyClientFilteringToIAPs", async () => { + const iAPProductIDToParentBundleID = {}; + for (const result of resultsToFilter) { + if (result.resultType === "inAppPurchase") { + const inAppPurchaseResult = result; + const inAppPurchaseLockup = inAppPurchaseResult.lockup; + if (inAppPurchaseLockup.parent && + inAppPurchaseLockup.productIdentifier && + inAppPurchaseLockup.parent.bundleId) { + iAPProductIDToParentBundleID[inAppPurchaseLockup.productIdentifier] = + inAppPurchaseLockup.parent.bundleId; + } + else { + validation.unexpectedNull("ignoredValue", "string", `required fields for ${inAppPurchaseLockup.adamId}`); + } + } + } + if (Object.keys(iAPProductIDToParentBundleID).length === 0) { + return await Promise.resolve(resultsToFilter); + } + return await objectGraph.clientOrdering.visibilityForIAPs(iAPProductIDToParentBundleID).then((visibilities) => { + const filteredResults = resultsToFilter.filter((result) => { + if (result.resultType !== "inAppPurchase") { + return true; + } + const inAppPurchaseResult = result; + const inAppPurchaseLockup = inAppPurchaseResult.lockup; + if (inAppPurchaseLockup.productIdentifier && visibilities[inAppPurchaseLockup.productIdentifier]) { + return true; + } + else { + return inAppPurchaseLockup.isVisibleByDefault; + } + }); + return filteredResults; + }); + }); +} +/** + * Whether or not current platform supports displaying given `searchResult`. + */ +function platformSupportsResultType(objectGraph, searchResult) { + if (objectGraph.host.isTV) { + switch (searchResult.resultType) { + case "content": + case "editorial": + return true; + default: + return false; + } + } + if (!objectGraph.host.isiOS && !objectGraph.client.isWeb) { + switch (searchResult.resultType) { + case "appEvent": + return false; + default: + break; + } + } + return true; +} +// endregion +//# sourceMappingURL=search-results-pipeline.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js new file mode 100644 index 0000000..60466dd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js @@ -0,0 +1,98 @@ +/** + * Build methods for Search Spell Correction. + * Spell correction allows users to change an previous search term to higher or lower confidence searches. + */ +import * as models from "../../api/models"; +import { isDefinedNonNull } from "../../foundation/json-parsing/server-data"; +import { addEventsToSearchAction } from "../metrics/helpers/clicks"; +import { popLocation, pushBasicLocation } from "../metrics/helpers/location"; +/** + * Build a `SearchResultsMessage` for a search result with given `SearchTermContext` + * @param termContext Context for the state of search terms. + */ +export function spellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions) { + const showUndoSpellCorrection = isDefinedNonNull(termContext.correctedTerm); + const showSuggestion = isDefinedNonNull(termContext.suggestedTerm); + pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "link", + }, "spellCorrection"); + if (showUndoSpellCorrection) { + return undoSpellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions); + } + if (showSuggestion) { + return correctionSuggestionMessageFromTermContext(objectGraph, termContext, metricsOptions); + } + popLocation(metricsOptions.locationTracker); + return null; +} +/** + * Build a message that allows user to undo an auto-applied spell correction on user-initiated term. + * This is the "Showing Results for ABC. Search Instead For XYZ" variation for high-confidence misspellings + * @param termContext Context for the state of search terms. + * @param locationTracker Location tracker for page it is appearing in. + */ +function undoSpellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions) { + // SearchAction for `term` again, disabling spell correction to `correctedTerm`. + const uncorrectedTerm = termContext.term; + const searchInsteadAction = searchActionForSpellCorrection(objectGraph, uncorrectedTerm, termContext.resultsTerm, "undoSpellCorrection"); + addEventsToSearchAction(objectGraph, searchInsteadAction, "button", metricsOptions.locationTracker); + // "Showing Results for correctedTerm" with no links. + const correctedToTerm = `${termContext.correctedTerm}`; + const showingResultsForTemplate = objectGraph.loc.string("SEARCH_SHOWING_RESULTS_FOR_TEMPLATE"); + const showingResultsForMessage = showingResultsForTemplate.replace("{searchTerm}", correctedToTerm); + const primaryText = new models.LinkableText(new models.StyledText(showingResultsForMessage, "text/x-apple-as3-nqml"), {}); + // "Search Instead for term" with link. + const searchInsteadForTemplate = objectGraph.loc.string("SEARCH_SEARCH_INSTEAD_FOR_TEMPLATE"); + const searchInsteadForMessage = searchInsteadForTemplate.replace("{searchTerm}", uncorrectedTerm); + const searchInsteadForLinks = {}; + searchInsteadForLinks[`${searchInsteadForMessage}`] = searchInsteadAction; + const secondaryText = new models.LinkableText(new models.StyledText(searchInsteadForMessage), searchInsteadForLinks); + return new models.SearchResultsMessage(primaryText, secondaryText, searchInsteadAction); +} +/** + * Build a message that allows user to accept a suggestion to modify a user-initiated term. + * This is the "Did you mean ABC?" variation for low-confidence misspellings + * @param termContext Context for the state of search terms. + * @param locationTracker Location tracker for page it is appearing in. + */ +function correctionSuggestionMessageFromTermContext(objectGraph, termContext, metricsOptions) { + // Search action for `suggestedTerm` + const suggestedTerm = termContext.suggestedTerm; + const suggestedSearchAction = searchActionForSpellCorrection(objectGraph, suggestedTerm, termContext.resultsTerm, "applySpellCorrection"); + addEventsToSearchAction(objectGraph, suggestedSearchAction, "button", metricsOptions.locationTracker); + // "Did you mean suggestedTerm?" where suggested term is linked. + const styledSuggestedTerm = `${suggestedTerm}`; + const didYouMeanTemplate = objectGraph.loc.string("SEARCH_DID_YOU_MEAN_TEMPLATE"); + const didYouMeanMessage = didYouMeanTemplate.replace("{searchTerm}", styledSuggestedTerm); + // Link both the raw suggested term, and suggested term followed by ? + const didYouMeanLinks = {}; + didYouMeanLinks[`${suggestedTerm}`] = suggestedSearchAction; + didYouMeanLinks[`${suggestedTerm}?`] = suggestedSearchAction; + const primaryText = new models.LinkableText(new models.StyledText(didYouMeanMessage, "text/x-apple-as3-nqml"), didYouMeanLinks); + return new models.SearchResultsMessage(primaryText, null, suggestedSearchAction); +} +// endregion +// region Search Action Builders +/** + * Build a SearchAction that is for: + * - Applying a suggested spell correction. + * - Undoing an automatically applied spell correction. + * + * Exported for testing + * + * @param suggestedOrUncorrectedTerm The suggestion or uncorrected term to search for. + * @param resultTerm The original result term. + * @param spellCorrectionActionType The type of spell correction search. + */ +export function searchActionForSpellCorrection(objectGraph, suggestedOrUncorrectedTerm, resultsTerm, spellCorrectionActionType) { + // SearchAction for `term` again, disabling spell correction to `correctedTerm`. + const suggestedSearchAction = new models.SearchAction(suggestedOrUncorrectedTerm, suggestedOrUncorrectedTerm, null, spellCorrectionActionType); + suggestedSearchAction.spellCheckEnabled = false; // Don't trigger corrections / suggestions again. + suggestedSearchAction.excludedTerms = [resultsTerm]; + suggestedSearchAction.originatingTerm = resultsTerm; + return suggestedSearchAction; +} +// endregion +//# sourceMappingURL=search-spell-correction.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js new file mode 100644 index 0000000..f49b680 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js @@ -0,0 +1,57 @@ +import { isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { currentPosition } from "../metrics/helpers/location"; +/** + * Opaque token to use for paginating a list of search results. + * This is used for both standard and segmented search results. + */ +export class SearchToken { +} +/// The number of results to load per page. +const suggestedMaxResutsPerPage = 30; +/** + * Create a search token for loading more search results. + * @param results Remaining set of data that is yet to be paginated + * @param requestMetadata The nature of request that was fired. + * @param responseMetadata The meta blob returned as part of initial search. This is preserved, as subsequent pagination requests don't hit search endpoints. + * @param metricsOptions Metrics options to preserve during pagination + */ +export function createSearchToken(objectGraph, results, requestMetadata, responseMetadata, metricsOptions) { + if (isNullOrEmpty(results)) { + return null; + } + return { + results: results, + maxPerPage: suggestedMaxResutsPerPage, + requestMetadata: requestMetadata, + metricsOptions: metricsOptions, + responseMetadata: responseMetadata !== null && responseMetadata !== void 0 ? responseMetadata : {}, + contentOffsetWithinResultsShelf: currentPosition(metricsOptions.locationTracker), + }; +} +/** + * Returns the next set of items to load. + * @param searchToken Search token used for paginating. + */ +export function getNextItemsToFetch(objectGraph, searchToken) { + if (!searchToken || !searchToken.results) { + return []; + } + return searchToken.results.slice(0, searchToken.maxPerPage); +} +/** + * Advance the search token by the items that were loaded, consistent with `getNextItemsToFetch` + * @param searchToken Search token to create new token with. + */ +export function advanceSearchTokenResults(objectGraph, searchToken) { + let nextPageResults = []; + if (searchToken && searchToken.results) { + nextPageResults = searchToken.results.slice(searchToken.maxPerPage, searchToken.results.length); + } + if (isNullOrEmpty(nextPageResults)) { + return null; + } + const nextToken = { ...searchToken }; + nextToken.results = nextPageResults; + return nextToken; +} +//# sourceMappingURL=search-token.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search.js new file mode 100644 index 0000000..aec35e0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search.js @@ -0,0 +1,1024 @@ +// +// search.ts +// AppStoreKit +// +// Created by Kevin MacWhinnie on 8/15/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import { PageInvocationPoint } from "@jet/environment/types/metrics"; +import * as models from "../../api/models"; +import { SearchResultsLearnMoreNotice } from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as metricsBuilder from "../metrics/builder"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersMisc from "../metrics/helpers/misc"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as guidedSearch from "./guided-search/guided-search"; +import { addGuidedSearchParentImpressionMetrics, addSearchResultParentImpressionMetrics, } from "./guided-search/guided-search-metrics"; +import * as searchAdsODML from "./search-ads-odml"; +import * as searchCommon from "./search-common"; +import { createDefaultSelectedFacetOptions, createSearchFacets, createSearchPageFacets } from "./search-facets"; +import * as searchResultsFetching from "./search-results-fetching"; +import { createSearchResultsLearnMoreNoticeLinkableText } from "./search-results-learn-more-notice"; +import * as searchResultsPipeline from "./search-results-pipeline"; +import * as searchSpellCorrection from "./search-spell-correction"; +import * as searchToken from "./search-token"; +// MARK: - Search Hints +/** + * Convert the response from the search hints endpoint into a model object. + * @param {String} prefixTerm The term the search hints are for (i.e. searchPrefix). + * @param {String} searchUrl The Url if we were to search for this term right away + * @param {*} response The API response. + * @return {SearchHintSet} The search hint set containing the search hint array + */ +export function searchHintsFromApiResponse(objectGraph, prefixTerm, hintsContainer) { + return validation.context("searchHintsFromApiResponse", () => { + var _a, _b, _c, _d; + const metricsOptions = { + targetType: "listItem", + pageInformation: metricsHelpersPage.pageInformationForSearchHintsPage(objectGraph, prefixTerm, hintsContainer.hintsRequestUrl, hintsContainer.dataSetId), + locationTracker: metricsHelpersLocation.newLocationTracker(), + }; + // Build user hint that matches what user typed. Appears in first position. + let userTypedHintAction = null; + if ((_a = hintsContainer.userTypedTerm) === null || _a === void 0 ? void 0 : _a.length) { + userTypedHintAction = new models.SearchAction(hintsContainer.userTypedTerm, hintsContainer.userTypedTerm, null, "userTypedHint"); + userTypedHintAction.spellCheckEnabled = true; + userTypedHintAction.prefixTerm = prefixTerm; + metricsHelpersImpressions.addImpressionMetricsToHintsSearchAction(objectGraph, userTypedHintAction, metricsOptions); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, userTypedHintAction, metricsOptions.targetType, metricsOptions.locationTracker, metricsOptions.pageInformation); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + // Build standard hints. + const searchHintActions = (_c = (_b = hintsContainer.rawHints) === null || _b === void 0 ? void 0 : _b.map((rawHint) => { + return searchHintAction(objectGraph, rawHint, prefixTerm, metricsOptions); + })) !== null && _c !== void 0 ? _c : []; + // Prepend user hint if any. + if (userTypedHintAction != null) { + searchHintActions.unshift(userTypedHintAction); + } + const hintSet = new models.SearchHintSet(searchHintActions, (_d = hintsContainer.ghostHintsTerm) !== null && _d !== void 0 ? _d : null); + /** + * Send `input` Search Events when search hints returns. + * For SSS, this is the granularity we agreed on, instead of sending it in native per keystroke. + * Unlike standard page metrics, we only: + * - Fire a 'input' search event + * - Setup pageFields for page fields generator. + */ + const searchEvent = metricsBuilder.createMetricsSearchData(objectGraph, prefixTerm, "key", "input", hintsContainer.hintsRequestUrl, { ...metricsHelpersMisc.fieldsFromPageInformation(metricsOptions.pageInformation) }); + hintSet.pageMetrics.pageFields = metricsHelpersMisc.fieldsFromPageInformation(metricsOptions.pageInformation); + hintSet.pageMetrics.addData(searchEvent, [PageInvocationPoint.pageEnter]); + hintSet.searchClearAction = createSearchCancelledOrClearedAction(objectGraph, "clear", metricsOptions.pageInformation, metricsOptions.locationTracker, prefixTerm); + hintSet.searchCancelAction = createSearchCancelledOrClearedAction(objectGraph, "cancel", metricsOptions.pageInformation, metricsOptions.locationTracker, prefixTerm); + return hintSet; + }); +} +/** + * Creates a search hint action from the search hint data + * @param objectGraph The App Store object graph + * @param hintData The hint object data + * @param prefixTerm The hint prefix term + * @param metricsOptions The metrics options to use for click and impressions metrics for this hint + * @returns A search hint action for the hint data + */ +function searchHintAction(objectGraph, hintData, prefixTerm, metricsOptions) { + var _a, _b, _c, _d, _e; + const searchEntity = (_a = searchEntityFromHintData(hintData)) !== null && _a !== void 0 ? _a : undefined; + const searchAction = new models.SearchAction((_b = hintData.displayTerm) !== null && _b !== void 0 ? _b : "", (_c = hintData.searchTerm) !== null && _c !== void 0 ? _c : "", null, "hints", searchEntity, hintData.source); + searchAction.artwork = contentArtwork.createArtworkForSystemImage(objectGraph, (_d = models.searchEntitySystemImage(searchEntity)) !== null && _d !== void 0 ? _d : "magnifyingglass"); + searchAction.spellCheckEnabled = true; + searchAction.prefixTerm = prefixTerm; + metricsHelpersImpressions.addImpressionMetricsToHintsSearchAction(objectGraph, searchAction, metricsOptions); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, metricsOptions.targetType, metricsOptions.locationTracker, (_e = metricsOptions.pageInformation) !== null && _e !== void 0 ? _e : undefined); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + return searchAction; +} +/** + * + * @param objectGraph The App Store object graph + * @param entity The entity value string from the hint object + * @param context The context value string from the hint object + * @returns The correct entity for the hint based on legacy entity types + * + * Entities: + * "arcade" ==> "apps" with context "arcade" + "developer" ==> "developers" + "story" ==> "editorial-items" + "watch" ==> "apps" with context "watch" + */ +function searchEntityFromHintData(rawHint) { + const hintEntity = rawHint.entity; + switch (hintEntity) { + case "apps": + return rawHint.context; + case "developers": + return "developer"; + case "editorial-items": + return "story"; + default: + return null; + } +} +// MARK: - Trending Searches +/** + * Convert the response from the trending searches endpoint into a model object. + * @param {*} response The API response. + * @return {TrendingSearch} A trending searches model object. + */ +export function trendingSearchesFromApiResponse(objectGraph, response) { + return validation.context("trendingSearchesFromApiResponse", () => { + const locationTracker = metricsHelpersLocation.newLocationTracker(); + const searches = serverData.asArrayOrEmpty(response, "trendingSearches").map((rawSearch) => { + const term = serverData.asString(rawSearch, "label"); + const searchAction = new models.SearchAction(term, term, serverData.asString(rawSearch, "url"), "trending"); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", locationTracker); + metricsHelpersLocation.nextPosition(locationTracker); + return searchAction; + }); + const title = searches.length > 0 ? serverData.asString(response, "header.label") : null; + const trendingSearches = new models.TrendingSearches(title, searches); + switch (objectGraph.client.deviceType) { + case "pad": + trendingSearches.maxNumberOfSearches = 10; + break; + case "phone": + trendingSearches.maxNumberOfSearches = 7; + break; + default: + break; + } + return trendingSearches; + }); +} +// MARK: - Search Results +/** + * Gets the shelf ID for search results, on a specfic segment if applicable + * @param segmentTitle The title of the segment this shelf is on, if using segmented search + * @returns The id for the search results shelf + * @note This needs to include the segment title for segment search results since the contents on + * each segment would otherwise have the same shelf id and therefore would lead to the content never changing + */ +function getSearchResultsShelfId(segmentTitle) { + if (isSome(segmentTitle) && segmentTitle.length !== 0) { + return `SearchResults.${segmentTitle}.shelfId`; + } + else { + return "SearchResults.shelfId"; + } +} +/** + * Gets metrics pageid to use for the search results page. + * @param segmentType The segment this page is displaying if any + * @returns The id for the search results page + */ +function getSearchResultsPageId(segmentType) { + switch (segmentType) { + case models.SegmentedSearchResultsPageSegmentType.iOS: + return "ios"; + case models.SegmentedSearchResultsPageSegmentType.visionOS: + return "visionos"; + default: + return "SearchTopResults"; + } +} +/** + * Creates a new empty `SearchResults` object. + * @return {SearchResults} An empty search results model object. + */ +export function emptyResults(objectGraph, requestFacets) { + const searchResults = new models.SearchResults(); + if (serverData.isDefinedNonNull(requestFacets)) { + searchResults.facets = createSearchFacets(objectGraph, requestFacets); + searchResults.pageFacets = createSearchPageFacets(objectGraph); + searchResults.selectedFacetOptions = createDefaultSelectedFacetOptions(objectGraph); + } + searchResults.results = []; + return searchResults; +} +/** + * Creates a new empty `SearchResultsPage` object. + * @return {SearchResults} An empty search results model object. + */ +export function emptyResultsPage(objectGraph, requestFacets) { + const searchResultsPage = new models.SearchResultsPage([]); + if (serverData.isDefinedNonNull(requestFacets)) { + searchResultsPage.facets = createSearchFacets(objectGraph, requestFacets); + searchResultsPage.pageFacets = createSearchPageFacets(objectGraph); + searchResultsPage.selectedFacetOptions = createDefaultSelectedFacetOptions(objectGraph); + } + return searchResultsPage; +} +/** + * A container like class to manage a search results page segment and surrounding context + */ +class SegmentedSearchResultsPageSegmentContext { +} +/** + * Builds `BaseSearchPage` from data. + */ +async function baseSearchPageFromResponse(objectGraph, combinedSearchData) { + return await validation.context("searchResultsFromResponse", async () => { + var _a; + const fetchTimingMetricsBuilder = (_a = objectGraph.fetchTimingMetricsBuilder) !== null && _a !== void 0 ? _a : new FetchTimingMetricsBuilder(); + const page = await fetchTimingMetricsBuilder.measureModelConstructionAsync(async () => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; + const response = combinedSearchData.catalogResponse; + const requestMetadata = combinedSearchData.requestMetadata; + const searchRequestUrl = requestMetadata.searchRequestUrl; + const sponsoredSearchRequestData = combinedSearchData.sponsoredSearchRequestData; + const guidedSearchData = response.results.guidedSearch; + // Term Context + const termContext = searchCommon.createTermContextForSpellcheckedSequentialResponse(objectGraph, requestMetadata.requestDescriptor, response); + // Metrics + const wasOdmlSuccessful = searchAdsODML.wasODMLSuccessful(objectGraph, combinedSearchData.sponsoredSearchAdvertData); + const metricsOptions = { + locationTracker: metricsHelpersLocation.newLocationTracker(), + pageInformation: metricsHelpersPage.pageInformationForSearchPage(objectGraph, requestMetadata.requestDescriptor, response, termContext, searchRequestUrl, getSearchResultsPageId(), sponsoredSearchRequestData, wasOdmlSuccessful, guidedSearchData), + createUniqueImpressionId: true, + }; + // Root container + const isShelfBasedSearch = objectGraph.featureFlags.isEnabled("shelves_2_0_search") || + objectGraph.client.isiOS || + objectGraph.client.isTV || + objectGraph.client.isVision || + objectGraph.client.isWeb; + const baseSearchPage = isShelfBasedSearch + ? new models.SearchResultsPage() + : new models.SearchResults(); + // Guided Search Experimentation (pinned vs mid-scroll) + const guidedSearchPosition = searchResultsPipeline.guidedSearchPositionFromSearchResponseMeta(response.meta, objectGraph); + if (isNothing(guidedSearchPosition)) { + /** + * Guided Search + * - note: This is important to occur before `createSearchResults` since its impression index should be lower than the container for search results. + */ + addModelsForGuidedSearch(objectGraph, baseSearchPage, requestMetadata, guidedSearchData === null || guidedSearchData === void 0 ? void 0 : guidedSearchData.facets, metricsOptions); + } + const shelfMetricsOptions = { + id: "search-results", + kind: null, + softwareType: null, + targetType: "SearchResults", + title: "Search Results", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + idType: "relationship", + }; + let resultsShelf; + if (isShelfBasedSearch) { + resultsShelf = new models.Shelf("searchResult"); + resultsShelf.id = getSearchResultsShelfId(); + resultsShelf.isHorizontal = false; + if (objectGraph.client.isWeb) { + resultsShelf.title = objectGraph.loc + .string("Search.ResultsTitle") + .replace("@@search_term@@", termContext.term); + } + // We need to create and apply impressions fields to the shelf before creating the search results, + // so that the shelf is sitting in the right location relative to its children from an impressions perspective. + metricsHelpersImpressions.addImpressionFields(objectGraph, resultsShelf, shelfMetricsOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, "Search Results"); + } + let advertData = dataForAdvertsFromCombinedData(objectGraph, combinedSearchData); + const appliedPolicy = (_a = combinedSearchData.sponsoredSearchAdvertData) === null || _a === void 0 ? void 0 : _a.appliedPolicy; + if (isSome(appliedPolicy)) { + // If there was an applied policy, suppress the ad. + advertData = []; + if (appliedPolicy === "ageRestricted") { + (_b = metricsOptions.pageInformation) === null || _b === void 0 ? void 0 : _b.iAdInfo.setMissedOpportunity(objectGraph, "ODP_NOAD", "searchResults"); + } + } + const unsafeSearch = (_f = (_e = (_d = (_c = response.meta) === null || _c === void 0 ? void 0 : _c.results) === null || _d === void 0 ? void 0 : _d.search) === null || _e === void 0 ? void 0 : _e.searchSafety) !== null && _f !== void 0 ? _f : false; + const missedOpportunityReason = ((_k = (_j = (_h = (_g = response.meta) === null || _g === void 0 ? void 0 : _g.results) === null || _h === void 0 ? void 0 : _h.search) === null || _j === void 0 ? void 0 : _j.reason) === null || _k === void 0 ? void 0 : _k.kind) === "no-results" ? "NLS_NORESULTS" : "NLS_NOAD"; + if (unsafeSearch) { + // If the search was deemed unsafe, suppress Ad and record the missed opportunity. + advertData = []; + (_l = metricsOptions.pageInformation) === null || _l === void 0 ? void 0 : _l.iAdInfo.setMissedOpportunity(objectGraph, missedOpportunityReason, "searchResults"); + } + /** + * Advert + Search Results + */ + const builderResults = await searchResultsPipeline.createSearchResults(objectGraph, requestMetadata, response.meta, metricsOptions, dataForSearchResultsFromCombinedData(objectGraph, combinedSearchData), advertData, guidedSearchData === null || guidedSearchData === void 0 ? void 0 : guidedSearchData.facets, installedStatesForAdvertsData(combinedSearchData), appStatesForAdvertsData(combinedSearchData)); + if (unsafeSearch && builderResults.builtSearchResults.length !== 0) { + const searchAdOpportunity = builderResults.builtSearchResults[0].lockup + .searchAdOpportunity; + searchAdOpportunity === null || searchAdOpportunity === void 0 ? void 0 : searchAdOpportunity.setMissedOpportunityReason(missedOpportunityReason); + searchAdOpportunity === null || searchAdOpportunity === void 0 ? void 0 : searchAdOpportunity.setTemplateType("APPLOCKUP"); + } + // Add result shelves to page. + if (isShelfBasedSearch && resultsShelf) { + const searchResultsPage = baseSearchPage; + resultsShelf.items = builderResults.builtSearchResults; + searchResultsPage.resultsParentImpressionMetrics = resultsShelf.impressionMetrics; + searchResultsPage.shelves.push(resultsShelf); + // Display the context message for the results (or lack thereof). + const contextCard = createSearchResultsContextCard(response.results.queryContext, objectGraph); + const resultsReason = (_p = (_o = (_m = response.meta) === null || _m === void 0 ? void 0 : _m.results) === null || _o === void 0 ? void 0 : _o.search) === null || _p === void 0 ? void 0 : _p.reason; + if ((resultsReason === null || resultsReason === void 0 ? void 0 : resultsReason.kind) === "no-results") { + searchResultsPage.unavailableReason = { + title: objectGraph.loc.stringWithFallback("Search.Results.Empty.Title", "No results"), + message: resultsReason.text, + action: actionFromSearchResultsLinks(resultsReason.links), + contextCard: contextCard, + }; + } + else if (contextCard) { + const paragraphShelf = new models.Shelf("searchResultsContextCard"); + paragraphShelf.items = [contextCard]; + const contextCardPosition = (_t = (_s = (_r = (_q = response.meta) === null || _q === void 0 ? void 0 : _q.displayStyle) === null || _r === void 0 ? void 0 : _r.queryContext) === null || _s === void 0 ? void 0 : _s.position) !== null && _t !== void 0 ? _t : 0; + if (contextCardPosition > 0) { + const postCardShelfContents = resultsShelf.items.splice(contextCardPosition); + const postCardResulsShelf = { + ...resultsShelf, + id: "searchResults2", + items: postCardShelfContents, + isValid: resultsShelf.isValid, + }; + searchResultsPage.shelves.push(paragraphShelf); + searchResultsPage.shelves.push(postCardResulsShelf); + } + else { + searchResultsPage.shelves.unshift(paragraphShelf); + } + } + // Remove unsafe searches from recents if no results were provided. + if (unsafeSearch && builderResults.builtSearchResults.length === 0) { + (_v = (_u = objectGraph.onDeviceSearchHistoryManager).removeRecentSearchTerm) === null || _v === void 0 ? void 0 : _v.call(_u, termContext.term); + } + } + else { + const searchResults = baseSearchPage; + searchResults.results = builderResults.builtSearchResults; + addSearchResultParentImpressionMetrics(objectGraph, searchResults, metricsOptions); + } + /** + * Next Page + */ + if (builderResults.deferredSearchResults.length > 0) { + baseSearchPage.nextPage = searchToken.createSearchToken(objectGraph, builderResults.deferredSearchResults, requestMetadata, response.meta, metricsOptions); + } + if (isShelfBasedSearch) { + metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker); + } + /** + * Spell Correction Message + */ + baseSearchPage.message = searchSpellCorrection.spellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions); + /** + * Search Entity + */ + const searchEntity = requestMetadata.requestDescriptor.searchEntity; + const searchEntitySpecified = !serverData.isNullOrEmpty(searchEntity); + if (!searchEntitySpecified) { + baseSearchPage.facets = createSearchFacets(objectGraph, requestMetadata.requestDescriptor.facets, combinedSearchData.categoriesFilterData); + baseSearchPage.pageFacets = createSearchPageFacets(objectGraph, combinedSearchData.categoriesFilterData); + baseSearchPage.selectedFacetOptions = serverData.isDefinedNonNullNonEmpty(combinedSearchData.requestMetadata.requestDescriptor.selectedFacetOptions) + ? combinedSearchData.requestMetadata.requestDescriptor.selectedFacetOptions + : createDefaultSelectedFacetOptions(objectGraph); + } + // Attach search term context + baseSearchPage.searchTermContext = termContext; + // Enable autoplay search results on all clients except tv. + baseSearchPage.isAutoPlayEnabled = objectGraph.client.deviceType !== "tv"; + baseSearchPage.isCondensedSearchLockupsEnabled = objectGraph.client.isPhone; + // Search Transparency + baseSearchPage.transparencyLink = createSearchResultsLearnMoreNoticeLinkableText(objectGraph, metricsOptions); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, baseSearchPage, metricsOptions.pageInformation); + baseSearchPage.searchClearAction = createSearchCancelledOrClearedAction(objectGraph, "clear", metricsOptions.pageInformation, metricsOptions.locationTracker, termContext.term); + baseSearchPage.searchCancelAction = createSearchCancelledOrClearedAction(objectGraph, "cancel", metricsOptions.pageInformation, metricsOptions.locationTracker, termContext.term); + return baseSearchPage; + }); + return page; + }); +} +// Creates context card model for search results from media API data. +export function createSearchResultsContextCard(queryContext, objectGraph) { + return validation.context("createSearchResultsContextCard", () => { + var _a, _b; + if (isNothing(queryContext) || !["iOS", "macOS"].includes(objectGraph.host.platform)) { + return undefined; // no server data or platform unsupported + } + const linkActions = (_a = queryContext.links) === null || _a === void 0 ? void 0 : _a.map(actionFromSearchResultsLink); + const linkedSubstrings = linkActions === null || linkActions === void 0 ? void 0 : linkActions.reduce((map, linkAction) => { + var _a; + if ((_a = linkAction.title) === null || _a === void 0 ? void 0 : _a.length) { + if (objectGraph.host.isMac) { + linkAction.title += "\u00a0\u2197"; + } + map[linkAction.title] = linkAction; + } + return map; + }, {}); + let rawText = queryContext.text; + if ((linkActions === null || linkActions === void 0 ? void 0 : linkActions.length) === 1 && ((_b = linkActions[0].title) === null || _b === void 0 ? void 0 : _b.length)) { + // Add single link to end of text, separated by a space. + rawText += " " + linkActions[0].title; + } + else if (linkActions && linkActions.length > 1) { + // Add multiple lines below end of text, separated by a new line. + if (rawText.length > 0) { + rawText += "\n"; + } + rawText += linkActions + .map((action) => action.title) + .filter((title) => title === null || title === void 0 ? void 0 : title.length) + .join("\n"); + } + const styledText = new models.StyledText(rawText); + const linkableText = new models.LinkableText(styledText, linkedSubstrings); + const contextCard = new models.SearchResultsContextCard(linkableText); + return contextCard; + }); +} +/// Creates and returns the first url action from the provided link data included in the search results response. +function actionFromSearchResultsLinks(linksData) { + return validation.context("actionFromSearchResultsLinks", () => { + const bestLinkData = linksData === null || linksData === void 0 ? void 0 : linksData.find((linkData) => linkData.url.length > 0); + return bestLinkData ? actionFromSearchResultsLink(bestLinkData) : undefined; + }); +} +/// Creates and returns an action from the provided link data included in the search results response. +function actionFromSearchResultsLink(linkData) { + return validation.context("actionFromSearchResultsLink", () => { + var _a; + const linkAction = new models.ExternalUrlAction(linkData.url, false); + linkAction.title = (_a = linkData.label) === null || _a === void 0 ? void 0 : _a.replace(" ", "\u00a0"); // Use non-breaking spaces for link labels. + linkAction.artwork = new models.Artwork("systemimage://arrow.up.forward", 0, 0, []); + return linkAction; + }); +} +function installedStatesForAdvertsData(combinedSearchData) { + var _a, _b; + return (_b = (_a = combinedSearchData.sponsoredSearchAdvertData) === null || _a === void 0 ? void 0 : _a.installedStates) !== null && _b !== void 0 ? _b : {}; +} +function appStatesForAdvertsData(combinedSearchData) { + var _a, _b; + return (_b = (_a = combinedSearchData.sponsoredSearchAdvertData) === null || _a === void 0 ? void 0 : _a.appStates) !== null && _b !== void 0 ? _b : {}; +} +export async function searchResultsFromResponse(objectGraph, combinedSearchData) { + return await baseSearchPageFromResponse(objectGraph, combinedSearchData); +} +export async function searchResultsPageFromResponse(objectGraph, combinedSearchData) { + return await baseSearchPageFromResponse(objectGraph, combinedSearchData); +} +/** + * Creates a new empty `SegmentedSearchResultsPage` object. + * @return An empty segmented search results model object. + */ +export function emptySegmentedResultsPage(objectGraph) { + return new models.SegmentedSearchResultsPage(); +} +/** + * Creates the segmented search results page form the response + * @param objectGraph The app store object graph + * @param combinedSearchData The combined data for the segmented search results response + * @returns A promise of the segmented search results page + */ +export async function segmentedSearchResultsPageFromResponse(objectGraph, combinedSearchData) { + return await validation.context("segmentedSearchResultsPageFromResponse", async () => { + const response = combinedSearchData.catalogResponse; + const requestMetadata = combinedSearchData.requestMetadata; + if (isSome(response.results.search.groups)) { + const fetchPages = response.results.search.groups.map(async (group) => { + const segmentResponse = await baseSegmentFromGroupResponse(objectGraph, combinedSearchData, response, group, requestMetadata); + return segmentResponse; + }); + return await Promise.all(fetchPages).then((pageContexts) => { + const page = new models.SegmentedSearchResultsPage(); + if (serverData.isNullOrEmpty(pageContexts)) { + return page; + } + const segments = pageContexts.map((context) => { + return context.segment; + }); + page.segments = segments; + const initialSegmentId = response.results.search.autoSelectedGroupId || segments[0].groupId; + page.selectedSegmentId = initialSegmentId; + addSegmentChangeActionsToPages(objectGraph, pageContexts, initialSegmentId); + return page; + }); + } + else { + return new models.SegmentedSearchResultsPage(); + } + }); +} +/** + * Adds all segment change actions to each search result page segment + * @param objectGraph The App Store object graph + * @param contexts The segment contexts for the page + * @param initialSegmentId The initial segment Id. + */ +function addSegmentChangeActionsToPages(objectGraph, contexts, initialSegmentId) { + const firstNonEmptySegmentContext = contexts.find((context) => { + const hasNonEmptyPage = context.segment.page.shelves.some((shelf) => { + const isNotEmpty = shelf.items.length > 0; + return isNotEmpty; + }); + return hasNonEmptyPage; + }); + const firstNonEmptySegmentId = firstNonEmptySegmentContext === null || firstNonEmptySegmentContext === void 0 ? void 0 : firstNonEmptySegmentContext.segment.groupId; + const segments = contexts.map((context) => { + return context.segment; + }); + const nativeSegmentGroupId = segmentGroupIdForPlatform(objectGraph, segments); + // Check if the native segment has content. + const nativeSegmentIsNonEmpty = segments.some((segment) => { + if (segment.groupId !== nativeSegmentGroupId) { + return false; + } + return segment.page.shelves.some((shelf) => { + return shelf.items.length > 0; + }); + }); + contexts.forEach((context) => { + context.segment.emptySegmentChangeAction = segmentChangeAction(objectGraph, SegmentChangeReason.EmptyResults, context.segment.page.id, context.metricsOptions, firstNonEmptySegmentId, context.segment.groupId); + // Only add the non-native segment change action if the native segment has content, and + // the initial segment wasn't the native segment. + if (nativeSegmentIsNonEmpty && initialSegmentId !== nativeSegmentGroupId) { + context.segment.nonNativeSegmentChangeAction = segmentChangeAction(objectGraph, SegmentChangeReason.NonNative, context.segment.page.id, context.metricsOptions, nativeSegmentGroupId, context.segment.groupId); + } + context.segment.segmentPickerSegmentChangeAction = segmentChangeAction(objectGraph, SegmentChangeReason.Picker, context.segment.page.id, context.metricsOptions, context.segment.groupId); + }); +} +/** + * + * @param objectGraph The app store object graph + * @param combinedSearchData The combined search results which contains the response for segmented results + * @param groupResponse The response for the segmented results + * @param searchData The data for a single search results segment + * @param requestMetadata The metadata from the search request + * @returns A promise of a segmented search results page segment + */ +async function baseSegmentFromGroupResponse(objectGraph, combinedSearchData, groupResponse, searchData, requestMetadata) { + return await validation.context("baseSearchPageFromGroupResponse", async () => { + const searchRequestUrl = requestMetadata.searchRequestUrl; + const segmentType = segmentTypeFromGroupId(searchData.groupId); + // Term Context + const termContext = searchCommon.createTermContextForSpellcheckedGroupedResponse(objectGraph, requestMetadata.requestDescriptor, groupResponse); + // Metrics + const metricsOptions = { + locationTracker: metricsHelpersLocation.newLocationTracker(), + pageInformation: metricsHelpersPage.pageInformationForSearchPage(objectGraph, requestMetadata.requestDescriptor, groupResponse, termContext, searchRequestUrl, getSearchResultsPageId(segmentType), null, false, null), + createUniqueImpressionId: true, + }; + const shelfMetricsOptions = { + id: "search-results", + kind: null, + softwareType: null, + targetType: "SearchResults", + title: "Search Results", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + idType: "relationship", + }; + // Root container + const searchResultsPage = new models.SearchResultsPage(); + const searchSegment = new models.SegmentedSearchResultsPageSegment(); + const resultsShelf = new models.Shelf("searchResult"); + resultsShelf.isHorizontal = false; + // We need to create and apply impressions fields to the shelf before creating the search results, + // so that the shelf is sitting in the right location relative to its children from an impressions perspective. + metricsHelpersImpressions.addImpressionFields(objectGraph, resultsShelf, shelfMetricsOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, "Search Results"); + const resultsData = mediaDataStructure.dataCollectionFromDataContainer(searchData); + const builderResults = await searchResultsPipeline.createSearchResults(objectGraph, requestMetadata, groupResponse.meta, metricsOptions, resultsData); + const learnMoreNoticeLinkableText = createSearchResultsLearnMoreNoticeLinkableText(objectGraph, metricsOptions); + const shelves = searchResultsShelvesFromBuilderResults(objectGraph, learnMoreNoticeLinkableText, builderResults, resultsShelf); + searchResultsPage.shelves.push(...shelves); + searchResultsPage.resultsParentImpressionMetrics = resultsShelf.impressionMetrics; + if (builderResults.deferredSearchResults.length > 0) { + searchResultsPage.nextPage = searchToken.createSearchToken(objectGraph, builderResults.deferredSearchResults, requestMetadata, groupResponse.meta, metricsOptions); + } + metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker); + /** + * Spell Correction Message + */ + searchResultsPage.message = searchSpellCorrection.spellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions); + // Enable autoplay search results on all clients except tv. + searchResultsPage.isAutoPlayEnabled = objectGraph.client.deviceType !== "tv"; + searchResultsPage.isCondensedSearchLockupsEnabled = objectGraph.client.isPhone; + // Search Transparency + searchResultsPage.transparencyLink = learnMoreNoticeLinkableText; + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, searchResultsPage, metricsOptions.pageInformation); + searchSegment.groupId = searchData.groupId; + resultsShelf.id = getSearchResultsShelfId(searchSegment.groupId); + searchSegment.page = searchResultsPage; + searchSegment.title = segmentTitleForSegmentType(objectGraph, false, segmentType); + return { + segment: searchSegment, + metricsOptions: metricsOptions, + }; + }); +} +/** + * Get an array of shelves from the search builder results. + * This is used to decide where to insert the search results learn more notice across + * initial and paginated page builds. + * + * This is currently only used on visionOS search paths, but has the appropriate checks to ensure + * it doesn't run on unsupported platforms, so could be used in all search code paths. + * @param objectGraph The Object Graph. + * @param learnMoreNoticeLinkableText The linkable text to display as part of the learn more notice. + * @param builderResults The results from `createSearchResults`. + * @param resultsShelf The initial shelf created to hold the results. This is passed in + * because in many cases a reference to this is required later when building out the page. + * @param startingContentOffsetWithinResults The starting content offset before the newly built + * content is added. For an initial page build, this will be zero, and for paginated pages this + * is stored on the token. + * @returns An array of shelves for a search results page, with the learn more notice inserted + * if required. + */ +function searchResultsShelvesFromBuilderResults(objectGraph, learnMoreNoticeLinkableText, builderResults, resultsShelf, startingContentOffsetWithinResults = 0) { + let learnMoreNoticeIndex = searchResultsLearnMoreNoticeIndex(objectGraph); + const builtResultsCount = builderResults.builtSearchResults.length; + const hasDeferredResults = builderResults.deferredSearchResults.length > 0; + // The total built results for the page so far. This is the initial page, plus any paginated content. + const totalBuiltResults = startingContentOffsetWithinResults + builtResultsCount; + // There are a number of conditions we don't want to insert the learn more notice: + // 1. No index at which to insert it - this generally means the platform doesn't support this method of insertion. + // 2. No linkable text object - the bag key wasn't present. + // 3. The starting offset is greater than or equal to the insertion index - we can assume we've already shown the + // notice in a previous page build. + // 4. We have deferred results and the total built results count is less than the threshold - we'll insert the + // notice in a subsequent fetch. + // 5. We have no built results and no deferred results, ie. the page will be empty. + // If we fail any of these conditions, we will just attach all the items into the single shelf and return that. + if (learnMoreNoticeIndex === undefined || + learnMoreNoticeLinkableText === undefined || + startingContentOffsetWithinResults >= learnMoreNoticeIndex || + (hasDeferredResults && totalBuiltResults < learnMoreNoticeIndex) || + (totalBuiltResults === 0 && !hasDeferredResults)) { + resultsShelf.items = builderResults.builtSearchResults; + return [resultsShelf]; + } + // Adjust the index by the starting offset, to ensure we take any items built in the initial fetch into account. + learnMoreNoticeIndex = learnMoreNoticeIndex - startingContentOffsetWithinResults; + // We have satisfied the insertion conditions for the learn more notice - we need to split the shelf in two to incorporate + // the learn more notice as its own shelf in between. This is due to layout restrictions on visionOS where we cannot + // accommodate a full width item in a grid of two items per row. + resultsShelf.items = builderResults.builtSearchResults.slice(0, learnMoreNoticeIndex); + const learnMoreNoticeShelf = new models.Shelf("searchResultsLearnMoreNotice"); + learnMoreNoticeShelf.isHorizontal = false; + const searchResult = new SearchResultsLearnMoreNotice(learnMoreNoticeLinkableText); + learnMoreNoticeShelf.items = [searchResult]; + const shelves = [resultsShelf, learnMoreNoticeShelf]; + const secondResultsShelfItems = builderResults.builtSearchResults.slice(learnMoreNoticeIndex); + if (secondResultsShelfItems.length > 0) { + const secondResultsShelf = new models.Shelf("searchResult"); + // Give the second results shelf the same impression metrics. This will result in each shelf contributing to the same impression metrics, + // the only difference is we will see two "viewedInfo" entries for each distinct shelf within the top level impressions item. + secondResultsShelf.impressionMetrics = resultsShelf.impressionMetrics; + secondResultsShelf.isHorizontal = false; + secondResultsShelf.items = secondResultsShelfItems; + shelves.push(secondResultsShelf); + } + return shelves; +} +/** + * Get the index for where the Search Results Learn More Notice should be inserted. + * Returns undefined if either: + * - the editorial ID is not available in the bag, or + * - the platform does not support this style of presentation. + * @param objectGraph The Object Graph + */ +function searchResultsLearnMoreNoticeIndex(objectGraph) { + const editorialItemId = objectGraph.bag.searchResultsLearnMoreEditorialId; + if (isNothing(editorialItemId) || (editorialItemId === null || editorialItemId === void 0 ? void 0 : editorialItemId.length) === 0) { + return undefined; + } + if (objectGraph.client.isVision) { + return 6; + } + return undefined; +} +export async function paginatedSearchResultsWithToken(objectGraph, token) { + return await validation.context("paginatedSearchResultsWithToken", async () => { + const nextItemsToFetch = searchToken.getNextItemsToFetch(objectGraph, token); + const advancedToken = searchToken.advanceSearchTokenResults(objectGraph, token); + if (nextItemsToFetch.length === 0) { + return await Promise.resolve(emptyResults(objectGraph)); + } + return await searchResultsFetching + .fetchSearchResultItems(objectGraph, nextItemsToFetch) + .then(async (dataContainer) => { + const resultsDatum = mediaDataStructure.dataCollectionFromDataContainer(dataContainer); + return await searchResultsPipeline + .createSearchResults(objectGraph, token.requestMetadata, token.responseMetadata, token.metricsOptions, resultsDatum) + .then((builderResults) => { + const searchResults = new models.SearchResults(); + searchResults.results = builderResults.builtSearchResults; + searchResults.nextPage = advancedToken; + return searchResults; + }); + }); + }); +} +export async function paginatedSearchResultsPageWithToken(objectGraph, token) { + return await validation.context("paginatedSearchResultsPageWithToken", async () => { + const nextItemsToFetch = searchToken.getNextItemsToFetch(objectGraph, token); + const advancedToken = searchToken.advanceSearchTokenResults(objectGraph, token); + if (nextItemsToFetch.length === 0) { + return await Promise.resolve(emptyResultsPage(objectGraph)); + } + return await searchResultsFetching + .fetchSearchResultItems(objectGraph, nextItemsToFetch) + .then(async (dataContainer) => { + const resultsDatum = mediaDataStructure.dataCollectionFromDataContainer(dataContainer); + const shelfMetricsOptions = { + id: "search-results", + kind: null, + softwareType: null, + targetType: "SearchResults", + title: "Search Results", + pageInformation: token.metricsOptions.pageInformation, + locationTracker: token.metricsOptions.locationTracker, + idType: "relationship", + }; + // Set up the "new" shelf + const resultsShelf = new models.Shelf("searchResult"); + resultsShelf.id = getSearchResultsShelfId(); + resultsShelf.isHorizontal = false; + // Shelf Impressions: Add impression fields. + // This shelf, and the associated metrics data isn't really used - it's basically just a vehicle for the new items to be + // merged into the old page/shelf. Even so, we create and apply impressions metrics correctly before setting the content + // location and current position so it looks correct. + metricsHelpersImpressions.addImpressionFields(objectGraph, resultsShelf, shelfMetricsOptions); + // Set the content location to the "search-results" shelf. + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, "Search Results"); + // Update position to content offset within the same search shelf prior to building the new results. + metricsHelpersLocation.setCurrentPosition(shelfMetricsOptions.locationTracker, token.contentOffsetWithinResultsShelf); + return await searchResultsPipeline + .createSearchResults(objectGraph, token.requestMetadata, token.responseMetadata, token.metricsOptions, resultsDatum) + .then((builderResults) => { + const learnMoreNoticeLinkableText = createSearchResultsLearnMoreNoticeLinkableText(objectGraph, shelfMetricsOptions); + const shelves = searchResultsShelvesFromBuilderResults(objectGraph, learnMoreNoticeLinkableText, builderResults, resultsShelf, token.contentOffsetWithinResultsShelf); + const searchResultsPage = new models.SearchResultsPage(shelves); + // Ensure we increment the offset for any future paginated results. + if (serverData.isDefinedNonNull(advancedToken)) { + advancedToken.contentOffsetWithinResultsShelf = metricsHelpersLocation.currentPosition(shelfMetricsOptions.locationTracker); + searchResultsPage.nextPage = advancedToken; + } + searchResultsPage.isCondensedSearchLockupsEnabled = objectGraph.client.isPhone; + searchResultsPage.resultsParentImpressionMetrics = resultsShelf.impressionMetrics; + metricsHelpersLocation.popLocation(token.metricsOptions.locationTracker); + searchResultsPage.searchClearAction = createSearchCancelledOrClearedAction(objectGraph, "clear", token.metricsOptions.pageInformation, token.metricsOptions.locationTracker, token.requestMetadata.requestDescriptor.term); + searchResultsPage.searchCancelAction = createSearchCancelledOrClearedAction(objectGraph, "cancel", token.metricsOptions.pageInformation, token.metricsOptions.locationTracker, token.requestMetadata.requestDescriptor.term); + return searchResultsPage; + }); + }); + }); +} +// region Extracting Data +/** + * Returns the array of data objects to build search results with. + */ +function dataForSearchResultsFromCombinedData(objectGraph, combinedSearchData) { + return mediaDataStructure.dataCollectionFromDataContainer(combinedSearchData.catalogResponse.results.search); +} +/** + * Returns the array of data objects to build search adverts with. + */ +function dataForAdvertsFromCombinedData(objectGraph, combinedSearchData) { + const rawAdvertsData = mediaDataStructure.dataCollectionFromDataContainer(combinedSearchData.catalogResponse.results["ads-result"]); + return searchAdsODML.applyNativeAdvertData(objectGraph, rawAdvertsData, combinedSearchData.sponsoredSearchAdvertData); +} +// endregion +// region Guided Search +/** + * Add models for Guided Search into `SearchResults` model, specifically: + * - `GuidedSearchToken`s + * - `GuidedSearchQuery`s + * - Metrics Container for tokens. + */ +function addModelsForGuidedSearch(objectGraph, searchResultsPage, requestMetadata, facetData, metricsOptions) { + if (!objectGraph.host.isiOS) { + return; + } + const request = requestMetadata.requestDescriptor; + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchRevisions", + }, ""); + const tokens = []; + // Tokens from facet data, if any + if (serverData.isDefinedNonNullNonEmpty(facetData)) { + for (const data of facetData) { + const token = guidedSearch.createGuidedSearchToken(objectGraph, "toggle", request, data, metricsOptions); + if (token) { + tokens.push(token); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + } + } + // Token from selected entity hint, iff there aren't tokens already + if (serverData.isNullOrEmpty(tokens) && requestMetadata.requestDescriptor.searchEntity) { + const entityClearingToken = guidedSearch.createGuidedSearchTokenClearingEntityFilter(objectGraph, requestMetadata.requestDescriptor, metricsOptions); + tokens.push(entityClearingToken); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + const queries = guidedSearch.createGuidedSearchQueries(objectGraph, requestMetadata.requestDescriptor, facetData); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + if (serverData.isDefinedNonNullNonEmpty(tokens)) { + searchResultsPage.guidedSearchTokens = tokens; + searchResultsPage.guidedSearchQueries = queries; + addGuidedSearchParentImpressionMetrics(objectGraph, searchResultsPage, metricsOptions); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); // increment **after** assigning guided token parent impression. + } +} +/// The contextual reason for the segment change +var SegmentChangeReason; +(function (SegmentChangeReason) { + SegmentChangeReason[SegmentChangeReason["EmptyResults"] = 0] = "EmptyResults"; + SegmentChangeReason[SegmentChangeReason["Picker"] = 1] = "Picker"; + SegmentChangeReason[SegmentChangeReason["NonNative"] = 2] = "NonNative"; +})(SegmentChangeReason || (SegmentChangeReason = {})); +/** + * Creates a segment change action for moving from one segment to another for a specific reason + * @param objectGraph The App Store object graph + * @param reason The reason why the segment is changing + * @param pageId The search results page id + * @param metricsOptions The metrics options for the segment this action will attach to + * @param switchingToGroupId The groupId of the segment we are switching to + * @param switchingFromGroupId The groupId of the segment we are switching from (the current segment) + * @returns A segment change action for the specific context and locations + */ +function segmentChangeAction(objectGraph, reason, pageId, metricsOptions, switchingToGroupId, switchingFromGroupId) { + const includeAppsSuffix = reason !== SegmentChangeReason.Picker; + const switchingToSegmentTitle = segmentTitleForSegmentType(objectGraph, includeAppsSuffix, segmentTypeFromGroupId(switchingToGroupId)); + let action; + switch (reason) { + case SegmentChangeReason.EmptyResults: + if (isNothing(switchingToSegmentTitle) || switchingFromGroupId === switchingToGroupId) { + return undefined; + } + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchResults", + }, "emptyResultsSegmentSwitch"); + const emptyResultsTitle = objectGraph.loc + .string("SEARCH_RESULTS_SWITCH_TO_OTHER_RESULTS") + .replace("{platformApps}", switchingToSegmentTitle); + action = new models.SearchPageSegmentChangeAction(switchingToGroupId, switchingToSegmentTitle, new models.StyledText(emptyResultsTitle)); + metricsHelpersClicks.addClickEventToSearchPageSegmentChangeAction(objectGraph, action, "button", metricsOptions.locationTracker); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + break; + case SegmentChangeReason.Picker: + if (isNothing(switchingToSegmentTitle)) { + return undefined; + } + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchResults", + }, "searchResultsSegmentSwitch"); + action = new models.SearchPageSegmentChangeAction(switchingToGroupId, switchingToSegmentTitle); + metricsHelpersClicks.addClickEventToSearchPageSegmentChangeAction(objectGraph, action, "button", metricsOptions.locationTracker); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + break; + case SegmentChangeReason.NonNative: + const currentSegmentType = segmentTypeFromGroupId(switchingFromGroupId); + if (currentSegmentType === segmentTypeForPlatform(objectGraph)) { + return undefined; + } + const switchingFromSegmentTitle = segmentTitleForSegmentType(objectGraph, true, segmentTypeFromGroupId(switchingFromGroupId)); + const nonNativeTitle = objectGraph.loc + .string("Search.Results.ShowingNonNativeResults") + .replace("@@current_platform_apps@@", switchingFromSegmentTitle) + .replace("@@native_platform_apps@@", switchingToSegmentTitle); + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchResults", + }, "nonNativeResultsSegmentSwitch"); + action = new models.SearchPageSegmentChangeAction(switchingToGroupId, switchingToSegmentTitle, new models.StyledText(nonNativeTitle, "text/x-apple-as3-nqml")); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + break; + default: + break; + } + const clickActionOptions = { + actionType: "navigate", + id: pageId, + targetType: "button", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, action, clickActionOptions); + return action; +} +/** + * Gets the segment type for the given group id + * @param groupId The group id of the segment + * @returns The segment type for the group id + */ +function segmentTypeFromGroupId(groupId) { + if (isSome(groupId)) { + switch (groupId) { + case "iOS": + case "ios": + return models.SegmentedSearchResultsPageSegmentType.iOS; + case "xrOS": + case "xros": + return models.SegmentedSearchResultsPageSegmentType.visionOS; + default: + return undefined; + } + } + return undefined; +} +/** + * Gets the segment type for the current platform + * @param objectGraph The App Store object graph + * @returns The segment type for the current platform + */ +function segmentTypeForPlatform(objectGraph) { + switch (objectGraph.client.deviceType) { + case "vision": + return models.SegmentedSearchResultsPageSegmentType.visionOS; + case "pad": + case "phone": + return models.SegmentedSearchResultsPageSegmentType.iOS; + default: + return undefined; + } +} +/** + * Gets the segment title for the given segment type + * @param objectGraph The App Store object graph + * @param includeAppsSuffix Whether to include the word "Apps" at the end of the title + * @param segmentType The segment type we want the title of + * @returns The segment title for the segment type + */ +function segmentTitleForSegmentType(objectGraph, includeAppsSuffix, segmentType) { + switch (segmentType) { + case models.SegmentedSearchResultsPageSegmentType.visionOS: + return includeAppsSuffix + ? objectGraph.loc.string("SEARCH_RESULTS_VISION_APPS_TITLE") + : objectGraph.loc.string("SEARCH_RESULTS_VISION_TITLE"); + case models.SegmentedSearchResultsPageSegmentType.iOS: + return includeAppsSuffix + ? objectGraph.loc.string("SEARCH_RESULTS_IPHONE_IPAD_APPS_TITLE") + : objectGraph.loc.string("SEARCH_RESULTS_IPHONE_IPAD_TITLE"); + default: + return undefined; + } +} +/** + * Gets the group id for the current platform + * @param objectGraph The App Store object graph + * @param segments The segments on the search results page + * @returns The group id for the current platform + */ +function segmentGroupIdForPlatform(objectGraph, segments) { + const nativeSegmentType = segmentTypeForPlatform(objectGraph); + const nativeSegment = segments.find((segment) => { + return segmentTypeFromGroupId(segment.groupId) === nativeSegmentType; + }); + return nativeSegment === null || nativeSegment === void 0 ? void 0 : nativeSegment.groupId; +} +/** + * + * @param objectGraph The App Store Object Graph + * @param searchClearActionType The way the user cleared the search (x in search field or cancel button in toolbar) + * @param pageInformation The metrics page information + * @param locationTracker The metrics location tracker + * @param searchTerm The current search term + * @returns An action to trigger when the search is cancelled or cleared + */ +export function createSearchCancelledOrClearedAction(objectGraph, searchClearActionType, pageInformation, locationTracker, searchTerm) { + const action = new models.BlankAction(); + let type; + let targetId; + switch (searchClearActionType) { + case "cancel": + type = "dismiss"; + targetId = "cancel"; + break; + case "clear": + type = "delete"; + targetId = "clear"; + break; + default: + break; + } + metricsHelpersClicks.addClickEventToSearchCancelOrDismissAction(objectGraph, action, { + targetType: "button", + id: targetId, + idType: undefined, + actionType: type, + pageInformation: pageInformation, + locationTracker: locationTracker, + }, "button", searchTerm); + return action; +} +//# sourceMappingURL=search.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js new file mode 100644 index 0000000..c24e8c3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js @@ -0,0 +1,148 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as searchLandingShelfController from "../landing/search-landing-shelf-controller"; +/** + * The `SearchHistoryShelfToken` type is responsible for plumbing through all + * the data needed to render an incomplete shelf (a shelf that requires a + * lookup) on the focus page. + */ +export class SearchHistoryShelfToken { + constructor(title, maxItems, shelfDisplayStyle, itemDisplayStyle) { + this.title = title; + this.maxItems = maxItems; + this.shelfDisplayStyle = shelfDisplayStyle; + this.itemDisplayStyle = itemDisplayStyle; + } +} +function encodedShelfToken(token) { + return encodeURIComponent(JSON.stringify(token)); +} +export function decodeShelfToken(json) { + if (isNothing(json)) { + return undefined; + } + return JSON.parse(decodeURIComponent(json)); +} +export function createShelfWithContext(objectGraph, pageContext, shelfAttributes) { + const token = new SearchHistoryShelfToken(shelfAttributes.title, shelfAttributes.displayCount, shelfAttributes.displayStyle, shelfAttributes.searchLandingItemDisplayStyle); + return createShelfWithToken(objectGraph, token, pageContext.metricsPageInformation, pageContext.metricsLocationTracker, pageContext.searchHistory); +} +function createShelfItems(objectGraph, metricsPageInformation, metricsLocationTracker, itemDisplayStyle, searchHistory) { + if (serverData.isNullOrEmpty(searchHistory)) { + return []; + } + const items = []; + for (const [historyIndex, historyItem] of searchHistory.entries()) { + const item = createShelfItem(objectGraph, historyItem, historyIndex, metricsPageInformation, metricsLocationTracker, itemDisplayStyle); + if (serverData.isNullOrEmpty(item)) { + continue; + } + items.push(item); + metricsHelpersLocation.nextPosition(metricsLocationTracker); + } + return items; +} +function createShelfURL(token) { + return `${Protocol.internal}:/${Path.searchLandingPage}/${Path.shelf}/?${Parameters.isOnDeviceSearchHistoryShelf}=true&${Parameters.token}=${encodedShelfToken(token)}`; +} +export function createShelfWithToken(objectGraph, token, metricsPageInformation, metricsLocationTracker, searchHistory) { + // Create Items + const items = createShelfItems(objectGraph, metricsPageInformation, metricsLocationTracker, token.itemDisplayStyle, searchHistory); + // Clear History Action + const clearHistoryAction = new models.ClearSearchHistoryAction(); + clearHistoryAction.title = objectGraph.loc.string("Action.ClearSearches"); + metricsHelpersClicks.addClickEventToClearSearchHistoryAction(objectGraph, clearHistoryAction); + // Clear History Sheet Action + const clearHistorySheetAction = new models.SheetAction([clearHistoryAction]); + clearHistorySheetAction.title = objectGraph.loc.string("Sheet.ClearSearches.Title"); + clearHistorySheetAction.message = objectGraph.loc.string("Sheet.ClearSearches.Message"); + clearHistorySheetAction.destructiveActionIndex = 0; + clearHistorySheetAction.isCancelable = true; + // Clear History Compound Action + const clearHistoryShelfAction = new models.CompoundAction([clearHistorySheetAction]); + clearHistoryShelfAction.title = objectGraph.loc.string("Action.Clear"); + // Shelf + const contentType = shelfContentTypeForDisplayStyle(token.shelfDisplayStyle); + const shelf = new models.Shelf(contentType); + shelf.id = "onDeviceSearchHistory"; + shelf.presentationHints = { isWidthConstrained: true }; + // Header + shelf.header = { + title: token.title, + accessoryAction: clearHistoryShelfAction, + }; + // Grid + if (shelf.contentType === "scrollablePill") { + shelf.isHorizontal = true; + shelf.rowsPerColumn = token.shelfDisplayStyle.layoutSize; + } + shelf.contentsMetadata = { + type: "searchFocusTwoColumnList", + numberOfColumns: items.length > 1 ? token.shelfDisplayStyle.layoutSize : 1, + }; + // Content + shelf.items = items; + shelf.isHidden = serverData.isNullOrEmpty(items); + shelf.refreshUrl = createShelfURL(token); + return shelf; +} +function shelfContentTypeForDisplayStyle(displayStyle) { + if (displayStyle.layout === "word_cloud" /* models.GenericSearchPageShelfDisplayStyleLayout.WordCloud */) { + return "scrollablePill"; + } + if (displayStyle.layoutSize === 2) { + // MAINTAINER'S NOTE: Automatically renders as single column in AX text sizes. + return "twoColumnList"; + } + return "singleColumnList"; +} +function createItemTitle(objectGraph, searchTerm, searchEntity) { + if (isNothing(searchEntity)) { + return searchTerm; + } + let formatLocKey; + if (searchEntity === "developer") { + formatLocKey = "Search.ResultsTitle.InDevelopers"; + } + else if (searchEntity === "story") { + formatLocKey = "Search.ResultsTitle.InStories"; + } + else if (searchEntity === "watch") { + formatLocKey = "Search.ResultsTitle.InWatch"; + } + else if (searchEntity === "arcade") { + formatLocKey = "Search.ResultsTitle.InArcade"; + } + if (isNothing(formatLocKey)) { + return searchTerm; + } + return objectGraph.loc.string(formatLocKey).replace("@@search_term@@", searchTerm); +} +function createShelfItem(objectGraph, historyItem, itemIndex, metricsPageInformation, metricsLocationTracker, displayStyle) { + const searchTerm = historyItem.term; + const searchEntity = historyItem.entity; + const searchAction = searchLandingShelfController.createFocusPageSearchAction(objectGraph, createItemTitle(objectGraph, searchTerm, searchEntity), searchTerm, searchEntity, metricsLocationTracker, "recents", undefined, /// MAINTAINER'S NOTE: In the future, we could keep track of the original search source and attribute this recent search to that. + metricsPageInformation, displayStyle); + if (isNothing(searchAction)) { + return null; + } + searchAction.id = historyItem.id; + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAction, { + targetType: "link", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + kind: "link", + softwareType: null, + title: historyItem.term, + hintsEntity: historyItem.entity, + id: `${itemIndex}`, + idType: "sequential", + }); + return searchAction; +} +//# sourceMappingURL=search-history-shelf.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js b/node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js new file mode 100644 index 0000000..c9ee59f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js @@ -0,0 +1,122 @@ +/** + Data Fetching for Sponsored Search. + */ +import { ads } from "../../api/typings/constants"; +import { asString, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { attributeAsDictionary } from "../../foundation/media/attributes"; +import { dataCollectionFromDataContainer, } from "../../foundation/media/data-structure"; +import { allProductVariantIdsForData, productVariantDataForData, productVariantIDForVariantData, } from "../product-page/product-page-variants"; +import { adLogger } from "./search-ads"; +import * as content from "../content/content"; +// region exports +/** + * Fetch the set of processed search ads with the raw adverts in the sequential response. + * @param sponsoredSearchRequestData The request data to fetch processed ads for. + * @param searchTerm The search term to fetch processed adverts for. + * @param fetchResponse The promise that will resolve to the response. + * @returns A promise that will provide `SponsoredSearchNativeAdvertData`. Note that even error conditions are represented in the result for instrumentation purposes. + */ +export async function fetchSponsoredSearchNativeAdvertData(objectGraph, sponsoredSearchRequestData, searchTerm, fetchResponse) { + var _a; + if (!sponsoredSearchRequestData.validAdRequest()) { + adLogger(objectGraph, `ODML fetch skipped - Malformed request`); + return { + adverts: [], + odmlSuccess: false, + }; + } + /** + * This is a weird path where we: + * 1. Fetch response from search + * 2. Pass parts of the response back to native to be modified by SearchAds. + */ + const response = await fetchResponse; + const adverts = sponsoredSearchAdvertsFromResponse(objectGraph, response); + const organics = sponsoredSearchOrganicsFromResponse(objectGraph, response, 1); // per POR, only 1 organic for now. + try { + if (!objectGraph.isAvailable(ads)) { + adLogger(objectGraph, `ODML fetch skipped - Unsupported client`); + return { + adverts: adverts, + odmlSuccess: false, + }; + } + else { + const processedAdverts = await objectGraph.ads.processAdvertsForSponsoredSearch(adverts, organics, searchTerm, objectGraph.bag.sponsoredSearchODMLTimeout, objectGraph.client.isPhone || objectGraph.client.isPad); + if (!processedAdverts.odmlSuccess) { + adLogger(objectGraph, `ODML processing failed`); + } + else { + adLogger(objectGraph, `ODML processing completed`); + } + return { + adverts: (_a = processedAdverts.adverts) !== null && _a !== void 0 ? _a : adverts, + odmlSuccess: processedAdverts.odmlSuccess, + installedStates: processedAdverts.installedStates, + appliedPolicy: processedAdverts.appliedPolicy, + appStates: processedAdverts.appStates, + }; + } + } + catch (e) { + adLogger(objectGraph, `ODML fetch failed - ${e}`); + return { + adverts: adverts, + odmlSuccess: false, + }; + } +} +// endregion +// region internals +/** + * Build the search advert models from the response. + */ +function sponsoredSearchAdvertsFromResponse(objectGraph, response) { + const adverts = dataCollectionFromDataContainer(response.results["ads-result"]); + const bridgedAdverts = []; + for (const ad of adverts) { + const id = asString(ad, "id"); + const adData = attributeAsDictionary(ad, "iads"); + if (isNullOrEmpty(id) || isNullOrEmpty(adData)) { + continue; + } + let productVariantId = null; + let allProductVariantIds = null; + if (objectGraph.bag.enableCPPInSearchAds) { + const productVariantData = productVariantDataForData(objectGraph, ad); + productVariantId = productVariantIDForVariantData(productVariantData); + allProductVariantIds = allProductVariantIdsForData(objectGraph, ad); + } + bridgedAdverts.push({ + instanceId: objectGraph.random.nextUUID(), + adamId: id, + assetInformation: {}, + adData: adData, + cppIds: allProductVariantIds, + serverCppId: productVariantId, + selectedCppId: productVariantId, + appBinaryTraits: content.appBinaryTraitsFromData(objectGraph, ad), + }); + } + return bridgedAdverts; +} +/** + * Build the search organic models from the response, up to `limit`. + */ +function sponsoredSearchOrganicsFromResponse(objectGraph, response, limit) { + const organics = dataCollectionFromDataContainer(response.results.search); + const bridgedOrganics = []; + for (const result of organics) { + const id = asString(result, "id"); + if (isNullOrEmpty(id)) { + continue; + } + bridgedOrganics.push({ + adamId: id, + assetInformation: {}, + }); + } + return bridgedOrganics; +} +// endregion +//# sourceMappingURL=sponsored-search-fetching.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js b/node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js new file mode 100644 index 0000000..28c0491 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js @@ -0,0 +1,23 @@ +import { FlowAction } from "../../api/models"; +import { makeSearchResultsPageIntent, } from "../../api/intents/search-results-page-intent"; +import { getLocale } from "../locale"; +import { makeCanonicalSearchResultsPageUrl } from "./search-page-url"; +/** + * Creates a `FlowAction` destined for the search results page + * + * This will be used by the "web" client to perform search from the search landing page, + * or to change the platform on search results page + */ +export function makeWebSearchAction(objectGraph, platform, term = "") { + const searchAction = new FlowAction("search"); + const destination = makeSearchResultsPageIntent({ + ...getLocale(objectGraph), + platform, + term, + origin: "externalUrl", + }); + searchAction.destination = destination; + searchAction.pageUrl = makeCanonicalSearchResultsPageUrl(objectGraph, destination); + return searchAction; +} +//# sourceMappingURL=web-search-action.js.map \ No newline at end of file -- cgit v1.2.3