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/arcade/arcade-common.js | |
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js new file mode 100644 index 0000000..440d350 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js @@ -0,0 +1,441 @@ +/** + * Created by ls on 5/15/18. + */ +import { Color, isSome } from "@jet/environment"; +import * as models from "../../api/models/index"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isDefinedNonNull } from "../../foundation/json-parsing/server-data"; +import * as mediaFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationships from "../../foundation/media/relationships"; +import { Path, Protocol } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as artworkBuilder from "../content/artwork/artwork"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as productPageVariants from "../product-page/product-page-variants"; +import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache"; +import { named } from "../../foundation/util/color-util"; +import { makeRoutableArcadeSeeAllPageIntent } from "../../api/intents/routable-arcade-see-all-page-intent"; +import { getPlatform } from "../preview-platform"; +import { getLocale } from "../locale"; +import { makeArcadeSeeAllCanonicalUrl } from "./arcade-see-all-routing"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +// endregion +// region Arcade Navigation Actions +/** + * Creates a flow action for going to the page to see all Arcade games. + * Defaults to sorting by release date (descending) + * @param {ArcadeSeeAllGamesPageSort} sort The order which the games in response will be sorted in. + * @param metricsPageInformation + * @param metricsLocationTracker + * @returns {FlowAction} Flow action to Arcade see all games page. + */ +export function seeAllArcadeGamesPageFlowAction(objectGraph, sort = "releaseDate", metricsPageInformation, metricsLocationTracker, title = undefined, id = undefined, idType = undefined, targetType = "button") { + const seeAllGamesUrl = urls.URL.fromComponents(Protocol.internal, null, `/${Path.arcadeSeeAllGames}`, { + sort: sort, + }); + const flowAction = new models.FlowAction("arcadeSeeAllGames", seeAllGamesUrl.build()); + flowAction.title = title !== null && title !== void 0 ? title : objectGraph.loc.string("Arcade.SeeAllGames.Button.Title"); + if (objectGraph.client.isWeb) { + const destination = makeRoutableArcadeSeeAllPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + }); + const pageUrl = makeArcadeSeeAllCanonicalUrl(objectGraph, destination); + flowAction.destination = destination; + flowAction.pageUrl = pageUrl; + } + const itemId = id !== null && id !== void 0 ? id : (objectGraph.client.isVision ? "SeeAllGames" : "arcade-see-all-games-button"); + const seeAllClickOptions = { + id: itemId, + idType: idType, + targetType: targetType, + actionType: "navigate", + actionContext: "Arcade", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, seeAllClickOptions); + return flowAction; +} +/** + * Create a flow action for opening the Arcade Subscribe page with optional parameters + * @param context The context in which the Arcade Subscribe page is being opened for, e.g. where the flow action was initiated from. + * @param contextualAppId Optional app ID to associate with flow, if any. This is used for flow into contextual upsell sheet. + * @returns {FlowAction} Flow action to `arcadeSubscribe` page. + */ +export function arcadeSubscribePageFlowAction(objectGraph, context, contextualAppId, purchaseSuccessAction, options) { + var _a, _b, _c, _d; + const upsellRequestInfo = new models.MarketingItemRequestInfo("arcade", context, objectGraph.bag.metricsTopic, contextualAppId); + upsellRequestInfo.purchaseSuccessAction = purchaseSuccessAction; + upsellRequestInfo.carrierLinkSuccessAction = purchaseSuccessAction; + const action = new models.FlowAction("upsellMarketingItem"); + if (isSome((_b = (_a = options === null || options === void 0 ? void 0 : options.pageInformation) === null || _a === void 0 ? void 0 : _a.searchTermContext) === null || _b === void 0 ? void 0 : _b.term)) { + upsellRequestInfo.metricsOverlay["searchTerm"] = (_c = options.pageInformation.searchTermContext) === null || _c === void 0 ? void 0 : _c.term; + } + const metricsIdentifierFields = (_d = objectGraph.metricsIdentifiersCache) === null || _d === void 0 ? void 0 : _d.getMetricsFieldsForTypes([ + MetricsIdentifierType.user, + MetricsIdentifierType.client, + ]); + if (isSome(metricsIdentifierFields)) { + upsellRequestInfo.metricsOverlay = { + ...upsellRequestInfo.metricsOverlay, + ...metricsIdentifierFields, + }; + } + action.pageData = upsellRequestInfo; + if (serverData.isDefinedNonNull(options)) { + metricsHelpersClicks.addClickEventToArcadeBuyInitiateAction(objectGraph, action, options); + } + return action; +} +/** + * Action to open the main Arcade page on each platform. + * Depending on the platform, this can be a tab change action or open action to separate Arcade app. + */ +export function openArcadeMainAction(objectGraph, metricsPageInformation, metricsLocationTracker, popToRoot) { + if (objectGraph.client.isTV) { + return openTVArcadeAppAction(objectGraph); + } + else { + const arcadeTabChangeAction = new models.TabChangeAction("arcade"); + if (serverData.isDefinedNonNull(popToRoot)) { + arcadeTabChangeAction.popToRoot = popToRoot; + } + /* + * Presidio / Yukon timeframe workaround for <rdar://problem/53600942> Allow deserialized TabChangeAction to have use `title` property from JS instead of always using `nil` + * We're wrapping a single `TabChangeAction` within `CompoundAction` since `TabChangeAction` deserialization drops the JS provided title. + * + * Tracking removing this workaround in: + * <rdar://problem/53601182> Arcade: Remove workaround for having a tab change action with a title + */ + return new models.CompoundAction([arcadeTabChangeAction]); + } +} +/** + * Creates an action to open Arcade app on tvOS. + */ +export function openTVArcadeAppAction(objectGraph) { + const url = "com.apple.Arcade://"; + return new models.ExternalUrlAction(url); +} +// endregion +/** + * Creates an action to open GamesUI. + */ +export function openGamesUIAction(objectGraph, target = { playNow: {} }) { + return new models.OpenGamesUIAction(target); +} +/** + * Creates Game Center header. + */ +export function makeGameCenterHeader(objectGraph, title = undefined, subtitle = undefined, useTitleArtwork = undefined) { + let eyebrowArtwork; + if (objectGraph.client.isTV) { + eyebrowArtwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://gamecenter.fill", 16, 16); + } + else { + eyebrowArtwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://GameCenterEyebrow", 16, 16); + } + const isShelfHeaderEnabled = objectGraph.featureFlags.isEnabled("shelf_header"); + const isGameCenterShelfHeaderEnabled = objectGraph.featureFlags.isEnabled("game_center_shelf_header"); + const isGSEUIEnabled = objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e"); + if (isGSEUIEnabled) { + const configuration = { + eyebrowColor: named("secondaryText"), + includeSeparator: !isShelfHeaderEnabled, + prefersShelfHeader: isGameCenterShelfHeaderEnabled, + }; + const eyebrow = objectGraph.loc.string("GAME_CENTER"); + const shelfHeader = { + eyebrow: eyebrow, + eyebrowArtwork: eyebrowArtwork, + eyebrowArtworkType: models.ShelfHeaderArtworkType.Icon, + title: title, + subtitle: subtitle, + configuration: configuration, + }; + return shelfHeader; + } + else { + const configuration = { + eyebrowColor: isGSEUIEnabled ? named("systemBlue") : undefined, + includeSeparator: !isShelfHeaderEnabled, + prefersShelfHeader: isGameCenterShelfHeaderEnabled, + }; + if (isSome(useTitleArtwork) && useTitleArtwork) { + const shelfHeader = { + title: title, + titleArtwork: eyebrowArtwork, + titleArtworkType: models.ShelfHeaderArtworkType.Icon, + subtitle: subtitle, + configuration: configuration, + }; + return shelfHeader; + } + else { + const eyebrow = objectGraph.loc.uppercased(objectGraph.loc.string("GAME_CENTER")); + const shelfHeader = { + eyebrow: eyebrow, + eyebrowArtwork: eyebrowArtwork, + eyebrowArtworkType: models.ShelfHeaderArtworkType.Icon, + title: title, + subtitle: subtitle, + configuration: configuration, + }; + return shelfHeader; + } + } +} +// region Arcade Catalog MAPI Requests +/** + * Base request for all MAPI requests fetching a set of Arcade games from catalog. + * This request has a server-defined implicit limit value (e.g. 100). + * This request should be bare-bones. If additional attributes are needed, it should be chained to this request. + * + * @returns {mediaFetching.Request} Request object for fetching Arcade apps from MAPI catalog endpoint. + */ +export function arcadeAppsRequest(objectGraph, includeComingSoon = false) { + let request = new mediaFetching.Request(objectGraph).forType("arcade-apps").includingAgeRestrictions(); + if (includeComingSoon) { + request = request.addingQuery("with", "comingSoonApps"); + } + // For visionOS, we need icons for bincompat games. + if (objectGraph.client.isVision) { + request = request.includingAdditionalPlatforms(["iphone", "ipad"]); + request.attributeIncludes.add("compatibilityControllerRequirement"); + } + return request; +} +/** + * Request for fetching a set of arcade apps for displaying a set of Arcade Icons, e.g. for Arcade Grouping Footer, Contextual Upsell Icon Grid, and iOS Arcade Showcase. + * This request is a very special request with MAPI data containing only `artwork` attribute. Data is meant to be used *as-is* for showing a set of icons only. + * + * Requirements: + * - Icon Artwork for apps only. Additional metadata should be pruned if possible. + * + * @param limit Limit of apps. This should be configured to fit the view's needs. + */ +export function arcadeAppsRequestForIcons(objectGraph, limit) { + return arcadeAppsRequest(objectGraph) + .withSparseLimit(limit) + .asPartialResponseLimitedToFields(["artwork"]) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); +} +// endregion +// region Arcade Upsell Request +export function arcadeUpsellRequest(objectGraph, context, contextualAppId) { + return arcadeUpsellMarketingItemRequest(objectGraph, context, contextualAppId); +} +export function arcadeUpsellMarketingItemRequest(objectGraph, context, contextualAppId) { + // We always want `context` to be provided, but some callers will provide this value from an messy source, e.g. extracted from URL param. Fall back to `generic` just in case. + if (serverData.isNullOrEmpty(context)) { + context = models.marketingItemContextFromString("generic"); + } + const request = new mediaFetching.Request(objectGraph) + .forType("upsellMarketingItem") + .addingQuery("serviceType", "arcade") + .addingQuery("placement", context) + .includingMetaKeys("marketing-items", ["metrics"]) + .includingRelationships(["contents"]) + .includingAttributes(["marketingArtwork", "marketingVideo"]) + .includingAgeRestrictions(); + // Append app id that is promoted, if any. + if (serverData.isDefinedNonNull(contextualAppId)) { + request.addingQuery("seed", contextualAppId); + } + return request; +} +// endregion +/** + * Grab recently played games. + * @param objectGraph The object graph. + * @param {number} limit The number of recently played games to return. + * @param {boolean} shouldHydrateAppsData Whether the apps data should be fetched from media api. + * @param {number} timeout A timeout in seconds. + * @returns {DataContainer} The media api data container with the recently played games. + */ +export async function getRecentlyPlayedGames(objectGraph, limit = null, shouldHydrateAppsData = false, timeout = null) { + return await new Promise((resolve, reject) => { + const isRecentlyPlayedGamesSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV; + // Check if the client supports recently played games. + if (!isRecentlyPlayedGamesSupported) { + resolve(null); + return; + } + const getRecentlyPlayGamesPromise = objectGraph.arcade.getRecentlyPlayedGamesWithTimeout(timeout); + // Get recently played games + getRecentlyPlayGamesPromise + .then((adamIds) => { + // Only perform request when there are recently played games. + if (serverData.isNull(adamIds) || adamIds.length === 0) { + resolve(null); + return; + } + // Enforce limit (if any). + if (serverData.isNumber(limit) && adamIds.length > limit) { + adamIds = adamIds.slice(0, limit); + } + if (shouldHydrateAppsData) { + // Fetch data for recently played games. + const attributes = [ + "editorialArtwork", + "editorialVideo", + "description", + "minimumOSVersion", + "minPlayers", + "maxPlayers", + "remoteControllerRequirement", + "requiresGameController", + "supportsSharePlay", + ]; + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + const mediaApiRequest = new mediaFetching.Request(objectGraph) + .withIdsOfType(adamIds, "apps") + .includingAttributes(attributes); + const fetchDataPromise = mediaNetwork.fetchData(objectGraph, mediaApiRequest); + fetchDataPromise.then((dataContainer) => resolve(dataContainer), (reason) => { + objectGraph.console.log(`getRecentlyPlayedGames() failed when calling mediaNetwork.fetchData() with reason: ${reason}`); + resolve(null); + }); + } + else { + // Create an incomplete data container to be fetched later. + const dataContainer = { + data: [], + }; + adamIds.forEach((adamId) => { + dataContainer.data.push({ + id: adamId, + type: "apps", + }); + }); + resolve(dataContainer); + } + }) + .catch((reason) => { + objectGraph.console.log(`getRecentlyPlayedGames() failed with: ${reason}`); + resolve(null); + }); + }); +} +/** + * Convenience function to build a `ArcadeUpsellData` representation from upsell relationship joined to some data. + * @seealso `upsellFromContentsOfUpsellResponse` + * @param objectGraph The object graph + * @param data Data containing `upsell` relationship to build `ArcadeUpsellData` with + */ +export function upsellFromRelationshipOf(objectGraph, data) { + // Data to extract. + let marketingItemData = null; + const upsellDataContainer = objectGraph.client.isVision || preprocessor.GAMES_TARGET + ? mediaRelationships.relationship(data, "contents") + : mediaRelationships.relationship(data, "upsell") || + mediaRelationships.relationship(data, "marketing-items"); + if (serverData.isNullOrEmpty(upsellDataContainer) || serverData.isNullOrEmpty(upsellDataContainer.data)) { + return null; + } + // Create marketing items array from data container. + const marketingItems = upsellDataContainer.data + .map((item) => { + if (item.type === "marketing-items") { + return item; + } + else { + return null; + } + }) + .filter((item) => isDefinedNonNull(item)); + // Return null if there are NOT any marketing items. + if (serverData.isNullOrEmpty(marketingItems)) { + return null; + } + const timeout = objectGraph.bag.marketingItemSelectionTimeout; + // If there is only one marketing item or timeout is zero, set this as the data. + // Otherwise get a single marketing item by calling AMS. + if (marketingItems.length === 1 || timeout === 0) { + marketingItemData = marketingItems[0]; + } + else { + try { + marketingItemData = objectGraph.arcade.getMarketingItemWithTimeout(marketingItems, timeout); + } + catch { + // Default to first item if the call was timed out. + marketingItemData = marketingItems[0]; + } + } + // Return null if marketing item is null. + if (serverData.isNull(marketingItemData)) { + return null; + } + return { + marketingItemData: marketingItemData, + }; +} +/** + * Convenience function to build a `ArcadeUpsellData` representation from contents of a engagement/upsell response. + * This doesn't really belong in + * @seealso `upsellFromRelationshipOf` + * @param arcadeUpsellResponse Response from the engagement/upsell endpoint. + */ +export function upsellFromContentsOfUpsellResponse(objectGraph, arcadeUpsellResponse) { + if (!arcadeUpsellResponse) { + return null; + } + let marketingItemData = null; + const responseDataArray = serverData.asArrayOrEmpty(arcadeUpsellResponse, "results.data"); + if (responseDataArray.length > 0) { + marketingItemData = responseDataArray[0]; + } + /** + * `arcadeUpsellResponse` is expected of form: + * { + * content: { mediaDataStructure.Data } + * } + * matching what is provided via the appropriate `action=<usage>` param from engagement/upsell endpoint. + */ + if (!serverData.isDefinedNonNull(marketingItemData)) { + return null; + } + return { + marketingItemData: marketingItemData, + }; +} +// endregion +// region Arcade Games For You Request +/** + * Request for fetching list of recommended Arcade apps from recommendations API. + * @param objectGraph The App Store object graph. + * @param limit Maximum number of apps to fetch. + * @returns {Request} Request object for fetching recommended Arcade apps from + * personalized recommendations endpoint. + */ +export function arcadeGamesForYouRequest(objectGraph, limit) { + let request = new mediaFetching.Request(objectGraph) + .forType("personal-recommendations") + .addingQuery("sparseLimit[contents]", `${limit}`) + .addingQuery("include[personal-recommendations]", "contents") + .addingQuery("filter[kind]", "arcadeGamesForYou") + .includingAgeRestrictions(); + // For visionOS, we need to include bincompat apps. + if (objectGraph.client.isVision) { + request = request.includingAdditionalPlatforms(["iphone", "ipad"]); + } + return request; +} +// endregion +// The color to use for Arcade content. +export const arcadeColor = Color.fromRGB(1, 90 / 255, 80 / 255); +//# sourceMappingURL=arcade-common.js.map
\ No newline at end of file |
