import * as models from "../../../api/models"; import * as serverData from "../../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../../foundation/media/attributes"; import * as mediaDataFetching from "../../../foundation/media/data-fetching"; import * as mediaDataStructure from "../../../foundation/media/data-structure"; import * as mediaNetwork from "../../../foundation/media/network"; import { ResponseMetadata } from "../../../foundation/network/network"; import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; import * as urls from "../../../foundation/network/urls"; import * as contentAttributes from "../../content/attributes"; import * as content from "../../content/content"; import * as lockups from "../../lockups/lockups"; import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; import * as metricsHelpersLocation from "../../metrics/helpers/location"; import { GroupingShelfController } from "./grouping-shelf-controller"; import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; import { makeGameCenterHeader, openGamesUIAction } from "../../arcade/arcade-common"; export class GroupingGameCenterPopularWithYourFriendsController extends GroupingShelfController { // region Constructors constructor() { super("GroupingGameCenterPopularWithYourFriendsController"); this.batchGroupKey = "gameCenter"; this.supportedFeaturedContentIds = new Set([ 495 /* groupingTypes.FeaturedContentID.AppStore_PopularWithYourFriendsMarker */, ]); } // endregion // region Shelf Builder shelfRoute(objectGraph) { return [ ...super.shelfRoute(objectGraph), { protocol: Protocol.internal, path: `/${Path.grouping}/${Path.shelf}/{token}`, query: [Parameters.isGameCenterPopularWithYourFriendsShelf], }, ]; } // endregion // region Shelf Creation Prerequisites /** * For a given mediaApiData extract the actual shelfContents array needed to render this shelf * * @param mediaApiData The outer shelfContents object containing the shelf contents */ initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { return { shelfContents: [], }; } /** * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` * needed to render this shelf * * @param objectGraph The App Store dependency graph * @param shelfUrl The url that this controller handled on a secondary fetch * @param parameters The extracted parameters from the shelf url */ async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { const popularWithYourFriendsPromise = objectGraph.gameCenter.fetchGamesPopularWithFriends(this.gameCategoryFilter(shelfToken.gamesFilter), 30); return await popularWithYourFriendsPromise.then(async (response) => { const gameplayRecords = response .map((item) => this.gameplayHistoryFromData(item)) .sort((a, b) => b.records.length - a.records.length); const adamIds = gameplayRecords .filter((record) => this.isCompatibleGameCenterPlatform(objectGraph, record.platformId)) .map((record) => record.adamId); if (adamIds.length === 0) { // One of the few exceptions where TS minifiers cannot derive data container type // without some help, in this case using explicit type for the value passed to promise. const emptyShelfData = { shelfContents: [] }; return await Promise.resolve(emptyShelfData); } // Send payload to MAPI to allow reco to reorder the "Popular with friends" shelf. const request = new mediaDataFetching.Request(objectGraph) // MAPI will refuse to serve this request if there are more than 100 items. .withIdsOfType(adamIds.slice(0, 100), "apps") .includingAgeRestrictions(); groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, request); return await mediaNetwork .fetchData(objectGraph, request, {}) .then((dataContainer) => { const shelfData = { shelfContents: dataContainer.data, responseTimingValues: dataContainer[ResponseMetadata.timingValues], }; return shelfData; }); }); } /** * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type * * @param objectGraph The App Store dependency graph * @param baseShelfToken The base grouping shelf token created by the grouping-controller * @param mediaApiData The outer data object containing the FC properties and data * @param groupingParseContext The parse context for the grouping page so far */ shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { return baseShelfToken; } incompleteShelfFetchStrategy(objectGraph) { return models.IncompleteShelfFetchStrategy.OnPageLoad; } // endregion // region Shelf Creation /** * * @param objectGraph The App Store dependency graph * @param shelfToken The shelf shelfToken for this current shelf creation request * @param shelfData The media api shelfContents array for this shelf * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, * this will be missing when this controller renders a secondary or incomplete shelf fetch. */ _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { if (shelfToken.isFirstRender) { return this.pendingPopularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken); } else { return this.popularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken); } } pendingPopularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken) { const shelf = this.popularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken); const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); shelf.url = groupingShelfUrl.param(Parameters.isGameCenterPopularWithYourFriendsShelf, "true").build(); return shelf; } popularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken) { const shelf = this.popularWithFriendsShelf(objectGraph, shelfData.shelfContents, shelfToken); shelf.mergeWhenFetched = true; shelf.batchGroup = this.batchGroupKey; // Hide when empty. shelf.isHidden = shelf.items.length === 0; // Configure header shelf.header.title = shelfToken.title; shelf.header.subtitle = shelfToken.subtitle; return shelf; } popularWithFriendsShelf(objectGraph, shelfContents, shelfToken) { const shelfStyle = shelfToken.shelfStyle || "mediumLockup"; const shelf = new models.Shelf(shelfStyle); shelf.isHorizontal = true; const maxNumberOfPlayersBeforeSeeAll = objectGraph.client.isTV ? 20 : 12; const items = []; for (let index = 0; index < shelfContents.length; index++) { const data = shelfContents[index]; const lockupOptions = { metricsOptions: { pageInformation: shelfToken.metricsPageInformation, locationTracker: shelfToken.metricsLocationTracker, recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), anonymizationOptions: { anonymizationString: `"GAME"${index + 1}`, }, }, artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfStyle), canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfStyle), shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && shelfToken.isArcadePage, shouldShowFriendsPlayingShowcase: true, }; // GameCenter should *not* be sending us IDs for non-GameCenter apps, but in the odd case that they do, we // we check against the app's metadata to make sure we are displaying GC apps only here. const isGameCenterEnabled = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isGameCenterEnabled"); // It's possible that a pre-order has become available to friends, but not you yet (due to CDN/timezone reasons). const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); if (isPreorder || !isGameCenterEnabled) { continue; } const lockup = lockups.lockupFromData(objectGraph, data, lockupOptions); lockup.clickAction = injectCrossfireFlowForGameCenter(objectGraph, lockup.clickAction); if (serverData.isDefinedNonNull(lockup)) { items.push(lockup); metricsHelpersLocation.nextPosition(lockupOptions.metricsOptions.locationTracker); } } let thresholdForPlatform; switch (objectGraph.client.deviceType) { case "phone": thresholdForPlatform = 2; break; case "pad": thresholdForPlatform = 6; break; case "mac": thresholdForPlatform = 6; break; case "tv": thresholdForPlatform = 6; break; default: thresholdForPlatform = 0; } shelf.header = makeGameCenterHeader(objectGraph); if (items.length < thresholdForPlatform) { shelf.isHidden = true; return shelf; } shelf.items = items.slice(0, maxNumberOfPlayersBeforeSeeAll); shelf.isHidden = false; shelf.batchGroup = "gameCenter"; if (items.length > maxNumberOfPlayersBeforeSeeAll) { // The shelf for the see all page const shelfForSeeAllItems = new models.Shelf("mediumLockup"); shelfForSeeAllItems.items = items; shelfForSeeAllItems.rowsPerColumn = 1; // See all page const seeAllPage = new models.GenericPage([shelfForSeeAllItems]); seeAllPage.title = shelfToken.title; // The action that opens the see all page const seeAllAction = new models.FlowAction("page"); seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); seeAllAction.pageData = seeAllPage; // Metrics metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, null, { pageInformation: shelfToken.metricsPageInformation, locationTracker: shelfToken.metricsLocationTracker, }); // Connect the shelf's seeAllAction groupingShelfControllerCommon.replaceShelfSeeAllAction(objectGraph, shelf, seeAllAction); } shelf.footerTitle = objectGraph.loc.string("Lockup.Footer.GamesApp"); shelf.footerAction = openGamesUIAction(objectGraph); shelf.footerStyle = { $kind: "games", bundleID: "com.apple.games", width: 16, height: 16, }; return shelf; } // endregion // region Helpers /** * Maps GKGamePlatform to client's `deviceType` * @param objectGraph * @param platformId The platform ID as defined by GameCenter's GKGamePlatform */ isCompatibleGameCenterPlatform(objectGraph, platformId) { switch (platformId) { case 1: return objectGraph.client.isiOS; case 2: return objectGraph.client.isMac; case 3: return objectGraph.client.isTV; case 4: return objectGraph.client.isWatch; default: return false; } } gameplayHistoryFromData(data) { const adamId = serverData.asString(data, "adamId"); const platformId = serverData.asNumber(data, "platformId"); const isArcade = serverData.asBooleanOrFalse(data, "isArcade"); const records = this.gameplayHistoryRecordFromData(serverData.asArrayOrEmpty(data, "records")); return new models.GameCenterGameplayHistory(adamId, platformId, isArcade, records); } gameplayHistoryRecordFromData(data) { return data.map((recordData) => { const playerId = serverData.asString(recordData, "playerId"); const timestamp = serverData.asNumber(recordData, "timestamp"); return new models.GameCenterGameplayHistoryRecord(playerId, timestamp); }); } /** * Convert GameCategoryFilter to GamesFilter. Ideally these would be the same, but seed 1 has already left the station. * @param gamesFilter */ gameCategoryFilter(gamesFilter) { if (gamesFilter === "nonArcade") { return "nonarcade"; } return gamesFilter; } // endregion // region Metrics shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { return { ...baseMetricsOptions, badges: { gameCenter: true, }, idType: "its_contentId", }; } } /** * For evaluating how we attribute referral from Game Center shelves to the product page * We going to inject a hardcoded refApp `com.apple.gamecenter.from.browse` through crossfire pipeline for tracking the page event and buy action * * - Modify the FlowAction to include the GameCenter ReferrerData. * - Replace the click action with a compound action of CrossfireReferralAction + FlowAction */ export function injectCrossfireFlowForGameCenter(objectGraph, clickAction) { if (["iOS", "macOS", "tvOS"].includes(objectGraph.host.platform) && clickAction instanceof models.FlowAction) { const gameCenterCrossfireReferrerData = { app: "com.apple.gamecenter.from.browse", kind: { name: "gameCenter", }, }; clickAction.referrerData = gameCenterCrossfireReferrerData; return new models.CompoundAction([ new models.CrossfireReferralAction(gameCenterCrossfireReferrerData), clickAction, ]); } else { return clickAction; } } //# sourceMappingURL=grouping-game-center-popular-with-your-friends-shelf-controller.js.map