import { isSome } from "@jet/environment/types/optional"; import * as models from "../../api/models"; import * as serverData from "../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../foundation/media/attributes"; import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; import * as color from "../../foundation/util/color-util"; import * as contentDeviceFamily from "../content/device-family"; import * as client from "../../foundation/wrappers/client"; import * as contentArtwork from "../content/artwork/artwork"; import * as contentAttributes from "../content/attributes"; import * as content from "../content/content"; import * as filtering from "../filtering"; import * as lockups from "../lockups/lockups"; import * as metricsHelpersClicks from "../metrics/helpers/clicks"; import { externalPurchasesLearnMoreAction, externalPurchasesPlacementIsEnabled, hasExternalPurchasesForData, } from "../offers/external-purchases"; import * as productPageUtil from "../product-page/product-page-util"; import { createExpandActionForAnnotation } from "./shelves/annotations/annotations"; import * as compatibilityAnnotation from "./shelves/annotations/compatibility-annotation"; import { appPlatformTitle } from "../../gameservicesui/src/common/media-preview-platform"; import { hasRemoteDownloadIdentifiers, supportsVisionPlatformForVisionCompanion } from "../offers/offers"; /** * Create a product page banner. * * @param data The raw data response for a product page JSON fetch. * @param bannerContext A collection of any other variables used when creating this banner. * @returns A banner. */ export function create(objectGraph, data, bannerContext) { let message = null; let focusedMessage = null; let action = null; let fullProductAction = null; let leadingArtwork = null; let leadingArtworkTintColor = null; let includeBackgroundBorder = false; const bannerKind = null; // 32-bit only app (disables buy button) // `requires32bit` is used only for macOS apps, which is why we have two similar flags here const is32bitOnly = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "is32bitOnly"); const requires32bit = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requires32bit"); if (is32bitOnly || requires32bit) { if (objectGraph.client.isMac) { message = objectGraph.loc.string("ProductPage.Banner.UpdateRequired.macOS"); } else if (objectGraph.client.isVision) { message = objectGraph.loc.string("ProductPage.Banner.UpdateRequired.Vision"); } else { message = objectGraph.loc.string("ProductPage.Banner.UpdateRequired.iOS"); } bannerContext.offerButtonShouldBeDisabled = true; } // Is SAD watch-only app if (message === null && filtering.shouldFilter(objectGraph, data, 1024 /* filtering.Filter.SADWatchApps */)) { message = objectGraph.loc.string("OFFER_WATCH_ONLY_BANNER"); bannerContext.offerButtonShouldBeDisabled = true; } // Required capabilities mismatch if (message === null && !lockups.deviceHasCapabilitiesFromData(objectGraph, data)) { message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER"); if (preprocessor.GAMES_TARGET) { message = objectGraph.loc.string("GameDetails.Page.Banner.NotCompatible.Message"); } else if (objectGraph.client.isTV) { // We don't support product page scrolling for this particular banner on TV message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_NO_LINK"); } else if (objectGraph.client.isVision) { // For visionOS, we show a sheet rather than scrolling const annotation = compatibilityAnnotation.createAnnotation(objectGraph, data, false, false, false, null); if (isSome(annotation)) { action = createExpandActionForAnnotation(objectGraph, annotation); if (isSome(action)) { action.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); message = objectGraph.loc .string("NOT_COMPATIBLE_BANNER_TEMPLATE") .replace("{linkTitle}", action.title); } } bannerContext.offerButtonShouldBeDisabled = true; } else if (objectGraph.client.isiOS || objectGraph.client.isMac) { // For iOS / macOS, add an action that allows users to scroll to the Compatibility item if (productPageUtil.isShelfBased(objectGraph)) { const scrollAction = new models.ShelfBasedPageScrollAction("information"); scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); scrollAction.indexId = "compatibilityAnnotation"; // Here we use the `fullProductAction` action rather than just `action`, // as our `ProductPageScrollAction` will only be valid once the full product page has loaded fullProductAction = scrollAction; } else { const informationSection = new models.ProductPageSection("shelf", "information"); const scrollAction = new models.ProductPageScrollAction(informationSection); scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); scrollAction.indexId = "compatibilityAnnotation"; // Here we use the `fullProductAction` action rather than just `action`, // as our `ProductPageScrollAction` will only be valid once the full product page has loaded fullProductAction = scrollAction; } message = objectGraph.loc .string("NOT_COMPATIBLE_BANNER_TEMPLATE") .replace("{linkTitle}", fullProductAction.title); bannerContext.offerButtonShouldBeDisabled = true; } } // Filtered system & legacy apps if (message === null && filtering.shouldFilter(objectGraph, data, 4 /* filtering.Filter.UnsupportedSystemDeletableApps */ | 32 /* filtering.Filter.LegacyApps */)) { message = objectGraph.loc.string("UNSUPPORTED_CAPABILITIES"); bannerContext.offerButtonShouldBeDisabled = true; } // App is supported on this OS version if (filtering.shouldFilter(objectGraph, data, 512 /* filtering.Filter.MinimumOSRequirement */)) { const minOSVersion = content.minimumOSVersionFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); switch (objectGraph.client.deviceType) { case "mac": message = objectGraph.loc.string("UNSUPPORTED_MACOS_VERSION").replace("{osVersion}", minOSVersion); break; case "phone": case "pad": message = objectGraph.loc.string("UNSUPPORTED_IOS_VERSION").replace("{osVersion}", minOSVersion); break; case "tv": message = objectGraph.loc.string("UNSUPPORTED_TVOS_VERSION").replace("{osVersion}", minOSVersion); break; case "watch": const minWatchOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); message = objectGraph.loc .string("UNSUPPORTED_WATCHOS_VERSION") .replace("{osVersion}", minWatchOSVersion); break; case "vision": message = objectGraph.loc.string("UNSUPPORTED_VISIONOS_VERSION").replace("{osVersion}", minOSVersion); break; default: message = objectGraph.loc.string("UNSUPPORTED_CAPABILITIES"); break; } // If the device is a phone but the app is watch-only, use the unsupported watch message. if (objectGraph.client.isPhone && contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")) { const minWatchOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); message = objectGraph.loc.string("UNSUPPORTED_WATCHOS_VERSION").replace("{osVersion}", minWatchOSVersion); } } // App is supported on companion's OS version if (objectGraph.client.isWatch && filtering.shouldFilter(objectGraph, data, 2048 /* filtering.Filter.MinimumCompanionOSRequirement */)) { const minOSVersion = mediaPlatformAttributes.platformAttributeAsString(data, contentAttributes.bestAttributePlatformFromData(objectGraph, data), "minimumOSVersion"); message = objectGraph.loc.string("UNSUPPORTED_IOS_VERSION").replace("{osVersion}", minOSVersion); } // If the current device is not supported if (filtering.shouldFilter(objectGraph, data, 128 /* filtering.Filter.UnsupportedPlatform */)) { bannerContext.offerButtonShouldBeDisabled = true; message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER"); if (preprocessor.GAMES_TARGET) { const supportedPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); if (supportedPlatforms.length === 1) { const supportedPlatform = supportedPlatforms[0]; const platformTitle = appPlatformTitle(objectGraph, supportedPlatform); message = objectGraph.localizer.string("GameDetails.Page.Banner.OnlyAvailable.Message", { platform: platformTitle, }); const systemImageName = content.systemImageNameForAppPlatform(supportedPlatform); leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, `systemimage://${systemImageName}`); } else { message = objectGraph.loc.string("GameDetails.Page.Banner.NotCompatible.Message"); } } else if (objectGraph.client.isTV) { // We don't support product page scrolling for this particular banner on TV message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_NO_LINK"); } else if (objectGraph.client.isVision) { // For visionOS, we show a sheet rather than scrolling const annotation = compatibilityAnnotation.createAnnotation(objectGraph, data, false, false, false, null); if (isSome(annotation)) { action = createExpandActionForAnnotation(objectGraph, annotation); if (isSome(action)) { action.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); message = objectGraph.loc .string("NOT_COMPATIBLE_BANNER_TEMPLATE") .replace("{linkTitle}", action.title); } } bannerContext.offerButtonShouldBeDisabled = true; } else if (objectGraph.client.isiOS || objectGraph.client.isMac) { if (productPageUtil.isShelfBased(objectGraph)) { const scrollAction = new models.ShelfBasedPageScrollAction("information"); scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); scrollAction.indexId = "compatibilityAnnotation"; // Here we use the `fullProductAction` action rather than just `action`, // as our `ProductPageScrollAction` will only be valid once the full product page has loaded fullProductAction = scrollAction; message = objectGraph.loc .string("NOT_COMPATIBLE_BANNER_TEMPLATE") .replace("{linkTitle}", scrollAction.title); } else { // For iOS / macOS, add an action that allows users to scroll to the Compatibility item const informationSection = new models.ProductPageSection("shelf", "information"); const scrollAction = new models.ProductPageScrollAction(informationSection); scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); scrollAction.indexId = "compatibilityAnnotation"; // Here we use the `fullProductAction` action rather than just `action`, // as our `ProductPageScrollAction` will only be valid once the full product page has loaded fullProductAction = scrollAction; message = objectGraph.loc .string("NOT_COMPATIBLE_BANNER_TEMPLATE") .replace("{linkTitle}", scrollAction.title); } } } // Perform Rosetta filtering for macOS. if (filtering.shouldFilter(objectGraph, data, 8192 /* filtering.Filter.MacOSRosetta */)) { if (productPageUtil.isShelfBased(objectGraph)) { const scrollAction = new models.ShelfBasedPageScrollAction("information"); scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); scrollAction.indexId = "compatibilityAnnotation"; // Here we use the `fullProductAction` action rather than just `action`, // as our `ProductPageScrollAction` will only be valid once the full product page has loaded fullProductAction = scrollAction; } else { const informationSection = new models.ProductPageSection("shelf", "information"); const scrollAction = new models.ProductPageScrollAction(informationSection); scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); scrollAction.indexId = "compatibilityAnnotation"; // Here we use the `fullProductAction` action rather than just `action`, // as our `ProductPageScrollAction` will only be valid once the full product page has loaded fullProductAction = scrollAction; } message = objectGraph.loc .string("NOT_COMPATIBLE_BANNER_TEMPLATE") .replace("{linkTitle}", fullProductAction.title); } // App is not supported by the current companion, eg. dynamic duo app on a Tinker watch const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); if (content.isUnsupportedByCurrentCompanion(objectGraph, data)) { if (isPreorder) { if (objectGraph.host.isWatch) { message = objectGraph.loc.string("UNSUPPORTED_COMPANION_CONFIGURATION_PREORDER"); } else { message = objectGraph.loc.string("ProductPage.WatchOS.PreOrderRequiresiPhone"); } } else { message = objectGraph.loc.string("UNSUPPORTED_COMPANION_CONFIGURATION", "Requires iPhone"); } bannerContext.offerButtonShouldBeDisabled = true; } // App is not supported on paired watch OS version const minimumWatchOSVersionString = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); if (message === null && bannerContext.clientIdentifierOverride === client.watchIdentifier && content.isActivePairedWatchOSBelowVersion(objectGraph, minimumWatchOSVersionString)) { message = objectGraph.loc .string("ProductPage.Banner.PairedWatchOSVersionBelowMinimum") .replace("{osVersion}", minimumWatchOSVersionString); } /// App requires a visionOS device const isVisionOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice"); if (isVisionOnlyApp && !objectGraph.client.isVision && !objectGraph.client.isWeb && !preprocessor.GAMES_TARGET && !objectGraph.client.isCompanionVisionApp) { const learnMoreTitle = objectGraph.loc.string("BANNER_VISION_ONLY_APP_LEARN_MORE_LINK"); action = visionOnlyAppLearnMoreAction(objectGraph, learnMoreTitle, bannerContext.metricsPageInformation, bannerContext.metricsLocationTracker); message = visionOnlyAppMessage(objectGraph, learnMoreTitle, action); fullProductAction = null; leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://visionpro"); leadingArtworkTintColor = color.named("secondaryText"); includeBackgroundBorder = true; } if (supportsVisionPlatformForVisionCompanion(objectGraph, data) && !hasRemoteDownloadIdentifiers(objectGraph)) { // This case is where we support the download to a visionOS device, but we don't have anything to download to. bannerContext.offerButtonShouldBeDisabled = true; message = objectGraph.loc.string("ProductPage.Banner.Companion.VisionDeviceRequired"); } else if (objectGraph.client.isCompanionVisionApp && !supportsVisionPlatformForVisionCompanion(objectGraph, data)) { // This case is the app doesn't support remote downloads, show this banner. bannerContext.offerButtonShouldBeDisabled = true; message = objectGraph.loc.string("ProductPage.Banner.Companion.RemoteDownloadUnavailable"); } // External purchases // If we have no banner, or the offer button is not disabled, give priority to the external purchases banner const hasExternalPurchases = hasExternalPurchasesForData(objectGraph, data); const externalPurchasesEnabled = externalPurchasesPlacementIsEnabled(objectGraph, "product-page-banner"); if ((message === null || !bannerContext.offerButtonShouldBeDisabled) && hasExternalPurchases && externalPurchasesEnabled) { const learnMoreTitle = objectGraph.loc.string("ProductPage.ExternalPurchasesBanner.LearnMore"); action = externalPurchasesLearnMoreAction(objectGraph, learnMoreTitle, bannerContext.metricsPageInformation, bannerContext.metricsLocationTracker); message = externalPurchasesMessage(objectGraph, learnMoreTitle, action); fullProductAction = null; if (objectGraph.bag.externalPurchasesIncludeProductPageBannerIcon) { if (objectGraph.bag.externalPurchasesProductPageBannerIconVariant === "secondaryInfoCircle") { leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://info.circle"); leadingArtworkTintColor = color.named("secondaryText"); } else { leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.triangle"); if (objectGraph.client.isVision) { leadingArtworkTintColor = color.named("secondaryText"); } else { leadingArtworkTintColor = color.named("systemRed"); } } } else { leadingArtwork = null; leadingArtworkTintColor = null; } if (objectGraph.client.isTV) { if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { const key = `ProductPage.ExternalPurchasesBanner.Focused.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; focusedMessage = objectGraph.loc.string(key); } else { focusedMessage = objectGraph.loc.string("ProductPage.ExternalPurchasesBanner.Focused"); } } includeBackgroundBorder = true; } if ((message === null || message === void 0 ? void 0 : message.length) > 0) { return new models.Banner(message, focusedMessage, action, fullProductAction, leadingArtwork, leadingArtworkTintColor, includeBackgroundBorder, bannerKind); } else if (!bannerContext.offerButtonShouldBeDisabled) { // If no banner or disabled button for above reasons, show an AppState based Banner if needed const appCapabilities = content.appBinaryTraitsFromData(objectGraph, data); if (serverData.isDefinedNonNullNonEmpty(appCapabilities)) { return createExternalBrowserCompatibilityBanner(objectGraph, appCapabilities); } } return null; } function createExternalBrowserBanner(appAction, appCapabilities, message) { return new models.Banner(message, undefined, undefined, undefined, undefined, undefined, undefined, undefined, { type: "canPerformAppAction", appAction: appAction, appCapabilities: appCapabilities, }); } function createExternalBrowserCompatibilityBanner(objectGraph, appCapabilities) { const message = objectGraph.loc.string("ProductPage.Banner.ExternalBrowser.Message"); let unknownMessage = message; let buyMessage = message; let downloadMessage = message; let updateMessage = message; let openMessage = message; // Debug builds append current app state to banner text for testing. if (["debug"].includes(objectGraph.client.buildType)) { unknownMessage += "\n(Internal: unknown)"; buyMessage += "\n(Internal: buyOrGet)"; downloadMessage += "\n(Internal: redownload)"; updateMessage += "\n(Internal: update)"; openMessage += "\n(Internal: open)"; } return new models.AppStateBanner( /* unknownBanner */ createExternalBrowserBanner("install", appCapabilities, unknownMessage), /* buyBanner */ createExternalBrowserBanner("install", appCapabilities, buyMessage), /* downloadBanner */ createExternalBrowserBanner("install", appCapabilities, downloadMessage), /* updateBanner */ createExternalBrowserBanner("update", appCapabilities, updateMessage), /* openBanner */ createExternalBrowserBanner("launch", appCapabilities, openMessage)); } /** * Creates the banner message to display for external purchases. * * @param objectGraph Current object graph * @param learnMoreTitle The title for a "Learn More" link * @param learnMoreAction The action associated with the "Learn More" link, if any * @returns The built message, which may or ma */ function externalPurchasesMessage(objectGraph, learnMoreTitle, learnMoreAction) { if (learnMoreTitle && learnMoreAction && !objectGraph.client.isTV && !objectGraph.client.isWatch && !objectGraph.client.isWeb) { if (objectGraph.client.isMac || objectGraph.client.isVision) { if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { const key = `ProductPage.ExternalPurchasesBanner.WithLink_WithoutNewline.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; return objectGraph.loc.string(key).replace("{learnMoreLink}", learnMoreTitle); } else { return objectGraph.loc .string("ProductPage.ExternalPurchasesBanner.WithLink_WithoutNewline") .replace("{learnMoreLink}", learnMoreTitle); } } else { if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { const key = `ProductPage.ExternalPurchasesBanner.WithLink.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; return objectGraph.loc.string(key).replace("{learnMoreLink}", learnMoreTitle); } else { return objectGraph.loc .string("ProductPage.ExternalPurchasesBanner.WithLink") .replace("{learnMoreLink}", learnMoreTitle); } } } else { if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { const key = `ProductPage.ExternalPurchasesBanner.NoLink.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; return objectGraph.loc.string(key); } else { return objectGraph.loc.string("ProductPage.ExternalPurchasesBanner.NoLink"); } } } /** * The message to use when viewing a visionOS only app on other platforms. * @param objectGraph Current object graph * @param learnMoreTitle Title for the learn more action * @param learnMoreAction An optional learn more action * @returns A message string */ function visionOnlyAppMessage(objectGraph, learnMoreTitle, learnMoreAction) { if (learnMoreTitle && learnMoreAction) { return objectGraph.loc.string("BANNER_VISION_ONLY_APP_WITH_LINK").replace("{learnMoreLink}", learnMoreTitle); } else { return objectGraph.loc.string("BANNER_VISION_ONLY_APP_NO_LINK"); } } /** * Creates a learn more action, for when viewing a visionOS only app on other platforms. * @param objectGraph Current object graph * @param title The title to assign to the action * @param metricsPageInformation Metrics page info * @param metricsLocationTracker Metrics location tracker * @returns A built action */ export function visionOnlyAppLearnMoreAction(objectGraph, title, metricsPageInformation, metricsLocationTracker) { const editorialItemId = objectGraph.bag.visionOnlyAppLearnMoreEditorialItemId; if (serverData.isNullOrEmpty(editorialItemId)) { return null; } const action = new models.FlowAction("article"); action.title = title; action.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; metricsHelpersClicks.addClickEventToAction(objectGraph, action, { id: "LearnMore", targetType: "link", actionType: "navigate", pageInformation: metricsPageInformation, locationTracker: metricsLocationTracker, }); return action; } //# sourceMappingURL=banner.js.map