import { isNothing, isSome } from "@jet/environment"; import * as validation from "@jet/environment/json/validation"; import { FetchTimingMetricsBuilder } from "@jet/environment/metrics"; import * as models from "../../api/models"; import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; import { asArrayOrEmpty, asBoolean, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; import { hasAttributes } from "../../foundation/media/attributes"; import * as mediaDataFetching from "../../foundation/media/data-fetching"; import * as mediaDataStructure from "../../foundation/media/data-structure"; import { fetchData } from "../../foundation/media/network"; import { Parameters, Path } from "../../foundation/network/url-constants"; import { URL } from "../../foundation/network/urls"; import { optional, required } from "../../foundation/util/promise-util"; import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; import { applySearchAdMissedOpportunityToShelvesIfNeeded, eligibleSlotPositionsForAdPlacement, iadInfoFromOnDeviceAdResponse, isAdPlacementEnabled, } from "../ads/ad-common"; import * as adIncidents from "../ads/ad-incident-recorder"; import { fetchAds, parallelOrganicRequestDidFinish } from "../ads/on-device-ad-fetch"; import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; import { fetchPageWithAdditionalPageRequirements } from "../builders/additional-page-requirement-util"; import { pageRouter } from "../builders/routing"; import { shouldUsePrerenderedIconArtwork } from "../content/content"; import { shelfForFooterButtons, shelfForTermsAndConditions, shelfForUnifiedMessage } from "../grouping/grouping-common"; import { newLocationTracker } from "../metrics/helpers/location"; import { iAdInformationFromMediaApiResponse } from "../metrics/helpers/models"; import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../metrics/helpers/page"; import { combinedRecoMetricsDataFromMetricsData } from "../metrics/helpers/util"; import * as onDevicePersonalization from "../personalization/on-device-personalization"; import { fetchTodayRecommendationsWithTimeout, isTodayTabArcadePersonalizationAvailable, } from "../personalization/on-device-recommendations-today"; import { setPreviewPlatform } from "../preview-platform"; import * as productPageVariants from "../product-page/product-page-variants"; import { newPageRefreshControllerFromResponse } from "../refresh/page-refresh-controller"; import { storeCohortIdForUserFromResponse } from "../search/landing/search-landing-cohort"; import { FlattenedTodayItemType, createAdShelfForTodayPageContextIfNecessary, feedPreviewUrlFromFlattenedTodayItems, flattenTodayModules, nextFlattenedItemIsHydrated, todayShelfForEditorialItem, todayShelfForEditorialItemGroup, } from "./today-parse-util"; import { TodayPageContext } from "./today-types"; /** * Keys used to manage additional data needed to render the today page. * * - onboardingCards: key used to fetch the onboarding cards for the feed. * * - ads: Key used for fetching the today placement ads */ export var TodayControllerAdditionalDataKey; (function (TodayControllerAdditionalDataKey) { TodayControllerAdditionalDataKey["OnboardingCards"] = "onboardingCards"; TodayControllerAdditionalDataKey["Ads"] = "ads"; TodayControllerAdditionalDataKey["ODP"] = "ODP"; TodayControllerAdditionalDataKey["AMDData"] = "amdData"; })(TodayControllerAdditionalDataKey || (TodayControllerAdditionalDataKey = {})); export function defaultPlatforms(objectGraph) { return mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph); } /// Resource attributes to include for `editorial-items` fetched for displaying in today page. export function defaultAttributes(objectGraph) { const attributes = [ "editorialArtwork", "editorialVideo", "enrichedEditorialNotes", "minimumOSVersion", "headerName", "headerBadge", "headerTagline", "editorialClientParams", "requiredCapabilities", "shortEditorialNotes", ]; if (objectGraph.bag.enableUpdatedAgeRatings) { attributes.push("ageRating"); } if (shouldUsePrerenderedIconArtwork(objectGraph)) { attributes.push("iconArtwork"); } return attributes; } export function prepareRequest(objectGraph, request, isPageRequest) { var _a, _b; request .includingAgeRestrictions() .includingAdditionalPlatforms(defaultPlatforms(objectGraph)) .includingAttributes(defaultAttributes(objectGraph)) .includingScopedAttributes("editorial-item-groups", ["editorialClientParams"]) .includingScopedRelationships("editorial-items", ["primary-content", "header-contents"]) .includingScopedRelationships("editorial-item-groups", ["header-contents"]) .enablingFeature("editorialItemGroups") .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) .includingRelationshipsForUpsell(true); if (isPageRequest) { request.includingAssociateKeys("editorial-item-groups", ["recommendations", "editorial-cards"]); request.includingAssociateKeys("editorial-items", ["editorial-cards"]); } if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { request.enablingFeature("appEvents"); request.addingQuery("meta", "personalizationData"); request.includingScopedRelationships("app-events", ["app"]); request.includingScopedAttributes("app-events", AppEventsAttributes); } if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { request.enablingFeature("offerItems"); if (isPageRequest) { request.includingKindsKeys("offer-items", ["winback"]); } } if (impressionDemotion.isImpressionDemotionAvailable(objectGraph)) { request.enablingFeature("eiGroupEISelectionOnDevice"); } if (isAdPlacementEnabled(objectGraph, "today")) { request.enablingFeature("adSupport"); } request.enablingFeature("heroStyles"); const displayDeviceDrivenContent = (_b = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.bool("displayDeviceDrivenContent")) !== null && _b !== void 0 ? _b : false; if (objectGraph.bag.enableDeviceDrivenDiscoveryContent && displayDeviceDrivenContent && request.resourceType === "today") { request.addingQuery("pairedDevices", "visionPro"); } } // MARK: - Page Fetching export function generateTodayPageRequest(objectGraph) { // Note: The sparse limit is now 2 because story groups will include all of their content which means // if we continue to use the original 6 count we'll end up with way too many EI's // rdar://108635958 ([Network Launch | 283ms | 19%] App Store - Dawn - D33: 21A236 vs 20E245 network-warm-launch: // extendedLaunchTime is 1731.0ms vs 1448.0ms) const sparseCount = objectGraph.client.isWeb ? 40 : 1; const sparseLimit = objectGraph.client.isWeb ? 40 : 2; const mediaApiRequest = new mediaDataFetching.Request(objectGraph) .forType("today") .withSparseCount(sparseCount) .withSparseLimit(sparseLimit); return setPreviewPlatform(objectGraph, mediaApiRequest); } /** * @param objectGraph The object graph for the AppStore. * @param onboardingCardIds The onboardingCard ids to fetch. * @returns The map of additional page requirements. */ function generateAdditionalPageRequirements(objectGraph, onboardingCardIds) { const additionalRequirements = {}; /** * Currently, to avoid double filtering, the fix for: * If an onboarding card is empty don't add it to the request * depends on the `Request` object filtering IDs from under caller. The caller is expected to perform some validation to * guarantee that this `Request` object is not malformed after it was processed here, and it can still build a valid URL. * * This mechanic is undesired, and once this filtering is removed from `Request`, the caller should filter empty onboarding * IDs via an explicit method instead before instantiating an malformed `Request`. */ if (isDefinedNonNullNonEmpty(onboardingCardIds)) { const onboardingCardsRequest = new mediaDataFetching.Request(objectGraph) .withIdsOfType(onboardingCardIds, "editorial-items") .includingAdditionalPlatforms(defaultPlatforms(objectGraph)) .includingAttributes(defaultAttributes(objectGraph)); additionalRequirements[TodayControllerAdditionalDataKey.OnboardingCards] = optional(fetchData(objectGraph, onboardingCardsRequest, {})); } if (isAdPlacementEnabled(objectGraph, "today")) { additionalRequirements[TodayControllerAdditionalDataKey.Ads] = optional(fetchAds(objectGraph, "today")); } if (isTodayTabArcadePersonalizationAvailable(objectGraph)) { additionalRequirements[TodayControllerAdditionalDataKey.ODP] = optional(fetchTodayRecommendationsWithTimeout(objectGraph, undefined)); } const amsEngagement = objectGraph.amsEngagement; if (amsEngagement && impressionDemotion.isImpressionDemotionAvailable(objectGraph)) { const request = { timeout: 1000, eventType: impressionDemotion.AMSEngagementAppStoreEventKey, tab: "today", }; additionalRequirements[TodayControllerAdditionalDataKey.AMDData] = optional(amsEngagement.performRequest(request)); } return additionalRequirements; } export async function fetchTodayPage(objectGraph, options) { const router = objectGraph.required(pageRouter); const isTodayTestPage = options.url === "x-as3-internal:/today/test"; const isTodayCardPreviewPage = options.url.indexOf(Path.todayCardPreview) !== -1; if (isTodayTestPage) { return await router.fetchPage(objectGraph, options.url, models.TodayPage); } else if (isTodayCardPreviewPage) { const dataFetchUrl = URL.from(options.url); dataFetchUrl.param(Parameters.fetchData, "true"); return await router.fetchPage(objectGraph, dataFetchUrl.build(), models.TodayPage); } else { const request = generateTodayPageRequest(objectGraph); prepareRequest(objectGraph, request, true); const requestHeaders = isSome(options.experimentIdHeader) ? { "X-Apple-User-Experiment-Ids": options.experimentIdHeader } : undefined; return await buildTodayPageFromRequest(objectGraph, request, requestHeaders, options); } } export async function buildTodayPageFromRequest(objectGraph, request, requestHeaders, options) { var _a; const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); const primaryDataFetchPromise = fetchData(modifiedObjectGraph, request, { headers: requestHeaders !== null && requestHeaders !== void 0 ? requestHeaders : undefined, }); // Should this be needed on web, this should have a .catch() added to avoid // an unhandled rejection. if (!objectGraph.client.isWeb) { void primaryDataFetchPromise.then((response) => { if (isAdPlacementEnabled(objectGraph, "today")) { parallelOrganicRequestDidFinish(objectGraph, "today"); } storeCohortIdForUserFromResponse(objectGraph, objectGraph.user.dsid, response); }); } const primaryDataFetchInput = required(primaryDataFetchPromise); const additionalRequirements = generateAdditionalPageRequirements(objectGraph, (_a = options === null || options === void 0 ? void 0 : options.onboardingCardIds) !== null && _a !== void 0 ? _a : []); const pageDataRequirements = await fetchPageWithAdditionalPageRequirements(objectGraph, primaryDataFetchInput, additionalRequirements); return fetchTimingMetricsBuilder.measureModelConstruction(() => { var _a, _b; const onboardingCardsResponse = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.OnboardingCards]; const onboardingCards = mediaDataStructure.dataCollectionFromDataContainer(onboardingCardsResponse); const adsResponse = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.Ads]; const todayRecommendations = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.ODP]; const amdData = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.AMDData]; const flattenedTodayItems = flattenTodayModules(objectGraph, (_b = (_a = pageDataRequirements.primaryPageData.results) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : [], todayRecommendations, onboardingCards, adsResponse); return buildTodayPage(modifiedObjectGraph, flattenedTodayItems, pageDataRequirements.primaryPageData, adsResponse, amdData); }); } // MARK: - Today Page Creation /** * @param objectGraph The dependency graph for the App Store * @param flattenedTodayItems The flattened Today items used to render the page * @param primaryPageData The primary page data for the Today page * @param adsResponse The response from ad platforms * @returns A TodayPage model */ export function buildTodayPage(objectGraph, flattenedTodayItems, primaryPageData, adsResponse, amdResponse) { return validation.context("renderPage", () => { if (isNullOrEmpty(flattenedTodayItems)) { return null; } const pageContext = createTodayPageContext(objectGraph, flattenedTodayItems, primaryPageData, adsResponse, amdResponse); const todayPage = todayPageFromPageContext(objectGraph, pageContext); todayPage.shelves.splice(0, 0, shelfForUnifiedMessage(objectGraph, "todayPageHeader")); return todayPage; }); } /** * From the provided today page context, create a new today page with the hydrated content * @param objectGraph The dependency graph for the App Store * @param pageContext The page context for the Today page */ export function todayPageFromPageContext(objectGraph, pageContext) { var _a; const shelves = []; const feedPreviewUrl = feedPreviewUrlFromFlattenedTodayItems(objectGraph, pageContext.remainingContent); let hasMoreHydratedContent = nextFlattenedItemIsHydrated(pageContext.remainingContent); const isPageLoadNotPaginated = (_a = pageContext.remainingContent[0]) === null || _a === void 0 ? void 0 : _a.isFirstItemInModule; while (hasMoreHydratedContent) { const todayItem = pageContext.remainingContent.shift(); if (isNothing(todayItem)) { hasMoreHydratedContent = false; break; } const preItemAdShelf = createAdShelfForTodayPageContextIfNecessary(objectGraph, pageContext); if (isSome(preItemAdShelf)) { shelves.push(preItemAdShelf); } pageContext.currentTodayItem = todayItem; let todayItemShelf = null; switch (todayItem.type) { case FlattenedTodayItemType.EditorialItem: todayItemShelf = todayShelfForEditorialItem(objectGraph, todayItem, pageContext); break; case FlattenedTodayItemType.EditorialItemGroup: todayItemShelf = todayShelfForEditorialItemGroup(objectGraph, todayItem, pageContext); break; default: break; } pageContext.currentTodayItem = undefined; if (isSome(todayItemShelf)) { shelves.push(todayItemShelf); } const postItemAdShelf = createAdShelfForTodayPageContextIfNecessary(objectGraph, pageContext); if (isSome(postItemAdShelf)) { shelves.push(postItemAdShelf); } hasMoreHydratedContent = nextFlattenedItemIsHydrated(pageContext.remainingContent); } if (isPageLoadNotPaginated) { applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, shelves, "today", "today", pageContext.pageInformation); } const todayPage = new models.TodayPage(shelves); const todayTab = objectGraph.bag.tabsStandard.find((tab) => tab.id === "today"); const todayTabTitle = asString(todayTab, "title"); const tabImageIdentifier = asString(todayTab, "image-identifier"); const includeTitleDetail = isNothing(tabImageIdentifier) || tabImageIdentifier === "doc.text.image"; const todayTitle = todayTabTitle !== null && todayTabTitle !== void 0 ? todayTabTitle : objectGraph.loc.string("PAGE_TITLE_TODAY"); todayPage.title = todayTitle; todayPage.tabTitle = todayTitle; todayPage.titleDetail = includeTitleDetail ? todayPageTitleDetail(objectGraph) : undefined; todayPage.shortTitleDetail = includeTitleDetail ? todayPageShortTitleDetail(objectGraph) : undefined; todayPage.longTitle = includeTitleDetail ? todayPageLongTitle(objectGraph) : undefined; todayPage.feedPreviewUrl = feedPreviewUrl; if (isDefinedNonNullNonEmpty(pageContext.remainingContent)) { pageContext.pageHasDisplayedContent = pageContext.pageHasDisplayedContent || shelves.some((shelf) => { isDefinedNonNullNonEmpty(shelf.items); }); todayPage.nextPage = pageContext; } else if (!objectGraph.client.isWeb) { footerShelvesForTodayPage(objectGraph, pageContext.pageInformation, pageContext.locationTracker).forEach((shelf) => { todayPage.shelves.push(shelf); }); } if (isAdPlacementEnabled(objectGraph, "today")) { // Ad Incidents todayPage.adIncidents = adIncidents.recordedIncidents(objectGraph, pageContext.adIncidentRecorder); } addMetricsEventsToPageWithInformation(objectGraph, todayPage, pageContext.pageInformation); return todayPage; } /** * Fetch the next batch of content for the Today page, and hydrate the page context * @param objectGraph The dependency graph for the App Store * @param pageContext The page context for the Today page * @returns */ export async function hydrateNextBatchOfContent(objectGraph, pageContext) { const batchLoadSize = pageContext.pageHasDisplayedContent ? 6 : 12; let continueAddingContent = true; let unhydratedItemIndex = 0; let unhydratedContents = []; while (continueAddingContent) { const nextContent = pageContext.remainingContent[unhydratedItemIndex]; switch (nextContent.type) { case FlattenedTodayItemType.EditorialItem: unhydratedContents.push(nextContent.data); break; case FlattenedTodayItemType.EditorialItemGroup: unhydratedContents.push(nextContent.data); const groupItems = asArrayOrEmpty(nextContent.data, "meta.associations.recommendations.data"); unhydratedContents = [...unhydratedContents, ...groupItems]; break; default: break; } unhydratedItemIndex++; continueAddingContent = unhydratedContents.length < batchLoadSize && unhydratedItemIndex < pageContext.remainingContent.length; } // MAPI request for module slice's contents const requestedContentIds = new Set(unhydratedContents.map((content) => content.id)); const contentsFetchRequest = new mediaDataFetching.Request(objectGraph, unhydratedContents, true, [ "recommendations", "editorial-cards", ]); prepareRequest(objectGraph, contentsFetchRequest, false); const fetchedData = await fetchData(objectGraph, contentsFetchRequest, { allowEmptyDataResponse: true }); const fetchedDataMap = {}; for (const data of fetchedData.data) { fetchedDataMap[data.id] = data; } // Hydrate the page context with the fetched data for (const content of pageContext.remainingContent) { hydrateFlattenedItemWithFetchedDataMap(content, fetchedDataMap); } /// Now take a pass and remove any items that are still unhydrated after performing the fetch pageContext.remainingContent = pageContext.remainingContent.filter((content) => { return content.isDataHydrated || !requestedContentIds.has(content.data.id); }); return pageContext; } /** * For a given today item, take the fetched MAPI data and apply that to the item. * * @param flattenedItem The today item we're going to be hydrating * @param fetchedDataMap The mapping from id to fetched media api data. */ function hydrateFlattenedItemWithFetchedDataMap(flattenedItem, fetchedDataMap) { var _a, _b, _c; switch (flattenedItem.type) { case FlattenedTodayItemType.EditorialItem: if (isDefinedNonNullNonEmpty(fetchedDataMap[flattenedItem.data.id])) { flattenedItem.data = { ...flattenedItem.data, ...fetchedDataMap[flattenedItem.data.id], }; } flattenedItem.isDataHydrated = hasAttributes(flattenedItem.data); break; case FlattenedTodayItemType.EditorialItemGroup: if (isDefinedNonNullNonEmpty(fetchedDataMap[flattenedItem.data.id])) { flattenedItem.data = { ...flattenedItem.data, attributes: (_a = fetchedDataMap[flattenedItem.data.id]) === null || _a === void 0 ? void 0 : _a.attributes, relationships: (_b = fetchedDataMap[flattenedItem.data.id]) === null || _b === void 0 ? void 0 : _b.relationships, meta: { ...flattenedItem.data.meta, ...(_c = fetchedDataMap[flattenedItem.data.id]) === null || _c === void 0 ? void 0 : _c.meta, }, }; } flattenedItem.isDataHydrated = hasAttributes(flattenedItem.data); break; default: break; } } /** * @param objectGraph The dependency graph for the App Store * @param todayItems The list of all the items displayed on the today page. * @param todayResponse The response from the today endpoint * @param adResponse The response from the ads service * @returns The context to use when parsing today cards */ function createTodayPageContext(objectGraph, todayItems, todayResponse, adsResponse, amdResponse) { var _a, _b, _c; const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Today", "today", todayResponse, null, (_a = iadInfoFromOnDeviceAdResponse(objectGraph, "today", adsResponse, todayItems)) !== null && _a !== void 0 ? _a : iAdInformationFromMediaApiResponse(objectGraph, "today", todayResponse, (_b = adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.onDeviceAd) === null || _b === void 0 ? void 0 : _b.positionInfo, todayItems)); const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); pageInformation.recoMetricsData = combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); const context = new TodayPageContext(todayItems, pageInformation, newLocationTracker(), newPageRefreshControllerFromResponse(todayResponse), impressionDemotion.impressionEventsFromData(objectGraph, amdResponse)); if (isAdPlacementEnabled(objectGraph, "today")) { const eligibleAdPositions = eligibleSlotPositionsForAdPlacement(objectGraph, "today"); if (isSome(eligibleAdPositions)) { // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. context.eligibleAdLocations = eligibleAdPositions.map((position) => position.slot); } const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adsResponse); context.adIncidentRecorder = adIncidentRecorder; context.adPlacementBehavior = getAdPlacementBehaviorForPage(todayItems); if (isNothing(adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.failureReason)) { context.adData = mediaDataStructure.dataFromDataContainer(objectGraph, adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.mediaResponse); const rawPositionInfo = (_c = adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.onDeviceAd) === null || _c === void 0 ? void 0 : _c.positionInfo; if (isDefinedNonNullNonEmpty(rawPositionInfo)) { // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. const adjustedAdLocation = rawPositionInfo.slot - 1; context.adLocation = adjustedAdLocation; } } } return context; } function getAdPlacementBehaviorForPage(todayItems) { let adPlacementBehavior = models.AdPlacementBehavior.insertIntoShelf; for (const todayItem of todayItems) { let todayItemAdPlacementBehavior = adPlacementBehavior; switch (todayItem.type) { case FlattenedTodayItemType.EditorialItem: todayItemAdPlacementBehavior = getAdPlacementBehaviorForData(todayItem.data); break; case FlattenedTodayItemType.EditorialItemGroup: const groupItems = asArrayOrEmpty(todayItem.data, "meta.associations.recommendations.data"); if (isDefinedNonNullNonEmpty(groupItems)) { for (const groupItem of groupItems) { todayItemAdPlacementBehavior = getAdPlacementBehaviorForData(groupItem); if (todayItemAdPlacementBehavior !== models.AdPlacementBehavior.insertIntoShelf) { break; } } } break; default: break; } if (todayItemAdPlacementBehavior !== models.AdPlacementBehavior.insertIntoShelf) { adPlacementBehavior = todayItemAdPlacementBehavior; break; } } return adPlacementBehavior; } function getAdPlacementBehaviorForData(data) { const replaceIfAdPresent = asBoolean(data, "meta.personalizationData.replaceIfAdPresent"); if (isDefinedNonNull(replaceIfAdPresent) && replaceIfAdPresent) { return models.AdPlacementBehavior.replaceOrganic; } else if (isDefinedNonNull(replaceIfAdPresent) && !replaceIfAdPresent) { return models.AdPlacementBehavior.dropAd; } else { return models.AdPlacementBehavior.insertIntoShelf; } } /** * Return the first shelf title for the today page. This is used to generate the tab title. * @param objectGraph The dependency graph of the app store */ function todayPageTitleDetail(objectGraph) { const now = new Date(); const dateFormat = objectGraph.loc.string("Today.ShelfTitle.DateFormat"); return objectGraph.loc.formatDate(dateFormat, now); } /** * Return the first shelf title for the today page on iPad. This is used to generate the tab title. * @param objectGraph The dependency graph of the app store */ function todayPageLongTitle(objectGraph) { if (!objectGraph.client.isPad) { return undefined; } const now = new Date(); const dateFormat = objectGraph.loc.string("Today.ShelfTitle.DateFormat.IPad"); return objectGraph.loc.formatDate(dateFormat, now); } /** * Return a short version of the first shelf title for the today page. This is used to generate the tab title, * and is used where less horizontal space is available. * @param objectGraph The dependency graph of the app store */ export function todayPageShortTitleDetail(objectGraph) { const now = new Date(); return objectGraph.loc.formatDate("MMM d", now); } /** * Generate the last shelves of the today page * @param objectGraph The dependency graph for the app * @param pageInformation The page information for the today page * @param locationTracker The Location tracker for the current today page * @returns The shelves that appear tha the bottom of the today page */ function footerShelvesForTodayPage(objectGraph, pageInformation, locationTracker) { const shelves = []; // Add Footer Buttons const footerButtonShelf = shelfForFooterButtons(objectGraph, pageInformation, locationTracker); if (isDefinedNonNull(footerButtonShelf)) { shelves.push(footerButtonShelf); } // Add T&C const url = objectGraph.bag.termsAndConditionsURL; if (isDefinedNonNull(url)) { const termsAndConditionsShelf = shelfForTermsAndConditions(objectGraph, url); shelves.push(termsAndConditionsShelf); } return shelves; } //# sourceMappingURL=today-controller-util.js.map