summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/search/search.js
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/search/search.js
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/search/search.js')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/search/search.js1024
1 files changed, 1024 insertions, 0 deletions
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