/** * 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 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: * 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=` 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