import * as validation from "@jet/environment/json/validation"; import * as models from "../../api/models"; import * as serverData from "../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../foundation/media/attributes"; import * as mediaDataStructure from "../../foundation/media/data-structure"; import * as mediaRelationship from "../../foundation/media/relationships"; import { Path, Protocol } from "../../foundation/network/url-constants"; import * as client from "../../foundation/wrappers/client"; import { watchosDeveloperRelationshipKey } from "./developer-request"; import * as content from "../content/content"; import * as lockups from "../lockups/lockups"; import * as metricsHelpersClicks from "../metrics/helpers/clicks"; import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; import * as metricsHelpersLocation from "../metrics/helpers/location"; import * as metricsHelpersPage from "../metrics/helpers/page"; import * as room from "../room/room-common"; export class DeveloperRoomToken extends room.RoomPageToken { } /// Ordering of shelves for macOS developer page per Relationship key. /// @seealso mediaUrlMapping.macOSDeveloperRelationshipKeys const macosDeveloperRelationshipOrder = [ "latest-release-app", "arcade-apps", "app-bundles", "mac-apps", "mac-os-compatible-ios-apps", ]; /// Ordering of shelves for iOS developer page per Relationship key. /// @seealso mediaUrlMapping.iosDeveloperRelationshipKeys const iosDeveloperRelationshipOrder = [ "latest-release-app", "arcade-apps", "system-apps", "app-bundles", "ios-apps", "imessage-apps", "watch-apps", "atv-apps", ]; /// The threshold for when to show the see all button. const seeAllThreshold = 8; /// Ordering of shelves for macOS developer page per Relationship key. /// @seealso mediaUrlMapping.macOSDeveloperRelationshipKeys const visionOSDeveloperRelationshipOrder = [ "latest-release-app", "xros-apps", "arcade-apps", "ios-apps", ]; /// Ordering of shelves for the web developer page per Relationship key. /// @seealso mediaUrlMapping.macOSDeveloperRelationshipKeys const webDeveloperRelationshipOrder = [ "latest-release-app", "system-apps", "app-bundles", "ios-apps", "mac-apps", "arcade-apps", "xros-apps", "atv-apps", "watch-apps", "imessage-apps", ]; export class DeveloperPageShelfToken { } export function developerPageFromResponse(objectGraph, response) { return validation.context("developerPageFromResponse", () => { const developerData = response.data.length ? response.data[0] : null; if (!developerData) { return null; } const metricsPageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "Artist", developerData.id, response); const locationTracker = metricsHelpersLocation.newLocationTracker(); const shelves = shelvesForDeveloperData(objectGraph, developerData, metricsPageInformation, locationTracker); // A single shelf should be vertical if (shelves.length === 1) { shelves[0].isHorizontal = false; } // Add developer description const itunesNotes = content.notesFromData(objectGraph, developerData, "standard"); if (itunesNotes) { const paragraph = new models.Paragraph(itunesNotes, "text/x-apple-as3-nqml"); const shelf = new models.Shelf("paragraph"); shelf.items = [paragraph]; shelves.unshift(shelf); } // Create the page const page = new models.GenericPage(shelves); page.title = mediaAttributes.attributeAsString(developerData, "name"); if (objectGraph.client.deviceType !== "watch") { page.presentationOptions = ["prefersLargeTitle"]; } page.canonicalURL = mediaAttributes.attributeAsString(developerData, "url"); // Setup metrics metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, metricsPageInformation); // Add the uber const uber = mediaAttributes.attributeAsDictionary(developerData, "editorialArtwork.bannerUber"); if (uber && !objectGraph.client.isVision) { const uberArtwork = content.artworkFromApiArtwork(objectGraph, uber, { cropCode: "sr", useCase: 21 /* content.ArtworkUseCase.Uber */, }); page.uber = uberArtwork; if (objectGraph.client.isiOS || objectGraph.client.isWeb) { const uberShelf = new models.Shelf("uber"); const uberModel = new models.Uber("above"); uberModel.artwork = uberArtwork; uberShelf.items = [uberModel]; uberModel.title = page.title; shelves.unshift(uberShelf); page.presentationOptions.push("prefersNonStandardBackButton"); if (!objectGraph.client.isWeb) { page.presentationOptions.push("prefersOverlayedPageHeader"); } } } return page; }); } function shelvesForDeveloperData(objectGraph, data, metricsPageInformation, locationTracker) { switch (objectGraph.client.deviceType) { case "mac": return macosShelvesForDeveloperData(objectGraph, data, metricsPageInformation, locationTracker); case "watch": return flatShelvesForDeveloperData(objectGraph, "smallLockup", data, watchosDeveloperRelationshipKey, objectGraph.loc.string("DEVELOPER_WATCH"), metricsPageInformation, locationTracker); case "vision": return orderedShelvesForDeveloperData(objectGraph, data, visionOSDeveloperRelationshipOrder, metricsPageInformation, locationTracker); case "web": return orderedShelvesForDeveloperData(objectGraph, data, webDeveloperRelationshipOrder, metricsPageInformation, locationTracker); default: return orderedShelvesForDeveloperData(objectGraph, data, iosDeveloperRelationshipOrder, metricsPageInformation, locationTracker); } } /** * Returns the iOS shelf title for a given relationship. * @param relationshipKey The relationship key. * @param developerData Media API data for the developer page. * @returns The localized shelf title. */ function iosShelfTitle(objectGraph, relationshipKey, developerData) { switch (relationshipKey) { case "latest-release-app": return objectGraph.loc.string("DEVELOPER_LATEST_RELEASE"); case "system-apps": return objectGraph.loc.string("DEVELOPER_SYSTEM_APPS"); case "imessage-apps": return objectGraph.loc.string("DEVELOPER_IMESSAGE"); case "watch-apps": return objectGraph.loc.string("DEVELOPER_WATCH"); case "atv-apps": return objectGraph.loc.string("DEVELOPER_TV"); case "app-bundles": return objectGraph.loc.string("DEVELOPER_BUNDLES"); case "xros-apps": return objectGraph.loc.string("DEVELOPER_VISION"); case "ios-apps": const hasApps = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasApps"); const hasGames = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasGames"); if (hasApps && hasGames) { return objectGraph.loc.string("DEVELOPER_APPS_AND_GAMES"); } else if (hasGames) { return objectGraph.loc.string("DEVELOPER_GAMES"); } else { return objectGraph.loc.string("DEVELOPER_APPS"); } case "arcade-apps": return objectGraph.loc.string("DEVELOPER_APPLE_ARCADE"); default: return null; } } /** * Returns the web shelf title for a given relationship. * @param relationshipKey The relationship key. * @param developerData Media API data for the developer page. * @returns The localized shelf title. */ function webShelfTitle(objectGraph, relationshipKey, developerData) { switch (relationshipKey) { case "latest-release-app": return objectGraph.loc.string("DEVELOPER_LATEST_RELEASE"); case "system-apps": return objectGraph.loc.string("DEVELOPER_SYSTEM_APPS"); case "imessage-apps": return objectGraph.loc.string("DEVELOPER_IMESSAGE"); case "watch-apps": return objectGraph.loc.string("DEVELOPER_WATCH"); case "atv-apps": return objectGraph.loc.string("DEVELOPER_TV"); case "app-bundles": return objectGraph.loc.string("DEVELOPER_BUNDLES"); case "xros-apps": return objectGraph.loc.string("DEVELOPER_VISION"); case "ios-apps": return objectGraph.loc.string("DEVELOPER_PHONE_PAD_APPS"); case "arcade-apps": return objectGraph.loc.string("DEVELOPER_APPLE_ARCADE"); case "mac-apps": return objectGraph.loc.string("DEVELOPER_MAC_APPS"); case "mac-os-compatible-ios-apps": return objectGraph.loc.string("DEVELOPER_PHONE_PAD_APPS"); default: return null; } } function orderedShelvesForDeveloperData(objectGraph, developerData, developerRelationshipOrdering, metricsPageInformation, locationTracker) { var _a, _b; if (objectGraph.host.isiOS) { // Filter duplicate Arcade apps on iOS filterDuplicateApps(developerData, "arcade-apps", ["ios-apps", "atv-apps"]); } let shelfContentType; let artworkUseCase; switch (objectGraph.client.deviceType) { case "tv": shelfContentType = "mediumLockup"; artworkUseCase = 2 /* content.ArtworkUseCase.LockupIconMedium */; break; case "web": shelfContentType = "mediumLockup"; artworkUseCase = 2 /* content.ArtworkUseCase.LockupIconMedium */; break; default: shelfContentType = "smallLockup"; artworkUseCase = 1 /* content.ArtworkUseCase.LockupIconSmall */; break; } let shelfCount = 0; const shelves = []; for (const relationship of developerRelationshipOrdering) { const dataContainer = mediaRelationship.relationship(developerData, relationship); const sectionData = serverData.asArrayOrEmpty(dataContainer, "data"); const contentCount = sectionData.length; if (contentCount === 0) { continue; } // Skip the latest release shelf if there are no items // Note: This typically happens on the Apple developer page // if the latest release is a system app that you're not eligible for if (relationship === "latest-release-app" && contentCount === 0) { continue; } // Setup some content specific options let clientIdentifier; if (relationship === "imessage-apps") { clientIdentifier = client.messagesIdentifier; } else if (relationship === "watch-apps") { clientIdentifier = client.watchIdentifier; } else if (relationship === "atv-apps") { clientIdentifier = client.tvIdentifier; } else { clientIdentifier = client.appStoreIdentifier; } // Determine the title let shelfTitle; if (objectGraph.client.isWeb) { shelfTitle = webShelfTitle(objectGraph, relationship, developerData); } else { shelfTitle = iosShelfTitle(objectGraph, relationship, developerData); } // Create a metrics context metricsHelpersLocation.pushContentLocation(objectGraph, { pageInformation: metricsPageInformation, locationTracker: locationTracker, idType: "sequential", id: `${shelfCount}`, targetType: "swoosh", }, shelfTitle); // Create the shelf const listOptions = { lockupOptions: { metricsOptions: { pageInformation: metricsPageInformation, locationTracker: locationTracker, }, clientIdentifierOverride: clientIdentifier, artworkUseCase: artworkUseCase, }, filter: 76532 /* filtering.Filter.DeveloperPage */, }; // Determine the ids we need to load const remainingData = sectionData.filter((data) => { return serverData.isNullOrEmpty(data.attributes); }); // Vision doesn't support See All on developer page shelves currently. const shouldShowSeeAll = (((_a = dataContainer.next) === null || _a === void 0 ? void 0 : _a.length) > 0 || contentCount >= seeAllThreshold) && !(objectGraph.client.isVision || objectGraph.client.isWeb); const shelf = shelfForData(objectGraph, shelfTitle, developerData.id, sectionData, relationship, shelfContentType, listOptions, metricsPageInformation, locationTracker, dataContainer.href, shouldShowSeeAll); // Ensure we're not too high const itemCount = shelf.items.length + remainingData.length; if (objectGraph.client.isVision) { if (itemCount < 5) { shelf.rowsPerColumn = 1; } else if (itemCount < 10) { shelf.rowsPerColumn = 2; } else { shelf.rowsPerColumn = 3; } } else if (objectGraph.client.isWeb) { shelf.rowsPerColumn = itemCount > 3 ? 2 : 1; } else if (itemCount < 3) { shelf.rowsPerColumn = itemCount; } // Add metrics before serializing token for url const shelfMetricsOptions = { id: null, kind: null, softwareType: null, targetType: "swoosh", title: shelf.title, pageInformation: metricsPageInformation, locationTracker: locationTracker, idType: null, }; metricsHelpersLocation.popLocation(locationTracker); metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); metricsHelpersLocation.nextPosition(locationTracker); if (remainingData.length) { const token = new DeveloperPageShelfToken(); token.title = shelfTitle; token.developerId = developerData.id; token.contentType = shelfContentType; token.remainingData = remainingData; token.lockupListOptions = listOptions; token.relationship = relationship; token.roomUrl = dataContainer.href; token.shouldShowSeeAll = shouldShowSeeAll; token.hasExistingContent = serverData.isDefinedNonNullNonEmpty(shelf.items); shelf.url = `${Protocol.internal}:/${Path.developer}/${Path.shelf}/` + encodeURIComponent(JSON.stringify(token)); } // Don't add the shelf if there are no items in it, and there is no more content to fetch. if (shelf.items.length > 0 || ((_b = shelf.url) === null || _b === void 0 ? void 0 : _b.length) > 0) { shelves.push(shelf); shelfCount++; } } return shelves; } /** * Returns the macOS shelf title for a given relationship. * @param relationshipKey The relationship key. * @param developerData Media API data for the developer page. * @returns The localized shelf title. */ function macosShelfTitle(objectGraph, relationshipKey, developerData) { switch (relationshipKey) { case "latest-release-app": return objectGraph.loc.string("DEVELOPER_LATEST_RELEASE"); case "app-bundles": return objectGraph.loc.string("DEVELOPER_BUNDLES"); case "mac-apps": if (objectGraph.appleSilicon.isSupportEnabled) { return objectGraph.loc.string("DEVELOPER_MAC_APPS"); } else { const hasApps = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasApps"); const hasGames = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasGames"); if (hasApps && hasGames) { return objectGraph.loc.string("DEVELOPER_APPS_AND_GAMES"); } else if (hasGames) { return objectGraph.loc.string("DEVELOPER_GAMES"); } else { return objectGraph.loc.string("DEVELOPER_APPS"); } } case "mac-os-compatible-ios-apps": return objectGraph.loc.string("DEVELOPER_PHONE_PAD_APPS"); case "arcade-apps": return objectGraph.loc.string("DEVELOPER_APPLE_ARCADE"); default: return null; } } function macosShelvesForDeveloperData(objectGraph, developerData, metricsPageInformation, locationTracker) { var _a; // Filter duplicate apps if (objectGraph.appleSilicon.isSupportEnabled) { filterDuplicateApps(developerData, "arcade-apps", ["mac-apps", "mac-os-compatible-ios-apps"]); filterDuplicateApps(developerData, "mac-apps", ["mac-os-compatible-ios-apps"]); } else { filterDuplicateApps(developerData, "arcade-apps", ["mac-apps"]); } const shelfContentType = "smallLockup"; const artworkUseCase = 1 /* content.ArtworkUseCase.LockupIconSmall */; let shelfCount = 0; const shelves = []; for (const relationship of macosDeveloperRelationshipOrder) { const dataContainer = mediaRelationship.relationship(developerData, relationship); const sectionData = serverData.asArrayOrEmpty(dataContainer, "data"); const contentCount = sectionData.length; if (contentCount === 0) { continue; } // Skip the latest release shelf if there are no items // Note: This typically happens on the Apple developer page // if the latest release is a system app that you're not eligible for if (relationship === "latest-release-app" && contentCount === 0) { continue; } // Determine the title const shelfTitle = macosShelfTitle(objectGraph, relationship, developerData); // Create a metrics context metricsHelpersLocation.pushContentLocation(objectGraph, { pageInformation: metricsPageInformation, locationTracker: locationTracker, idType: "sequential", id: `${shelfCount}`, targetType: "swoosh", }, shelfTitle); // Create the shelf const listOptions = { lockupOptions: { metricsOptions: { pageInformation: metricsPageInformation, locationTracker: locationTracker, }, artworkUseCase: artworkUseCase, }, filter: 76532 /* filtering.Filter.DeveloperPage */, }; // Determine the ids we need to load const remainingData = sectionData.filter((data) => { return serverData.isNullOrEmpty(data.attributes); }); const shouldShowSeeAll = ((_a = dataContainer.next) === null || _a === void 0 ? void 0 : _a.length) > 0 || contentCount >= seeAllThreshold; const shelf = shelfForData(objectGraph, shelfTitle, developerData.id, sectionData, relationship, shelfContentType, listOptions, metricsPageInformation, locationTracker, dataContainer.href, shouldShowSeeAll); // Ensure we're not too high const itemCount = shelf.items.length + remainingData.length; if (itemCount < 3) { shelf.rowsPerColumn = itemCount; } // Add metrics before serializing token for url const shelfMetricsOptions = { id: null, kind: null, softwareType: null, targetType: "swoosh", title: shelf.title, pageInformation: metricsPageInformation, locationTracker: locationTracker, idType: null, }; metricsHelpersLocation.popLocation(locationTracker); metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); metricsHelpersLocation.nextPosition(locationTracker); if (remainingData.length) { const token = new DeveloperPageShelfToken(); token.title = shelfTitle; token.developerId = developerData.id; token.contentType = "smallLockup"; token.remainingData = remainingData; token.lockupListOptions = listOptions; token.relationship = relationship; token.roomUrl = dataContainer.href; token.shouldShowSeeAll = shouldShowSeeAll; token.hasExistingContent = serverData.isDefinedNonNullNonEmpty(shelf.items); shelf.url = `${Protocol.internal}:/${Path.developer}/${Path.shelf}/` + encodeURIComponent(JSON.stringify(token)); } // Don't add the shelf if there are no items in it if (shelf.items.length > 0) { shelves.push(shelf); shelfCount++; } } return shelves; } /** * Creates a list of apps for the given relationship type. */ function flatShelvesForDeveloperData(objectGraph, contentType, developerData, relationshipType, shelfTitle, metricsPageInformation, locationTracker) { const dataCollection = mediaRelationship.relationshipCollection(developerData, relationshipType); const listOptions = { lockupOptions: { metricsOptions: { pageInformation: metricsPageInformation, locationTracker: locationTracker, }, artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, }, filter: 76532 /* filtering.Filter.DeveloperPage */, }; // Create a metrics context metricsHelpersLocation.pushContentLocation(objectGraph, { pageInformation: metricsPageInformation, locationTracker: locationTracker, idType: "sequential", id: `0`, targetType: "swoosh", }, shelfTitle); const shelf = shelfForData(objectGraph, shelfTitle, developerData.id, dataCollection, relationshipType, contentType, listOptions, metricsPageInformation, locationTracker, null, false); // Determine the ids we need to load const remainingData = dataCollection.filter((data) => { return serverData.isNullOrEmpty(data.attributes); }); // Add metrics before serializing token for url const shelfMetricsOptions = { id: null, kind: null, softwareType: null, targetType: "swoosh", title: shelfTitle, pageInformation: metricsPageInformation, locationTracker: locationTracker, idType: null, }; metricsHelpersLocation.popLocation(locationTracker); metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); metricsHelpersLocation.nextPosition(locationTracker); if (remainingData.length) { const token = new DeveloperPageShelfToken(); token.title = shelfTitle; token.developerId = developerData.id; token.contentType = contentType; token.remainingData = remainingData; token.lockupListOptions = listOptions; token.hasExistingContent = serverData.isDefinedNonNullNonEmpty(shelf.items); shelf.url = `${Protocol.internal}:/${Path.developer}/${Path.shelf}/` + encodeURIComponent(JSON.stringify(token)); } return [shelf]; } export function shelfForData(objectGraph, title, developerId, dataArray, developerRelationship, contentType, listOptions, pageInformation, locationTracker, roomUrl, includeSeeAll) { const shelf = new models.Shelf(contentType); shelf.title = title; switch (contentType) { case "screenshotsLockup": shelf.items = lockups.screenshotsLockupsFromData(objectGraph, dataArray, listOptions); shelf.isHorizontal = false; shelf.presentationHints = { showSupplementaryText: false }; break; case "smallLockup": default: shelf.items = lockups.lockupsFromData(objectGraph, dataArray, listOptions); shelf.isHorizontal = objectGraph.client.deviceType !== "watch"; break; } if (includeSeeAll) { // Create token for the see all room const roomToken = new DeveloperRoomToken(); roomToken.title = title; roomToken.url = roomUrl; roomToken.developerId = developerId; roomToken.relationshipId = developerRelationship; roomToken.clientIdentifierOverride = listOptions.lockupOptions.clientIdentifierOverride; const seeAllAction = new models.FlowAction("page"); seeAllAction.pageUrl = developerRoomUrlWithToken(objectGraph, roomToken); seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); seeAllAction.pageData = room.seeAllPage(objectGraph, title); metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, seeAllAction.pageUrl, { pageInformation, locationTracker, }); shelf.seeAllAction = seeAllAction; } return shelf; } /** * Determines the URL to use for the developer room page. * @param {string} token The token to use. * @returns {string} The string to use for the developer room page. */ function developerRoomUrlWithToken(objectGraph, token) { if (!serverData.isDefinedNonNull(token)) { return null; } return `${Protocol.internal}:/${Path.developer}/${Path.room}/` + encodeURIComponent(JSON.stringify(token)); } /** * Prune duplicate entries for apps that can appear in other shelves by modifying `developerData` in place. * For example if `arcade-apps` relation contains id `12345`, we want to prune `12345` from shelves like "Games" and "Apple TV" */ function filterDuplicateApps(developerData, authoritativeRelationshipKey, filteredRelationshipKeys) { const arcadeRelationship = mediaRelationship.relationship(developerData, authoritativeRelationshipKey); if (serverData.isNull(arcadeRelationship)) { return; // No arcade relationship } const arcadeApps = mediaDataStructure.dataCollectionFromDataContainer(arcadeRelationship); if (serverData.isNull(arcadeApps)) { return; // No arcade relationship data } // IDs to filter from other shelves since they are in arcade shelf const arcadeAppIds = arcadeApps.map((app) => app.id); for (const relationshipKeyToFilter of filteredRelationshipKeys) { const relationshipToFilter = mediaRelationship.relationship(developerData, relationshipKeyToFilter); if (serverData.isNull(relationshipToFilter)) { continue; // Skip if relationship didn't exist } const relationshipDataToFilter = mediaDataStructure.dataCollectionFromDataContainer(relationshipToFilter); if (serverData.isNull(relationshipToFilter)) { continue; // Skip if relationship had no `data` } // Overwrite with filtered data developerData.relationships[relationshipKeyToFilter].data = relationshipDataToFilter.filter((data) => !arcadeAppIds.includes(data.id)); } } //# sourceMappingURL=developer-common.js.map