diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/builders | |
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/builders')
5 files changed, 1077 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/additional-page-requirement-util.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/additional-page-requirement-util.js new file mode 100644 index 0000000..242d28c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/additional-page-requirement-util.js @@ -0,0 +1,36 @@ +import { isNothing } from "@jet/environment"; +import { allMixed } from "../../foundation/util/promise-util"; +export async function fetchPageWithAdditionalPageRequirements(objectGraph, primaryPageRequirement, additionalRequirements) { + var _a; + // Data Frankensteining Pt 1. - Additional Page Requirements: + // Additional requests that are fired *IN PARALLEL* with page request to fetch additional data needed to render page. + // Why are these not included in a singular page fetch? I don't know. + // This builder API is used to request data that will *definitely* be used, regardless of the content of initial page fetch's response. + const additionalRequirementKeys = Object.keys(additionalRequirements); + const additionalPageRequirementPromises = additionalRequirementKeys.map(async (key) => { + return await additionalRequirements[key]; + }); + const allPageRequirements = await allMixed([primaryPageRequirement, ...additionalPageRequirementPromises]); + const primaryPageDataResult = allPageRequirements[0]; + const primaryPageData = primaryPageDataResult.value; + if (!primaryPageDataResult.success || isNothing(primaryPageData)) { + throw (_a = primaryPageDataResult.error) !== null && _a !== void 0 ? _a : new Error("Unknown primaryPageData error"); + } + const additionalData = {}; + const additionalPageRequirements = allPageRequirements.slice(1); + for (const [index, additionalPageRequirement] of additionalPageRequirements.entries()) { + const additionalRequirementKey = additionalRequirementKeys[index]; + if (additionalPageRequirement.success) { + additionalData[additionalRequirementKey] = additionalPageRequirement.value; + } + else { + additionalData[additionalRequirementKey] = null; + objectGraph.console.log(`Builder - failed to fetch additionalPageRequirement ${additionalRequirementKey}, reason: ${additionalPageRequirement.error}`); + } + } + return { + primaryPageData, + additionalData, + }; +} +//# sourceMappingURL=additional-page-requirement-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/pagination.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/pagination.js new file mode 100644 index 0000000..5f11fc7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/pagination.js @@ -0,0 +1,51 @@ +/** + * Created by km on 10/5/16. + */ +/// The suggested maximum items to load per page. +export const suggestedMaxPerPage = 20; +/** + * Returns a single page of lookup data from a page token. + * @param pageToken The page token to extract the ids from. + * @returns An array of zero or more ids to lookup. + */ +export function getDataForLookup(objectGraph, pageToken) { + if (!pageToken || !pageToken.remainingContent) { + return []; + } + return pageToken.remainingContent.slice(0, pageToken.maxPerPage); +} +/** + * Returns a page token containing the ids to fetch for the next page of data. + * @param pageToken The page token to advance. + * @param count The number of items to get, or all items if not specified + * @returns A new page token. + */ +export function nextPageToken(objectGraph, pageToken, count) { + const nextToken = { ...pageToken }; + if (pageToken && pageToken.remainingContent) { + const nextContent = nextDataRange(objectGraph, pageToken.remainingContent, pageToken.maxPerPage, count); + nextToken.remainingContent = nextContent; + } + return nextToken; +} +/** + * Returns a subset of the content from start to the end of the array + * @param ids The array of IDs from which to get the sub array + * @param start The starting index + * @param count The number of items to get, or all items if not specified + * @returns An array of all ids from start until the end of the array + */ +export function nextDataRange(objectGraph, dataArray, start, count) { + return dataArray.slice(start, count !== null && count !== void 0 ? count : dataArray.length); +} +export function createMediaPageToken(objectGraph, remaining, url, nextHref = null, pageInformation, locationTracker) { + return { + remainingContent: remaining, + maxPerPage: suggestedMaxPerPage, + highestOrdinal: 0, + url: url, + metricsPageInformation: pageInformation, + metricsLocationTracker: locationTracker, + }; +} +//# sourceMappingURL=pagination.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js new file mode 100644 index 0000000..1655d6d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js @@ -0,0 +1,321 @@ +/** + * Created by km on 11/17/16. + */ +/* This file DOES NOT target nodejs usage. */ +import { isNothing, isSome } from "@jet/environment"; +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Parameters } from "../../foundation/network/url-constants"; +import * as routingComponents from "../../foundation/routing/routing-components"; +import * as objects from "../../foundation/util/objects"; +import { allOptional, tryAwait } from "../../foundation/util/promise-util"; +import * as productPageCommon from "../product-page/product-page-common"; +// region Routers +export const pageRouter = makeMetatype("app-store:page-router"); +export class PageRouter { + constructor() { + this.registeredBuilders = new Set(); + this.pageRouter = new routingComponents.UrlRouter(); + this.shelfRouter = new routingComponents.UrlRouter(); + this.paginationRouter = new routingComponents.UrlRouter(); + // endregion + } + /** + * Register a page builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} pageBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerPageBuilder(objectGraph, pageBuilder) { + if (this.registeredBuilders.has(pageBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${pageBuilder.builderClass}`); + } + this.registeredBuilders.add(pageBuilder.builderClass); + this.pageRouter.associate(pageBuilder.pageRoute(objectGraph), pageBuilder); + this.shelfRouter.associate(pageBuilder.shelfRoute(objectGraph), pageBuilder); + this.paginationRouter.associate(pageBuilder.paginationRoute(objectGraph), pageBuilder); + } + /** + * Register a shelf builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} shelfBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerShelfBuilder(objectGraph, shelfBuilder) { + if (this.registeredBuilders.has(shelfBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${shelfBuilder.builderClass}`); + } + this.registeredBuilders.add(shelfBuilder.builderClass); + this.shelfRouter.associate(shelfBuilder.shelfRoute(objectGraph), shelfBuilder); + } + /** + * Register a pagination builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} paginationBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerPaginationBuilder(objectGraph, paginationBuilder) { + if (this.registeredBuilders.has(paginationBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${paginationBuilder.builderClass}`); + } + this.registeredBuilders.add(paginationBuilder.builderClass); + this.paginationRouter.associate(paginationBuilder.paginationRoute(objectGraph), paginationBuilder); + } + // endregion + // region Exported API + /** + * Determine the type of destination for given `url`. + * @param {string} url The url to fetch `flowPage` for. + * @returns {FlowPage} The FlowPage for given `url` + */ + fetchFlowPage(url) { + const routerResult = this.pageRouter.routedObjectForUrl(url); + if (!routerResult.object) { + return `unknown`; + } + const builder = routerResult.object; + // Product URLs can go to `writeReview` via deep links in iOS, so the page type may not necessary be `product` + // So check that they are not routed to the reviews sections + if (builder.builderClass === "ProductBuilder" && + routerResult.parameters[Parameters.action] !== productPageCommon.reviewsAction && + routerResult.parameters[Parameters.action] !== productPageCommon.writeReviewAction) { + return "product"; + } + return builder.pageType(); + } + /** + * Fetch the contents of a page. + * @param objectGraph + * @param url The URL to determine what kind of page to load. + * @param pageType The meta type of the expected page, e.g. `models.ProductPage`. + * This is used to perform a runtime type check to prevent hard to track bugs. + * @returns A promise which will resolve into a page of `pageType`. + */ + async fetchPage(objectGraph, url, pageType) { + return await this.fetchAction(objectGraph, url, null, false).then(async (pageAction) => { + return await new Promise((resolve, reject) => { + if (!pageAction) { + throw new Error(`Promise resolved to null action for: ${url}`); + } + if (pageAction.actionClass === "FlowAction") { + const pageData = pageAction.pageData; + if (!objects.isTypeOf(pageData, pageType)) { + // As we have no data of the correct type, check if we have a redirect + const pageUrl = pageAction.pageUrl; + const isRedirectingToSelf = pageUrl === url; + if (pageUrl && !isRedirectingToSelf) { + // Re-process this fetch with the new URL. + this.fetchPage(objectGraph, pageUrl, pageType) + .then((page) => { + resolve(page); + }) + .catch((error) => { + reject(error); + }); + return; + } + reject(new Error(`pageData is not expected type ${pageType.name}, ${JSON.stringify(pageData)}`)); + return; + } + resolve(pageData); + return; + } + else if (pageAction.actionClass === "TabChangeAction") { + const tabChangeAction = pageAction; + if (tabChangeAction.actions.length === 1 && + tabChangeAction.actions[0].actionClass === "FlowAction") { + const pageData = tabChangeAction.actions[0].pageData; + if (!objects.isTypeOf(pageData, pageType)) { + reject(new Error(`pageData is not expected type ${pageType.name}, ${JSON.stringify(pageData)}`)); + return; + } + resolve(pageData); + return; + } + } + reject(new Error("Action is not a flowAction or a tabChangeAction that contains a single flowAction.")); + }); + }); + } + /** + * Fetch an action for a given url, including page data if there is any to return. + * @param url The URL to determine which actions to take. + * @param referrerData Optional incoming deep link referrer data. + * @param isIncomingURL Whether the fetch is for deep link. + * @param visitedUrls Optional set of URLs already visited in this redirect chain to prevent cycles. + * @returns A promise that will resolve to an Action. + */ + async fetchAction(objectGraph, url, referrerData, isIncomingURL, visitedUrls = new Set()) { + var _a; + const routerResult = this.pageRouter.routedObjectForUrl(url); + if (!routerResult.object) { + // Urls fed into `fetchAction` can redirect to supported routes, thus we attempt to + // chain the redirect and pipe it back into `fetchAction`. + return await this.redirectAndRefetchActionIfPossible(objectGraph, routerResult.normalizedUrl, visitedUrls); + } + const builder = routerResult.object; + return await builder.handlePage(objectGraph, routerResult.normalizedUrl, (_a = routerResult.parameters) !== null && _a !== void 0 ? _a : {}, routerResult.matchedRuleIdentifier, referrerData, isIncomingURL); + } + /** + * Fetch the next page of a page. The returned page will have its `shelves`, `nextPage` properties set. + * @param pageToken The page token to use. + * @returns A promise that will resolve to a page. + */ + async fetchMoreOfPage(objectGraph, pageToken) { + const url = pageToken.url; + if (!url) { + throw new Error("Page token missing required `url` property"); + } + const routerResult = this.paginationRouter.routedObjectForUrl(url); + if (!routerResult.object) { + throw new Error(`fetchMoreOfPage: Unhandled pagination url: ${url}`); + } + const builder = routerResult.object; + return await builder.handlePagination(objectGraph, routerResult.normalizedUrl, routerResult.parameters, routerResult.matchedRuleIdentifier, pageToken); + } + /** + * Fetch the contents of multiple shelves, producing a shelf batch object. + * @param requests A hash of request keys to URL strings. + * @returns A promise that will resolve into multiple shelves. + */ + async fetchShelves(objectGraph, requests) { + // eslint-disable-next-line promise/param-names + const shelfRequestKeys = Object.keys(requests); + const shelfUrls = shelfRequestKeys.map((key) => requests[key]); + // Guaranteed never to throw an error + const shelfForUrl = async (shelfUrl) => { + if (isNothing(shelfUrl)) { + throw new Error(`fetchShelves: Null shelf url`); + } + const routerResult = this.shelfRouter.routedObjectForUrl(shelfUrl); + if (isNothing(routerResult.object)) { + throw new Error(`fetchShelves: Unhandled shelf url: ${shelfUrl}`); + } + const builder = routerResult.object; + const shelf = await builder.handleShelf(objectGraph, routerResult.normalizedUrl, routerResult.parameters, routerResult.matchedRuleIdentifier); + return shelf; + }; + // Map each URL to a promise and make them all optional + const resolvedShelves = await allOptional(shelfUrls.map(shelfForUrl)); + const batch = { + shelves: {}, + errors: {}, + }; + for (const [index, resolvedShelf] of resolvedShelves.entries()) { + const requestKey = shelfRequestKeys[index]; + if (isNothing(requestKey)) { + // This should never happen as we create a shelf request for each key + continue; + } + if (resolvedShelf.success) { + batch.shelves[requestKey] = resolvedShelf.value; + } + else { + // For rejected promises, access the error property + batch.errors[requestKey] = resolvedShelf.error; + } + } + const hasResults = Object.keys(batch.shelves).length > 0; + if (hasResults) { + return batch; + } + else { + const messages = Object.keys(batch.errors).map((key) => batch.errors[key].message); + if (messages.length === 0) { + throw new Error(`Could not load any shelves: ${JSON.stringify(requests)}`); + } + else { + throw new Error(messages.join("\n")); + } + } + } + // endregion + // region Redirect + /** + * Given an `url`, it will fire a GET request and then try to refetch the action for url if + * it was redirected. This is used for urls that are unknown, but redirects to a supported url. + * + * @param objectGraph + * @param {string} url to attempt to redirect with. This is expected to be normalized. + * @param {PromiseResolveFunction<models.Action>} resolve function to resolve promise to an `Action`. + * @param {PromiseRejectFunction} reject function to reject promise to `Error`. + * @param {Set<string>} visitedUrls Set of URLs already visited in this redirect chain to prevent cycles. + */ + async redirectAndRefetchActionIfPossible(objectGraph, url, visitedUrls = new Set()) { + // Check for redirect cycle + const urlString = url.toString(); + if (visitedUrls.has(urlString)) { + throw new Error(`redirectAndRefetchActionIfPossible: Redirect cycle detected for URL: ${urlString}`); + } + // Add current URL to visited set + visitedUrls.add(urlString); + const fetchResult = await tryAwait(objectGraph.network.fetch({ + url: urlString, + method: "GET", + })); + if (!fetchResult.success) { + throw new Error(`redirectAndRefetchActionIfPossible: Failed to fetch page at url: ${url}`); + } + const response = fetchResult.value; + if (this.hasGotoURLInResponse(objectGraph, response)) { + // If the response body contains a goto url fail silently as we will natively try to load the URL. + return new models.BlankAction(); + } + else if (response.status === 200 && response.redirected && response.url) { + // Only route if this was a redirect; otherwise we don't know how to handle this page + return await this.fetchAction(objectGraph, response.url, null, false, visitedUrls); + } + else { + throw new Error(`redirectAndRefetchActionIfPossible: Unhandled page url: ${url}`); + } + } + /** + * Given a `response`, it will check to see if the response body contains a goto URL + * A goto URL is returned by the server in a plist as a way of navigating to a new page + * AMSURLSession which is the native response handler looks for these URLs and tries to load them natively + * @param objectGraph + * @param {FetchResponse} the response body from the server + */ + hasGotoURLInResponse(objectGraph, response) { + if (serverData.isNullOrEmpty(response.body)) { + return false; + } + try { + const responseData = serverData.asJSONValue(objectGraph.plist.parse(response.body)); + const responseActionKind = serverData.asString(responseData, "action.kind"); + const responseActionUrl = serverData.asString(responseData, "action.url"); + if (responseActionKind === "Goto" && isSome(responseActionUrl)) { + return true; + } + return false; + } + catch { + return false; + } + } + // endregion + // region Testing + /** + * Fetch the builder for a given url. + * @param url The URL that the builder is associated with. + * @returns A builder if any was associated, or null if no match was found. + * @note FOR TESTING ONLY. + */ + fetchBuilder(url) { + const routerResult = this.pageRouter.routedObjectForUrl(url) || + this.shelfRouter.routedObjectForUrl(url) || + this.paginationRouter.routedObjectForUrl(url); + return routerResult ? routerResult.object : null; + } +} +//# sourceMappingURL=routing.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping-utils.js new file mode 100644 index 0000000..5d8e1ac --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping-utils.js @@ -0,0 +1,42 @@ +/** + * Adds the required url query params to a media API request for Contingent Items + */ +export function configureContingentItemsForMediaRequest(mediaApiRequest) { + mediaApiRequest.includingScopedRelationships("contingent-items", ["trunk-app", "branch-app", "branch"]); + mediaApiRequest.includingViews(["events-and-offers", "standalone-merchandised-in-apps"]); + mediaApiRequest.includingScopedAttributes("contingent-items", ["name", "subtitle", "additionalTerms"]); + mediaApiRequest.includingMetaKeys("contingent-items:branch", ["contingentItemOffer"]); + mediaApiRequest.includingMetaKeys("contingent-items:association.trunks", ["useTrunkArtwork"]); + mediaApiRequest.includingAssociateKeys("contingent-items", ["trunks"]); +} +/** + * Adds the required url query params to a groupings API request for Contingent Items + */ +export function configureContingentItemsForGroupingRequest(mediaApiRequest) { + mediaApiRequest.includingScopedRelationships("contingent-items", ["trunk-app", "branch-app", "branch"]); + mediaApiRequest.includingScopedAttributes("contingent-items", ["name", "subtitle", "additionalTerms"]); + mediaApiRequest.includingMetaKeys("contingent-items:branch", ["contingentItemOffer"]); + mediaApiRequest.includingMetaKeys("contingent-items:association.trunks", ["useTrunkArtwork"]); + mediaApiRequest.includingAssociateKeys("contingent-items", ["trunks"]); +} +export function configureTagsForMediaRequest(mediaApiRequest) { + mediaApiRequest.enablingFeature("tags"); + mediaApiRequest.includingScopedAttributes("tags", ["editorialArtwork"]); + mediaApiRequest.addingQuery("associate[tags]", "apps"); + mediaApiRequest.includingScopedRelationships("editorial-elements", ["contents", "see-all-contents"]); +} +/** + * Adds the required url query params to a media API request for Winback Offer items + */ +export function configureOfferItemsForMediaRequest(mediaApiRequest) { + mediaApiRequest.includingScopedRelationships("offer-items", ["salables"]); + mediaApiRequest.includingScopedAttributes("offer-items", [ + "title", + "subtitle", + "additionalTerms", + "redemptionExpirationDate", + ]); + mediaApiRequest.includingMetaKeys("offer-items:salables", ["discountOffer"]); + mediaApiRequest.includingKindsKeys("offer-items", ["winback"]); +} +//# sourceMappingURL=url-mapping-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping.js new file mode 100644 index 0000000..fdaa85b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping.js @@ -0,0 +1,627 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as mediaAPI from "@apple-media-services/media-api"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as urlBuilder from "../../foundation/media/url-builder"; +import { Host, Parameters, Path, previewHosts, ProductPageParameters } from "../../foundation/network/url-constants"; +import { configureMediaRequestForGameRecommendations } from "../../gameservicesui/src/foundation/media-api/requests/game-recommendations"; +import * as client from "../../foundation/wrappers/client"; +import { isAdPlacementEnabled } from "../ads/ad-common"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import * as privacySuppression from "../privacy/privacy-suppression"; +import * as productPageVariants from "../product-page/product-page-variants"; +import * as mediaRequestUtils from "./url-mapping-utils"; +import { addDeveloperRequestProperties, makeDeveloperRequest } from "../developer/developer-request"; +import { isReviewSummaryEnabled } from "../product-page/reviews"; +import { isProductAccessibilityLabelsEnabled, shouldSuppressAccessibilityLabelsForAdamId, shouldSuppressAccessibilityLabelsForBundleId, } from "../accessibility/accessibility-common"; +import { buildArticlePageRequest } from "../today/article-request"; +import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent"; +import { getLocale } from "../locale"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +import { MediaApiConfiguration } from "../../gameservicesui/src/foundation/media-api/media-api-configuration"; +const appsResourceAttributes = [ + "description", + "fileSizeByDevice", + "messagesScreenshots", + "minimumOSVersion", + "privacyPolicyUrl", + "promotionalText", + "screenshotsByType", + "supportsFunCamera", + "supportURLForLanguage", + "versionHistory", + "videoPreviewsByType", + "websiteUrl", + "expectedReleaseDateDisplayFormat", + "requirementsByDeviceFamily", + "remoteControllerRequirement", + "backgroundAssetsInfo", + "backgroundAssetsInfoWithOptional", + "supportsSharePlay", + "installSizeByDeviceInBytes", + "miniGamesDeepLink", + "gameDisplayName", +]; +// exporting this will be bad in the long run +// adding for <rdar://problem/39578622> Catagories: Games results in cannot connect to app store. +// should be removed when we stop handling media api urls in the router +export function isMediaUrl(objectGraph, url) { + const host = url.host; + if (serverData.isNull(host)) { + return false; + } + const mediaApiHosts = [ + objectGraph.bag.mediaHost, + objectGraph.bag.mediaEdgeHost(objectGraph), + objectGraph.bag.mediaEdgeSearchHost, + ]; + for (const mediaApiHost of mediaApiHosts) { + if (serverData.isNull(mediaApiHost)) { + continue; + } + if (host.indexOf(mediaApiHost) !== -1) { + return true; + } + } + return false; +} +export function hrefToRoutableUrl(objectGraph, href, additionalQueryParams) { + if (isNothing(href)) { + return undefined; + } + if (preprocessor.GAMES_TARGET) { + const request = new mediaAPI.Request(objectGraph, href); + if (isSome(additionalQueryParams)) { + for (const [key, value] of Object.entries(additionalQueryParams)) { + request.addingQuery(key, value); + } + } + return mediaAPI.buildURLFromRequest(new MediaApiConfiguration(objectGraph), request).toString(); + } + else { + const request = new mediaDataFetching.Request(objectGraph, href).addingQueryValues(additionalQueryParams); + return urlBuilder.buildURLFromRequest(objectGraph, request).toString(); + } +} +// region Grouping +export function mediaApiGroupingURLFromHref(objectGraph, href) { + const attributes = [ + "editorialArtwork", + "editorialVideo", + "isAppleWatchSupported", + "requiredCapabilities", + "minimumOSVersion", + "expectedReleaseDateDisplayFormat", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + // <rdar://problem/56198088> Catalyst apps have clickable 'get' button on 10.14. + // - minimumOSVersion required to filter catalyst apps from groupings. + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, href) + .includingAttributes(attributes) + .includingAgeRestrictions() + .includingMacOSCompatibleIOSAppsWhenSupported(); + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + mediaApiRequest.enablingFeature("appEvents"); + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedRelationships("app-events", ["app"]); + mediaApiRequest.includingScopedRelationships("editorial-item-shelves", ["app-events"]); + } + const context = mediaDataFetching.defaultGroupingContextForClient(objectGraph); + if (serverData.isDefinedNonNull(context)) { + mediaApiRequest.addingQuery("contexts", context); + } + return mediaApiRequest; +} +// endregion +// region Editorial Stories +export function mappedMediaApiEditorialItemURL(objectGraph, url, isIncomingURL) { + const pathComponents = url.pathComponents(); + if (pathComponents.indexOf("story") < 0 && + pathComponents.indexOf("editorialItem") < 0 && + pathComponents.indexOf("viewEditorialItem") < 0 && + url.host !== Host.spamBlockingExtensions && + url.host !== Host.sharePlayApps && + url.host !== Host.buddyOnboarding) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + let editorialItemId; + if (url.host === Host.spamBlockingExtensions) { + // Populate editorial item ID for spam blocking extensions. + editorialItemId = objectGraph.props.asString("spamBlockingExtensionsEditorialItemID"); + } + else if (url.host === Host.sharePlayApps) { + editorialItemId = objectGraph.bag.sharePlayAppsEditorialItemId; + } + else if (url.host === Host.buddyOnboarding) { + editorialItemId = objectGraph.bag.buddyOnboardingEditorialItemId; + } + else { + // Otherwise extract the ID from the URL. + editorialItemId = extractedIdFromURL(url); + } + if (!serverData.isNumber(editorialItemId)) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + if (!serverData.isNumber(editorialItemId)) { + throw new Error(`Unable to map ${url} to a media api url`); + } + const mediaApiRequest = buildArticlePageRequest(objectGraph, makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + id: editorialItemId, + }), isIncomingURL); + const editorialCardId = url.query[Parameters.editorialCardId]; + if (serverData.isDefinedNonNullNonEmpty(editorialCardId)) { + mediaApiRequest.withFilter("canvas:cardId", editorialCardId); + } + const previewParam = url.query[Parameters.preview]; + if (previewHosts.has(url.host) && (previewParam === null || previewParam === void 0 ? void 0 : previewParam.length) > 0) { + mediaApiRequest.addingQuery("preview", previewParam); + mediaApiRequest.isStorePreviewRequest = true; + } + return mediaApiRequest.attributingTo(url.build()); +} +// endregion +// region Product +const entityIdRegex = /id([0-9]+)\/?$/i; +export function extractedIdFromURL(url) { + // 1. Look at the query parameter for the id + let extractedId = url.query["id"]; + // 2. Consider the last path component /id<id> + const pathName = url.pathname; + if (serverData.isNull(extractedId) && (pathName === null || pathName === void 0 ? void 0 : pathName.length) > 0) { + const match = entityIdRegex.exec(pathName); + if (match && match.length > 1) { + extractedId = match[1]; + } + } + // 3. At this point, if the ID is not found, try looking for /<id> instead, since this is what the last path + // component looks like in QA now. + if (!serverData.isNumber(extractedId)) { + const pathComponents = url.pathComponents(); + if (serverData.isDefinedNonNullNonEmpty(pathComponents)) { + extractedId = pathComponents[pathComponents.length - 1]; + } + } + return extractedId; +} +const productRedirectionTable = { + "915249334": "1462947752", // rdar://49929244 (SAD: Redirect from the old 2nd party Shortcuts product page to the new 1st party page on iOS 13) +}; +export function mappedMediaApiProductURL(objectGraph, url, includeUnlistedApps = false) { + var _a, _b; + let productId = extractedIdFromURL(url); + if (!serverData.isNumber(productId)) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + // Redirect to a different product if needed + if (productId in productRedirectionTable) { + productId = productRedirectionTable[productId]; + } + let resourceType = "apps"; + let attributes = appsResourceAttributes; + let relationships = []; + if (preprocessor.GAMES_TARGET) { + relationships = ["alternate-apps"]; + } + else { + relationships = [ + "customers-also-bought-apps", + "reviews", + "app-bundles", + "top-in-apps", + "related-editorial-items", + "alternate-apps", + ]; + } + const pathComponents = (_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split("/"); + const isBundle = isSome(pathComponents) && pathComponents.includes(Path.productBundle); + if (isBundle) { + resourceType = "app-bundles"; + attributes = [ + "screenshotsByType", + "videoPreviewsByType", + "minimumOSVersion", + "requirementsByDeviceFamily", + "remoteControllerRequirement", + ]; + relationships = ["apps", "reviews", "related-editorial-items"]; + } + if (!preprocessor.GAMES_TARGET) { + relationships.push("developer-other-apps"); + } + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForAdamId(objectGraph, productId)) { + attributes.push("privacy"); + } + if (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForAdamId(objectGraph, productId)) { + attributes.push("accessibility"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + if (objectGraph.bag.enableLicenses) { + attributes.push("licenses"); + } + // TODO <rdar://problem/49763626> Product Page should not request developer information if supportsArcade is false + // We need to pipe the supportsArcade information from the sidepack, through the lockup, to the productPage action URL + // until then, isArcade will always be null. + // If no arcade affiliation is specified, or the product definitely isArcade, fetch the developer genre. + const isArcade = serverData.asBoolean(url.query[Parameters.isArcade]); + if (isArcade === null || isArcade) { + attributes.push("minPlayers", "maxPlayers", "editorialVideo"); + relationships.push("developer"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + attributes.push("isHighMotion"); + if (objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + attributes.push("spatialControllerRequirement"); + } + } + if (objectGraph.bag.enableSellerInfo) { + attributes.push("sellerInfo"); + } + // Extends the API to include Internet Content Provider Info + if (objectGraph.bag.enableSellerICPAnnotation) { + attributes.push("internetContentProviderInfo"); + } + // Extends the API to include supported Game Center features. + if (objectGraph.bag.gameCenterExtendSupportedFeatures) { + attributes.push("supportedGameCenterFeatures"); + } + // Extends the API to include Review Summary + if (isReviewSummaryEnabled(objectGraph)) { + relationships.push("review-summary"); + } + if (preprocessor.GAMES_TARGET) { + attributes.push("gamesUrl"); + attributes.push("isEligibleForGamesApp"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .withIdOfType(productId, resourceType) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingAgeRestrictions() + .includingRelationships(relationships) + .includingAttributes(attributes) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) + .includingViews(["categorizations"]); + if (!preprocessor.GAMES_TARGET) { + mediaApiRequest.addingRelationshipLimit("reviews", mediaDataFetching.defaultProductPageReviewsLimitForClient(objectGraph)); + } + if (preprocessor.GAMES_TARGET) { + mediaApiRequest.includingScopedRelationships("apps", ["activities", "challenges"]); + mediaApiRequest.includingViews(["customers-also-bought-games", "developer-other-games"]); + mediaApiRequest.enablingFeature("unlistedApps"); + if (objectGraph.debugSettings.enableHighlightsFromProductPageResponse) { + configureMediaRequestForGameRecommendations(mediaApiRequest); + } + } + if (((_b = url.query[Parameters.productVariantID]) === null || _b === void 0 ? void 0 : _b.length) > 0) { + mediaApiRequest.addingQuery(Parameters.productVariantID, url.query[Parameters.productVariantID]); + } + // If a `minExternalVersionId` is specified, pass this through to the MAPI request + const minExternalVersionId = url.query[ProductPageParameters.minExternalVersionId]; + if (serverData.isDefinedNonNull(minExternalVersionId)) { + mediaApiRequest.addingQuery(ProductPageParameters.minExternalVersionId, minExternalVersionId); + } + // Disabling for bundles due to rdar://78542145 ([REG] Pre-Prod JS: SkySeed: App bundle product page does not load) + if (appPromotionsCommon.appEventsAreEnabled(objectGraph) && !isBundle) { + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingScopedRelationships("apps", ["app-events"]); + } + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedAvailableIn("app-events", ["future"]); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureContingentItemsForMediaRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + if (isAdPlacementEnabled(objectGraph, "productPageYMAL")) { + mediaApiRequest.enablingFeature("cabAdSupport"); + } + if (includeUnlistedApps) { + mediaApiRequest.enablingFeature("unlistedApps"); + } + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingRelationships(["merchandised-in-apps"]); + } + return mediaApiRequest.attributingTo(url.build()); +} +// endregion +// region Developer +export function mediaApiDeveloperURLFromHref(objectGraph, href) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, href); + return addDeveloperRequestProperties(objectGraph, mediaApiRequest); +} +export function mappedMediaApiDeveloperURL(objectGraph, url) { + const developerId = extractedIdFromURL(url); + if (!serverData.isNumber(developerId)) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + return makeDeveloperRequest(objectGraph, developerId); +} +// endregion +// region Charts +/** + * Creates a Media API request for one or more charts using provided critera. + * @param objectGraph The App Store object graph. + * @param genreId The genre for the chart request. + * @param charts The list of charts for the request. + * @param ageBandId The age band to include in the request. + * @param clientIdentifier The identifier of the current client. + * @returns A Media API request for one or more charts using provided critera. + */ +export function mediaApiChartRequestForGenre(objectGraph, genreId, charts, ageBandId = null, clientIdentifier = objectGraph.host.clientIdentifier) { + const attributes = ["isAppleWatchSupported", "requiredCapabilities", "minimumOSVersion"]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .forType("charts") + .addingQuery("genre", genreId) + .includingAttributes(attributes) + .addingQuery("types", "apps") + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + if (serverData.isDefinedNonNullNonEmpty(charts)) { + mediaApiRequest.addingQuery(Parameters.charts, charts); + } + if (clientIdentifier === client.watchIdentifier) { + mediaApiRequest.addingContext("watch"); + } + else if (clientIdentifier === client.messagesIdentifier) { + mediaApiRequest.addingContext("messages"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + mediaApiRequest.includingAdditionalPlatforms(["iphone", "ipad"]); + } + if (isSome(ageBandId)) { + mediaApiRequest.addingQuery(Parameters.ages, ageBandId); + } + return mediaApiRequest; +} +export function mediaApiChartsRequest(objectGraph, genreId, ageBandId = null) { + let request = new mediaDataFetching.Request(objectGraph) + .forType("charts") + .addingQuery("types", "apps") + .addingQuery("genre", genreId) + .includingMacOSCompatibleIOSAppsWhenSupported(true); + if (ageBandId) { + request = request.addingQuery("ages", ageBandId); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + request = request.includingAdditionalPlatforms(["iphone", "ipad"]); + } + return request; +} +// endregion +// region Lookups +export function lookupURLForProductId(objectGraph, productId, isPurchasesProduct, productVariantID, isBundle, includeUnlistedApps) { + const attributes = appsResourceAttributes; + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForAdamId(objectGraph, productId)) { + attributes.push("privacy"); + // The "web" client needs the full privacy information in the initial page load; + // unlike other platforms, it does not load the full details on a separate page + if (objectGraph.client.isWeb) { + attributes.push("privacyDetails"); + } + } + if (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForAdamId(objectGraph, productId)) { + attributes.push("accessibility"); + // The web client requires complete accessibility information on initial page load. + // Unlike other platforms that load full details on separate pages, + // the web client displays this information in a modal overlay. + if (objectGraph.client.isWeb) { + attributes.push("accessibilityDetails"); + } + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + attributes.push("isHighMotion"); + if (objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + attributes.push("spatialControllerRequirement"); + } + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + if (objectGraph.bag.enableLicenses) { + attributes.push("licenses"); + } + // Extends the API to include supported Game Center features. + if (objectGraph.bag.gameCenterExtendSupportedFeatures) { + attributes.push("supportedGameCenterFeatures"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .withIdOfType(productId, "apps") + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(attributes) + .includingRelationships([ + "developer", + "customers-also-bought-apps", + "developer-other-apps", + "reviews", + "app-bundles", + "top-in-apps", + "related-editorial-items", + "alternate-apps", + ]) + .addingRelationshipLimit("reviews", mediaDataFetching.defaultProductPageReviewsLimitForClient(objectGraph)) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + if (objectGraph.client.isWeb) { + // Ensure that the "web" client fully hydrates all PDP shelves + mediaApiRequest + .includingScopedSparseLimit("apps:customers-also-bought-apps", 40) + .includingScopedSparseLimit("apps:developer-other-apps", 40) + .includingScopedSparseLimit("apps:related-editorial-items", 40); + // Ensures web can render the DSA information in the annotation shelf + if (objectGraph.bag.enableSellerInfo) { + attributes.push("sellerInfo"); + } + } + if (isPurchasesProduct) { + mediaApiRequest.addingQuery("availability", "redownload"); + } + if (isSome(productVariantID)) { + mediaApiRequest.addingQuery(Parameters.productVariantID, productVariantID); + } + if (includeUnlistedApps) { + mediaApiRequest.enablingFeature("unlistedApps"); + } + // Disabling for bundles due to rdar://78542145 ([REG] Pre-Prod JS: SkySeed: App bundle product page does not load) + if (appPromotionsCommon.appEventsAreEnabled(objectGraph) && !isBundle) { + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingScopedRelationships("apps", ["app-events"]); + } + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedAvailableIn("app-events", ["future"]); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureContingentItemsForMediaRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingRelationships(["merchandised-in-apps"]); + } + // Extends the API to include Review Summary + if (isReviewSummaryEnabled(objectGraph)) { + mediaApiRequest.includingRelationships(["review-summary"]); + } + if (isAdPlacementEnabled(objectGraph, "productPageYMAL")) { + mediaApiRequest.enablingFeature("cabAdSupport"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + return mediaApiRequest; +} +export function lookupURLForBundleId(objectGraph, bundleId, productVariantID, includeUnlistedApps) { + const attributes = appsResourceAttributes; + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForBundleId(objectGraph, bundleId)) { + attributes.push("privacy"); + } + if (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForBundleId(objectGraph, bundleId)) { + attributes.push("accessibility"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + attributes.push("isHighMotion"); + if (objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + attributes.push("spatialControllerRequirement"); + } + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + if (objectGraph.bag.enableLicenses) { + attributes.push("licenses"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .forType("apps") + .withFilter("bundleId", bundleId) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingRelationships([ + "developer", + "customers-also-bought-apps", + "developer-other-apps", + "reviews", + "app-bundles", + "top-in-apps", + "related-editorial-items", + "alternate-apps", + ]) + .includingAttributes(attributes) + .addingRelationshipLimit("reviews", mediaDataFetching.defaultProductPageReviewsLimitForClient(objectGraph)) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + if (isSome(productVariantID)) { + mediaApiRequest.addingQuery(Parameters.productVariantID, productVariantID); + } + if (isAdPlacementEnabled(objectGraph, "productPageYMAL")) { + mediaApiRequest.enablingFeature("cabAdSupport"); + } + if (includeUnlistedApps) { + mediaApiRequest.enablingFeature("unlistedApps"); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingViews(["standalone-merchandised-in-apps"]); + } + else { + mediaApiRequest.includingRelationships(["merchandised-in-apps"]); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + return mediaApiRequest; +} +export function mediaApiProductSeeAllRequest(objectGraph, productId, relationship) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .withIdOfType(productId, "apps") + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) + .includingRelationships([relationship, "apps", "developer"]) + .includingScopedSparseLimit(`apps:${relationship}`, 20); + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureContingentItemsForMediaRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + return mediaApiRequest; +} +// endregion +//# sourceMappingURL=url-mapping.js.map
\ No newline at end of file |
