From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../app-store/tmp/src/common/offers/offers.js | 1735 ++++++++++++++++++++ 1 file changed, 1735 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/offers/offers.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/offers/offers.js') diff --git a/node_modules/@jet-app/app-store/tmp/src/common/offers/offers.js b/node_modules/@jet-app/app-store/tmp/src/common/offers/offers.js new file mode 100644 index 0000000..2cb2986 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/offers/offers.js @@ -0,0 +1,1735 @@ +// +// offers.ts +// AppStoreKit +// +// Created by Kevin MacWhinnie on 8/15/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as validation from "@jet/environment/json/validation"; +import { isNothing, 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 dateUtil from "../../foundation/util/date-util"; +import * as objects from "../../foundation/util/objects"; +import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache"; +import * as client from "../../foundation/wrappers/client"; +import * as ageRatings from "../content/age-ratings"; +import * as artworkBuilder from "../content/artwork/artwork"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as contentDeviceFamily from "../content/device-family"; +import * as gameController from "../content/game-controller"; +import * as sad from "../content/sad"; +import * as filtering from "../filtering"; +import * as links from "../linking/os-update-links"; +import * as lockups from "../lockups/lockups"; +import { adBuyParamKeys } from "../metrics/helpers/buy"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsUtil from "../metrics/helpers/util"; +import * as productVariant from "../product-page/product-page-variants"; +import { externalPurchasesPlacementIsEnabled, hasExternalPurchasesForData } from "./external-purchases"; +/** + * Create a purchase configuration (aka purchase token) from the provided product and other data. + * @param objectGraph The App Store object graph. + * @param product The data for the product. + * @param buyParams The buy params being passed through the offer. + * @param isPreorder Whether the purchase is a pre-order. + * @param excludeAttribution Whether attribution should be excluded for this purchase. + * @param pageInformation The information for the page the purchase will take place on. + * @param metricsPlatformDisplayStyle The platform display style for the purchase. + * @param options The MetricsClickOptions. + * @param referrerData Referrer data for the purchase. + * @param isDefaultBrowser Whether the install is a default browser install. + * @returns A complete PurchaseConfiguration. + */ +export function purchaseConfigurationFromProduct(objectGraph, product, buyParams, isPreorder, excludeAttribution, pageInformation, metricsPlatformDisplayStyle, options, referrerData, isDefaultBrowser) { + return validation.context("purchaseConfigurationFromProduct", () => { + const appTitle = mediaAttributes.attributeAsString(product, "name"); + let vendor = mediaAttributes.attributeAsString(product, "artistName"); + if (!vendor) { + vendor = "test"; + } + const bundleId = sad.systemApps(objectGraph).bundleIdFromData(product); + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, product); + const lineItem = mediaAttributes.attributeAsString(product, "iad.lineItem"); + const preflightPackageUrl = contentAttributes.contentAttributeAsString(objectGraph, product, "preflightPackageUrl"); + const supportsArcade = content.isArcadeSupported(objectGraph, product); + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, product, objectGraph.appleSilicon.isSupportEnabled); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, product); + const extRefApp2 = serverData.asString(referrerData, "app"); + const extRefUrl2 = serverData.asString(referrerData, "externalUrl"); + // Get the Serial Numbers from the devices we want to download from + const remoteDownloadIdentifiers = supportsVisionDownloadFromVisionCompanion(objectGraph, product) + ? objectGraph.client.remoteDownloadIdentifiers + : []; + const hasMacIPAPackage = hasMacIPAPackageForData(objectGraph, product); + const contentRating = ageRatings.value(objectGraph, product, true); + const purchaseConfiguration = new models.PurchaseConfiguration(buyParams, vendor, appTitle, bundleId, appPlatforms, isPreorder, excludeAttribution, metricsPlatformDisplayStyle, lineItem, false, preflightPackageUrl, supportsArcade, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, options.inAppEventId, extRefApp2, extRefUrl2, undefined, content.appBinaryTraitsFromData(objectGraph, product), isDefaultBrowser, remoteDownloadIdentifiers, hasMacIPAPackage, contentRating); + purchaseConfiguration.pageInformation = { ...pageInformation }; + purchaseConfiguration.productVariantData = productVariant.productVariantDataForData(objectGraph, product); + purchaseConfiguration.targetType = options.targetType; + purchaseConfiguration.metricsKind = options.kind; + return purchaseConfiguration; + }); +} +/** + * Extract the offer data from the provided data object. + * @param objectGraph The App Store object graph. + * @param data The data blob from which to extract the offer data. + * @param attributePlatformOverride An override platform, from which to fetch the offer data. + * @returns A `JSONData` with offer data. + */ +export function offerDataFromData(objectGraph, data, attributePlatformOverride = undefined) { + return validation.context("offerDataFromData", () => { + const offers = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "offers", attributePlatformOverride); + if (offers.length === 0) { + return null; + } + return offers[0]; + }); +} +export function offerDataFromMarketingItem(objectGraph, marketingItemData) { + const offersArray = mediaAttributes.attributeAsArrayOrEmpty(marketingItemData, "offers"); + if (offersArray.length === 0) { + return null; + } + return offersArray[0]; +} +export function updateOfferDataFromData(objectGraph, data) { + return validation.context("updateOfferDataFromData", () => { + const offers = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "offers"); + if (offers.length === 0) { + return null; + } + for (const offerDict of offers) { + const type = serverData.asString(offerDict, "type"); + if (type === "update") { + return offerDict; + } + } + return null; + }); +} +/** + * Create an offer action using the provided offer data. + * @param objectGraph The App Store object graph. + * @param offerData The offer data from the original data. + * @param data The data for the product. + * @param isPreorder Whether the purchase is a pre-order. + * @param includeBetaApps Whether beta apps should be included. + * @param metricsPlatformDisplayStyle The platform display style for the purchase. + * @param options The MetricsClickOptions. + * @param context The context in which the offer will be placed. + * @param referrerData Referrer data for the purchase. + * @param isDefaultBrowser Whether the install is a default browser install. + * @returns A complete OfferAction. + */ +export function offerActionFromOfferData(objectGraph, offerData, data, isPreorder, includeBetaApps, metricsPlatformDisplayStyle, options, context = "default", referrerData, isDefaultBrowser, parentAdamId) { + return validation.context(`offerActionFromOfferData: ${data.id}`, () => { + var _a, _b, _c, _d, _e, _f, _g; + /* Buy Params */ + let buyParams = serverData.asString(offerData, "buyParams"); + if (serverData.isNull(buyParams)) { + validation.unexpectedNull("ignoredValue", "string", "item.offer.buyParams"); + return null; + } + // This was added as a workaround for: + // Bundles: Suppress CMB dialog + // + // TODO: We need to suppress this dialog until CmB is completed: + // Bundles: Implement complete my bundle + // + // TODO: Unsuppress when commerce has new intrim alerts ready: + // Bundles: Remove Supressing CMB dialog + if (data.type === "app-bundles") { + if (buyParams.indexOf("rebuy") >= 0) { + buyParams = buyParams.replace("rebuy=false", "rebuy=true"); + } + else { + if (buyParams.length > 0) { + buyParams += "&"; + } + buyParams += "rebuy=true"; + } + } + if (serverData.isDefinedNonNullNonEmpty(options.inAppEventId)) { + if (buyParams.length > 0) { + buyParams += "&"; + } + buyParams += `mtInAppEventId=${options.inAppEventId}`; + } + if (options.isAdvert) { + const iAdPlacementId = (_b = (_a = options.pageInformation) === null || _a === void 0 ? void 0 : _a.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementId; + if (serverData.isDefinedNonNull(iAdPlacementId)) { + if (buyParams.length > 0) { + buyParams += "&"; + } + buyParams += `${adBuyParamKeys.placementId}=${iAdPlacementId}`; + } + const iAdContainerId = (_d = (_c = options.pageInformation) === null || _c === void 0 ? void 0 : _c.iAdInfo) === null || _d === void 0 ? void 0 : _d.containerId; + if (serverData.isDefinedNonNull(iAdContainerId)) { + if (buyParams.length > 0) { + buyParams += "&"; + } + buyParams += `${adBuyParamKeys.containerId}=${iAdContainerId}`; + } + const iAdTemplateType = (_f = (_e = options.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo) === null || _f === void 0 ? void 0 : _f.clickFields["iAdTemplateType"]; + if (serverData.isDefinedNonNull(iAdTemplateType)) { + if (buyParams.length > 0) { + buyParams += "&"; + } + buyParams += `${adBuyParamKeys.templateType}=${iAdTemplateType}`; + } + } + /* Adam Id */ + const adamId = data.id; + if (serverData.isNull(adamId)) { + validation.unexpectedNull("ignoredValue", "string", "item.offer.id"); + return null; + } + /* Purchase Configuration */ + const purchaseConfiguration = purchaseConfigurationFromProduct(objectGraph, data, buyParams, isPreorder, options.excludeAttribution, options.pageInformation, metricsPlatformDisplayStyle, options, referrerData, isDefaultBrowser); + const action = internalOfferActionFromOfferData(objectGraph, offerData, adamId, purchaseConfiguration, includeBetaApps, context, (_g = options.isAdvert) !== null && _g !== void 0 ? _g : false, parentAdamId); + metricsHelpersClicks.addBuyEventToOfferActionOnPage(objectGraph, action, options, isPreorder, isDefaultBrowser); + return action; + }); +} +/** + * Determines whether a given offer action is a free. + * @param offerAction The offer action to check. + * @returns true if it's a free + */ +export function isFreeFromOfferAction(objectGraph, offerAction) { + return serverData.isNull(offerAction) || serverData.isNull(offerAction.price) || offerAction.price === 0; +} +export function expectedReleaseDateFromData(objectGraph, data) { + return validation.context("expectedReleaseDateFromData", () => { + const expectedReleaseDateString = mediaAttributes.attributeAsString(data, "offers.0.expectedReleaseDate"); + return dateUtil.parseDateOmittingTimeFromString(expectedReleaseDateString); + }); +} +/** + * Determines the price for the offer data. + * @param {JSONData} offer The offer data from which to determine the price. + * @returns {number} The price for the offer. + */ +export function priceFromOfferData(objectGraph, offer) { + const type = serverData.asString(offer, "type"); + if (type === "buy" || type === "complete" || type === "preorder") { + return serverData.asNumber(offer, "price"); + } + return null; +} +/** + * Common builder for `OfferAction` model object. Metrics events should be added by calling function. + * @param offer JSONData of offer from platform data. + * @param adamId AdamId of offered product. + * @param purchaseConfiguration Configuration to use in offer. + * @param includeBetaApps Whether to include beta apps in the offer button configuration. + * @param context The context within which this offer button is visible. + * @param isAd Whether offer button is for an ad. + * @returns An `OfferAction` with given parameters. + */ +function internalOfferActionFromOfferData(objectGraph, offer, adamId, purchaseConfiguration, includeBetaApps, context = "default", isAd = false, parentAdamId) { + return validation.context("offerActionFromOfferData", () => { + // Localize action title + const type = serverData.asString(offer, "type"); + const useAdsLocale = isAd && context === "default" && isSome(objectGraph.bag.adsOverrideLanguage); + const adsOverrideLocalizer = useAdsLocale ? objectGraph.adsLoc : objectGraph.loc; + let actionTitle; + switch (type) { + case "get": + if (context === "flowPreview") { + actionTitle = objectGraph.loc.string("OfferButton.FlowPreview.Get", "Get"); + } + else { + const watchOSActionTitleKey = "OfferButton.Title.Get.TitleCase"; + const actionTitleKey = objectGraph.client.isWatch ? watchOSActionTitleKey : "OfferButton.Title.Get"; + actionTitle = adsOverrideLocalizer.string(actionTitleKey); + } + break; + case "preorder": + if (context === "flowPreview") { + actionTitle = objectGraph.loc.string("OfferButton.FlowPreview.Preorder", "Pre-Order"); + } + else { + actionTitle = adsOverrideLocalizer.string("OfferButton.Title.Get"); + } + break; + default: + actionTitle = type; + } + let actionPrice = null; + let actionPriceFormatted = null; + const price = priceFromOfferData(objectGraph, offer); + if (price > 0) { + actionPrice = price; + actionPriceFormatted = serverData.asString(offer, "priceFormatted"); + } + const expectedReleaseDateString = serverData.asString(offer, "expectedReleaseDate"); + const expectedReleaseDate = dateUtil.parseDateOmittingTimeFromString(expectedReleaseDateString); + const action = new models.OfferAction(actionTitle, adamId, purchaseConfiguration, parentAdamId); + action.price = actionPrice; + action.priceFormatted = actionPriceFormatted; + action.expectedReleaseDate = expectedReleaseDate; + action.includeBetaApps = includeBetaApps; + return action; + }); +} +function wrapOfferActionForPreorder(objectGraph, action, data, options, context) { + if (serverData.isNull(action)) { + return null; + } + const preorderStateAction = cancellablePreorderOfferStateAction(objectGraph, data, action, false, options); + preorderStateAction.buyAction = action; + return preorderStateAction; +} +export function wrapOfferActionIfNeeded(objectGraph, action, data, isPreorder, metricsOptions, context = "default", clientIdentifierOverride = null, shouldNavigateToProductPage = false) { + if (isNothing(action)) { + return null; + } + // Emit update links for macOS installers + if (content.isMacOSInstaller(objectGraph, data)) { + if (context === "flowPreview") { + return null; + } + // When `default`, i.e. not product page, show "VIEW". + if (context === "default") { + return lockups.actionFromData(objectGraph, data, metricsOptions, null); + } + // Otherwise punt to Software Update + const matchingOSBundle = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + if (serverData.isDefinedNonNullNonEmpty(matchingOSBundle)) { + const installUpdateUrl = links.osUpdateUrl("mac", matchingOSBundle); + if (isSome(installUpdateUrl)) { + const updateAction = new models.ExternalUrlAction(installUpdateUrl); + return new models.OfferStateAction(action.adamId, updateAction); + } + } + } + if (context === "default") { + if (data.type === "app-bundles" || + content.isUnsupportedByCurrentCompanion(objectGraph, data) || + shouldNavigateToProductPage) { + return lockups.actionFromData(objectGraph, data, metricsOptions, null); + } + } + // Wrap non-Arcade preorders to enable view to drill into product page (except for tvOS) + // Note: Arcade pre-orders are handled by `wrapArcadeAppOfferActionIfNeeded`. + if (isPreorder && objectGraph.client.deviceType !== "tv" && !content.isArcadeSupported(objectGraph, data)) { + const wrappedPreorderAction = wrapOfferActionForPreorder(objectGraph, action, data, metricsOptions, context); + if (wrappedPreorderAction !== null) { + return wrappedPreorderAction; + } + } + // Configure chain of dialogs for vision-only purchase. + // Note: Arcade purchases are handled by `wrapArcadeAppOfferActionIfNeeded`. + const isVisionOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice"); + const isCompanion = objectGraph.client.isCompanionVisionApp; + if (!content.isArcadeSupported(objectGraph, data) && + (isCompanion || (isVisionOnlyApp && objectGraph.client.deviceType !== "vision"))) { + const isFree = isFreeFromOfferAction(objectGraph, action); + const isArcade = content.isArcadeSupported(objectGraph, data); + return visionAppActionForBuyAction(objectGraph, action, data, isFree, isArcade); + } + // Configure chain of dialogs for tvOS-only purchase. + const isTvOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "tvos"); + if (isTvOnlyApp && objectGraph.client.deviceType !== "tv") { + const requiresGameController = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresGameController"); + return tvOnlyAppActionForBuyAction(objectGraph, action, requiresGameController); + } + // Configure dialogs for watchOS-only purchase. + const isWatchOnlyApp = !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isDeliveredInIOSAppForWatchOS") && + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"); + if (isWatchOnlyApp && objectGraph.client.deviceType !== "watch") { + return watchOnlyAppActionForBuyAction(objectGraph, action); + } + // App is not supported on paired watch OS version + const minimumWatchOSVersionString = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); + if (serverData.isDefinedNonNullNonEmpty(clientIdentifierOverride) && + clientIdentifierOverride === client.watchIdentifier && + isSome(minimumWatchOSVersionString) && + content.isActivePairedWatchOSBelowVersion(objectGraph, minimumWatchOSVersionString)) { + return watchUpdateRequiredActionForBuyAction(objectGraph, action, minimumWatchOSVersionString); + } + if (shouldWrapOffer(objectGraph, data)) { + // Handle all Arcade offers. + if (content.isArcadeSupported(objectGraph, data)) { + return wrapArcadeAppOfferActionIfNeeded(objectGraph, action, data, isPreorder, context, arcadeSubscribeContextFromOfferContext(objectGraph, context, isPreorder), metricsOptions); + } + // Wrap offers on tvOS in `OfferAlertAction` in order to check requirements and confirm. + const offerActionIndex = createOfferAlertActionIfNeeded(objectGraph, action, data, isPreorder, metricsOptions); + const offerAlertAction = offerActionIndex.startAction; + // Handle all NON-Arcade pre-orders. + if (isPreorder) { + const preorderStateAction = cancellablePreorderOfferStateAction(objectGraph, data, offerActionIndex.underlyingOfferAction, false, metricsOptions); + preorderStateAction.buyAction = offerAlertAction; + return preorderStateAction; + } + // Handle NON-Arcade NON-pre-orders. + return offerAlertAction; + } + else { + // Arcade offers do *not* get wrapped in a two-phase confirmation. + return wrapOfferInTwoPhasedConfirmationActionIfNeeded(objectGraph, action, isPreorder, metricsOptions); + } +} +function shouldWrapOffer(objectGraph, data) { + if (content.isArcadeSupported(objectGraph, data)) { + return true; + } + if (objectGraph.client.isTV || objectGraph.client.isVision) { + return true; + } + return false; +} +function arcadeSubscribeContextFromOfferContext(objectGraph, offerContext, isArcadePreorder) { + if (isArcadePreorder) { + return models.marketingItemContextFromString("arcadeComingSoon"); + } + switch (offerContext) { + case "productPage": + return models.marketingItemContextFromString("productPage"); + case "default": + case "flowPreview": + return models.marketingItemContextFromString("groupingLockup"); + default: + return models.marketingItemContextFromString("generic"); + } +} +export function appInstallActionFromAppData(objectGraph, data, offerContext, marketingItemContext, isPreorder, metricsOptions, clientIdentifierOverride = null) { + switch (marketingItemContext) { + case models.marketingItemContextFromString("productPage"): + case models.marketingItemContextFromString("groupingLockup"): + const primaryIcon = content.iconFromData(objectGraph, data, { + useCase: 3 /* content.ArtworkUseCase.LockupIconLarge */, + }); + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, metricsOptions); + const offerData = offerDataFromData(objectGraph, data); + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, primaryIcon, clientIdentifierOverride); + return offerActionFromOfferData(objectGraph, offerData, data, isPreorder, false, metricsPlatformDisplayStyle, metricsClickOptions, offerContext); + default: + return null; + } +} +/** + * Ensures that the provided action is properly wrapped if it is for an Arcade app. + * @param objectGraph The object graph. + * @param action The naked offer action. + * @param underlyingAction The buy action for tvOS. + * @param data The data for the product. + * @param isPreorder Indicates whether or not this offer action is for a preorder. + * @param offerContext Contextual information about this offer. + * @param marketingItemContext Contextual information about the marketing item. + * @param metricsOptions Contextual information about metrics. + */ +function wrapArcadeAppOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, offerContext, marketingItemContext, metricsOptions) { + if (!content.isArcadeSupported(objectGraph, data)) { + return offerAction; + } + switch (objectGraph.client.deviceType) { + case "tv": + return wrapArcadeAppOfferActionForTV(objectGraph, offerAction, data, isPreorder, marketingItemContext, metricsOptions); + case "vision": + return wrapArcadeAppOfferActionForVision(objectGraph, offerAction, data, isPreorder, marketingItemContext, metricsOptions); + default: + return wrapArcadeAppOfferActionForOtherPlatforms(objectGraph, offerAction, data, isPreorder, offerContext, marketingItemContext, metricsOptions); + } +} +/** + * Wrap the offer action for Arcade app in TV to check requirements, presenting upsell and confirm. + * + * If the app is preorder app: + * - It should show `OfferAlertAction` first to check requirements (eg: game controller) + * - After alert is shown, perform the preorder action to subscribe/unsubscribe coming soon order. + * - Then show the upsell with rate limit applied. + * - If user purchase subscription success, perform the buy action. + * + * Otherwise: + * - Show the `OfferAlertAction` first to check requirements (eg: game controller) + * - After alert is shown, show the upsell action. + * - If user purchase subscription success, perform the buy action. + * + * @param objectGraph The object graph. + * @param action The offer action that perform requirements check before buy. + * @param underlyingAction The buy action for tvOS. + * @param data The data for the product. + * @param isPreorder Indicates whether or not this offer action is for a preorder. + * @param marketingItemContext Contextual information about the marketing item. + * @param metricsOptions Contextual information about metrics. + */ +function wrapArcadeAppOfferActionForTV(objectGraph, offerAction, data, isPreorder, marketingItemContext, metricsOptions) { + var _a, _b, _c, _d; + // Wrap offers on tvOS in `OfferAlertAction` in order to check requirements and confirm. + const offerActionIndex = createTVOfferAlertActionIfNeeded(objectGraph, offerAction, null, data, isPreorder, metricsOptions); + const offerAlertAction = offerActionIndex.startAction; + const buyAction = offerActionIndex.underlyingOfferAction; + const isContextual = models.isContextualUpsellContext(marketingItemContext); + const upsellRequestInfo = new models.MarketingItemRequestInfo("arcade", marketingItemContext, objectGraph.bag.metricsTopic, data.id); + if (isSome((_b = (_a = metricsOptions.pageInformation) === null || _a === void 0 ? void 0 : _a.searchTermContext) === null || _b === void 0 ? void 0 : _b.term)) { + upsellRequestInfo.metricsOverlay["searchTerm"] = (_c = metricsOptions.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, + MetricsIdentifierType.canonical, + ]); + if (isSome(metricsIdentifierFields)) { + upsellRequestInfo.metricsOverlay = { + ...upsellRequestInfo.metricsOverlay, + ...metricsIdentifierFields, + }; + } + if (isContextual) { + upsellRequestInfo.purchaseSuccessAction = buyAction; + upsellRequestInfo.carrierLinkSuccessAction = buyAction; + } + const upsellAction = new models.FlowAction("upsellMarketingItem"); + upsellAction.pageData = upsellRequestInfo; + if (metricsOptions && metricsOptions.pageInformation) { + upsellAction.referrerUrl = metricsOptions.pageInformation.pageUrl; + } + // Always add buyParams to actionDetails of offer actions so that we have them when reporting events for subscription buys + if (offerAlertAction instanceof models.OfferAction) { + metricsOptions.actionDetails = { + buyParams: offerAlertAction.purchaseConfiguration.buyParams, + ...metricsOptions.actionDetails, + }; + } + metricsHelpersClicks.addClickEventToArcadeBuyInitiateAction(objectGraph, upsellAction, metricsOptions); + // Arcade Pre-order logic + if (isPreorder) { + const preorderStateAction = cancellablePreorderOfferStateAction(objectGraph, data, buyAction, true, metricsOptions); // For subscribers + preorderStateAction.buyAction = offerAlertAction; + const preorderOrCancelSubscribePageAction = cancellablePreorderOfferStateAction(objectGraph, data, buyAction, true, metricsOptions); // For non-subscribers + preorderOrCancelSubscribePageAction.buyAction = offerAlertAction; + preorderStateAction.subscribePageAction = preorderOrCancelSubscribePageAction; + buyAction.buyCompletedAction = arcadePreOrderBuyCompleteActionForTV(objectGraph, upsellAction); + return preorderStateAction; + } + else { + // If user is not subscribed to Arcade: show OfferAlertAction then display upsell + // Otherwise: we want to show OfferAlertAction without upsell + const stateAction = new models.OfferStateAction(data.id, offerAlertAction); + const wrappedUpsellAction = createTVOfferAlertActionIfNeeded(objectGraph, offerAction, upsellAction, data, isPreorder, metricsOptions).startAction; + stateAction.subscribePageAction = wrappedUpsellAction; + return stateAction; + } +} +/** + * Wrap the offer action for Arcade app in Vision to check requirements, presenting upsell and confirm. + * + * If the app is preorder app: + * - It should show `OfferAlertAction` first to check requirements (eg: game controller) + * - After alert is shown, perform the preorder action to subscribe/unsubscribe coming soon order. + * - Then show the upsell with rate limit applied. + * - If user purchase subscription success, perform the buy action. + * + * Otherwise: + * - Show the `OfferAlertAction` first to check requirements (eg: game controller) + * - After alert is shown, show the upsell action. + * - If user purchase subscription success, perform the buy action. + * + * @param objectGraph The object graph. + * @param action The offer action that perform requirements check before buy. + * @param underlyingAction The buy action for tvOS. + * @param data The data for the product. + * @param isPreorder Indicates whether or not this offer action is for a preorder. + * @param marketingItemContext Contextual information about the marketing item. + * @param metricsOptions Contextual information about metrics. + */ +function wrapArcadeAppOfferActionForVision(objectGraph, offerAction, data, isPreorder, marketingItemContext, metricsOptions) { + var _a, _b, _c, _d; + // Wrap offers on tvOS in `OfferAlertAction` in order to check requirements and confirm. + const offerActionIndex = createVisionOfferAlertActionIfNeeded(objectGraph, offerAction, null, data, isPreorder, metricsOptions); + const offerAlertAction = offerActionIndex.startAction; + const buyAction = offerActionIndex.underlyingOfferAction; + const isContextual = models.isContextualUpsellContext(marketingItemContext); + const upsellRequestInfo = new models.MarketingItemRequestInfo("arcade", marketingItemContext, objectGraph.bag.metricsTopic, data.id); + if (isSome((_b = (_a = metricsOptions.pageInformation) === null || _a === void 0 ? void 0 : _a.searchTermContext) === null || _b === void 0 ? void 0 : _b.term)) { + upsellRequestInfo.metricsOverlay["searchTerm"] = (_c = metricsOptions.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, + MetricsIdentifierType.canonical, + ]); + if (isSome(metricsIdentifierFields)) { + upsellRequestInfo.metricsOverlay = { + ...upsellRequestInfo.metricsOverlay, + ...metricsIdentifierFields, + }; + } + if (isContextual) { + upsellRequestInfo.purchaseSuccessAction = buyAction; + upsellRequestInfo.carrierLinkSuccessAction = buyAction; + } + const upsellAction = new models.FlowAction("upsellMarketingItem"); + upsellAction.pageData = upsellRequestInfo; + if (metricsOptions && metricsOptions.pageInformation) { + upsellAction.referrerUrl = metricsOptions.pageInformation.pageUrl; + } + // Always add buyParams to actionDetails of offer actions so that we have them when reporting events for subscription buys + if (offerAlertAction instanceof models.OfferAction) { + metricsOptions.actionDetails = { + buyParams: offerAlertAction.purchaseConfiguration.buyParams, + ...metricsOptions.actionDetails, + }; + } + metricsHelpersClicks.addClickEventToArcadeBuyInitiateAction(objectGraph, upsellAction, metricsOptions); + // Arcade Pre-order logic + if (isPreorder && buyAction !== null) { + const preorderStateAction = cancellablePreorderOfferStateAction(objectGraph, data, buyAction, true, metricsOptions); // For subscribers + preorderStateAction.buyAction = offerAlertAction; + const preorderOrCancelSubscribePageAction = cancellablePreorderOfferStateAction(objectGraph, data, buyAction, true, metricsOptions); // For non-subscribers + preorderOrCancelSubscribePageAction.buyAction = offerAlertAction; + preorderStateAction.subscribePageAction = preorderOrCancelSubscribePageAction; + buyAction.buyCompletedAction = arcadePreOrderBuyCompleteAction(objectGraph, upsellAction); + return preorderStateAction; + } + else { + // If user is not subscribed to Arcade: show OfferAlertAction then display upsell + // Otherwise: we want to show OfferAlertAction without upsell + const stateAction = new models.OfferStateAction(data.id, offerAlertAction); + const wrappedUpsellAction = createVisionOfferAlertActionIfNeeded(objectGraph, offerAction, upsellAction, data, isPreorder, metricsOptions).startAction; + stateAction.subscribePageAction = wrappedUpsellAction; + return stateAction; + } +} +/** + * Wrap the offer action for Arcade app in MacOS, iOS, WatchOS to present the upsell or subscribe/unsubscribe preorder + * + * If the app is preorder app: + * - Wraps the offer action with `cancellablePreorderOfferStateAction` for subscribe/unsubscribe coming soon order. + * - After the preorder offer action completed, it will call the Upsell FlowAction with rate limited. + * + * Otherwise: + * - Create a new OfferState action + * - Create an Upsell FlowAction, and add the upsell action as `subscribePageAction`. + * - Add the offer action as `purchaseSuccessAction` or `carrierLinkSuccessAction` to the Upsell FlowAction. + * + * @param objectGraph The object graph. + * @param action The offer action that perform requirements check before buy. + * @param underlyingAction The buy action for tvOS. + * @param data The data for the product. + * @param isPreorder Indicates whether or not this offer action is for a preorder. + * @param offerContext Contextual information about this offer. + * @param marketingItemContext Contextual information about the marketing item. + * @param metricsOptions Contextual information about metrics. + * @returns the final action for offer button. + */ +function wrapArcadeAppOfferActionForOtherPlatforms(objectGraph, offerAction, data, isPreorder, offerContext, marketingItemContext, metricsOptions) { + var _a, _b, _c, _d; + const isContextual = models.isContextualUpsellContext(marketingItemContext); + const upsellRequestInfo = new models.MarketingItemRequestInfo("arcade", marketingItemContext, objectGraph.bag.metricsTopic, data.id); + if (isSome((_b = (_a = metricsOptions.pageInformation) === null || _a === void 0 ? void 0 : _a.searchTermContext) === null || _b === void 0 ? void 0 : _b.term)) { + upsellRequestInfo.metricsOverlay["searchTerm"] = (_c = metricsOptions.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, + MetricsIdentifierType.canonical, + ]); + if (isSome(metricsIdentifierFields)) { + upsellRequestInfo.metricsOverlay = { + ...upsellRequestInfo.metricsOverlay, + ...metricsIdentifierFields, + }; + } + // If the host app is not set, and this is the isCompanionVisionApp and the supportsCompanionCheck is permitted, + // update the host app to ClientIdentifier.VisionCompanion. + if (objectGraph.props.enabled("supportsCompanionCheck") && + objectGraph.client.isCompanionVisionApp && + isNothing(upsellRequestInfo.metricsOverlay["hostApp"])) { + upsellRequestInfo.metricsOverlay["hostApp"] = "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */; + } + if (isContextual) { + upsellRequestInfo.purchaseSuccessAction = offerAction; + upsellRequestInfo.carrierLinkSuccessAction = offerAction; + } + const upsellAction = new models.FlowAction("upsellMarketingItem"); + upsellAction.pageData = upsellRequestInfo; + if (metricsOptions && metricsOptions.pageInformation) { + upsellAction.referrerUrl = metricsOptions.pageInformation.pageUrl; + } + // Always add buyParams to actionDetails of offer actions so that we have them when reporting events for subscription buys + metricsOptions.actionDetails = { + buyParams: offerAction.purchaseConfiguration.buyParams, + ...metricsOptions.actionDetails, + }; + metricsHelpersClicks.addClickEventToArcadeBuyInitiateAction(objectGraph, upsellAction, metricsOptions); + // Arcade Pre-order logic + if (isPreorder) { + // iOS and macOS + if (offerAction instanceof models.OfferAction) { + const preorderStateAction = cancellablePreorderOfferStateAction(objectGraph, data, offerAction, true, metricsOptions); // For subscribers + preorderStateAction.buyAction = offerAction; + const preorderOrCancelSubscribePageAction = cancellablePreorderOfferStateAction(objectGraph, data, offerAction, true, metricsOptions); // For non-subscribers + preorderOrCancelSubscribePageAction.buyAction = offerAction; + preorderStateAction.subscribePageAction = preorderOrCancelSubscribePageAction; + offerAction.buyCompletedAction = arcadePreOrderBuyCompleteAction(objectGraph, upsellAction); + return preorderStateAction; + } + } + // Vision only, Arcade purchase + const isVisionOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice"); + if (supportsVisionDownloadFromVisionCompanion(objectGraph, data) || + (isVisionOnlyApp && objectGraph.client.deviceType !== "vision")) { + // Alert action for subscribed user with offer action + const alertActionForSubscribed = visionAppAlertForBuyAction(objectGraph, offerAction, data, true, true); + // Alert action for unsubscribed user with upsell action + const alertActionForUnsubscribed = visionAppAlertForBuyAction(objectGraph, upsellAction, data, true, true); + // Offer state action + const offerStateAction = new models.OfferStateAction(data.id, alertActionForSubscribed); + offerStateAction.subscribePageAction = new models.OfferStateAction(data.id, alertActionForUnsubscribed); + return offerStateAction; + } + // No change + const stateAction = new models.OfferStateAction(data.id, offerAction); + stateAction.subscribePageAction = upsellAction; + return stateAction; +} +/** + * An offer state action for a preorder that is cancellable. + * @param objectGraph The object graph. + * @param data The app data. + * @param action An offer action that will either purchase the app, or present a cancellation dialog. + * @param isArcade Indicates if this app is an Arcade app. + * @param metricsClickOptions The metrics click options used for this interaction. + */ +function cancellablePreorderOfferStateAction(objectGraph, data, action, isArcade, metricsClickOptions) { + let defaultAction; + if (isArcade) { + const subscribedAction = wrappedCancellablePreorderAction(objectGraph, data, action.purchaseConfiguration.appName, isArcade, true, metricsClickOptions); + const notSubscribedAction = wrappedCancellablePreorderAction(objectGraph, data, action.purchaseConfiguration.appName, isArcade, false, metricsClickOptions); + defaultAction = new models.ArcadeSubscriptionStateAction(notSubscribedAction, notSubscribedAction, subscribedAction, notSubscribedAction); + } + else { + defaultAction = wrappedCancellablePreorderAction(objectGraph, data, action.purchaseConfiguration.appName, false, false, metricsClickOptions); + } + return new models.OfferStateAction(data.id, defaultAction); +} +/** + * A cancellable preorder action that is wrapped in an alert or sheet. + * @param objectGraph The object graph + * @param data The app data + * @param appName The name of the app that will be cancelled. + * @param isArcade Indicates if this app is an Arcade app. + * @param isSubscribedToArcade Indicates if this action is for an Arcade subscriber or not. + * @param metricsClickOptions The metrics click options used for this interaction. + */ +function wrappedCancellablePreorderAction(objectGraph, data, appName, isArcade, isSubscribedToArcade, metricsClickOptions) { + const cancelPreorderAction = new models.CancelPreorderAction(data.id, isArcade); + let title; + let cancelTitle; + let body; + // rdar://144833292 (Remove Cancel Preorder Native Loc) + // Once the service strings are localised and deployed, we should remove the native loc strings. + if (preprocessor.GAMES_TARGET) { + title = objectGraph.loc.string("PreOrder.Cancel.Title"); + if (objectGraph.client.isAutomaticDownloadingEnabled() && ((isArcade && isSubscribedToArcade) || !isArcade)) { + cancelPreorderAction.title = objectGraph.loc.string("PreOrder.Cancel.Button.Download"); + body = objectGraph.loc.string("PreOrder.Cancel.Body.Download").replace("{appName}", appName); + } + else { + cancelPreorderAction.title = objectGraph.loc.string("PreOrder.Cancel.Button"); + body = objectGraph.loc.string("PreOrder.Cancel.Body").replace("{appName}", appName); + } + cancelTitle = objectGraph.loc.string("PreOrder.Cancel.NotNow"); + } + else { + title = objectGraph.loc.string("CANCEL_COMING_SOON_TITLE"); + if (objectGraph.client.isAutomaticDownloadingEnabled() && ((isArcade && isSubscribedToArcade) || !isArcade)) { + cancelPreorderAction.title = objectGraph.loc.string("CANCEL_COMING_SOON_BUTTON_DOWNLOAD"); + body = objectGraph.loc.string("COMING_SOON_BODY_DOWNLOAD").replace("{appName}", appName); + } + else { + cancelPreorderAction.title = objectGraph.loc.string("CANCEL_COMING_SOON_BUTTON"); + body = objectGraph.loc.string("COMING_SOON_BODY").replace("{appName}", appName); + } + cancelTitle = objectGraph.loc.string("CANCEL_COMING_SOON_CANCEL"); + } + let wrappedCancellationAction; + if (objectGraph.client.deviceType === "mac" || objectGraph.client.deviceType === "tv") { + const alertAction = new models.AlertAction("default"); + alertAction.title = title; + alertAction.message = body; + alertAction.buttonActions = [cancelPreorderAction]; + alertAction.isCancelable = true; + alertAction.cancelTitle = cancelTitle; + alertAction.destructiveActionIndex = 0; + wrappedCancellationAction = alertAction; + } + else if (objectGraph.client.deviceType === "vision" || preprocessor.GAMES_TARGET) { + const visionAlertAction = new models.AlertAction("default"); + visionAlertAction.title = title; + visionAlertAction.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://bell.slash.fill", 95, 90); + visionAlertAction.message = body; + visionAlertAction.buttonActions = [cancelPreorderAction]; + visionAlertAction.isCancelable = true; + visionAlertAction.cancelTitle = cancelTitle; + wrappedCancellationAction = visionAlertAction; + } + else { + const sheetAction = new models.SheetAction([cancelPreorderAction]); + sheetAction.title = title; + sheetAction.message = body; + sheetAction.isCancelable = true; + sheetAction.cancelTitle = cancelTitle; + sheetAction.isCustom = false; + sheetAction.destructiveActionIndex = 0; + wrappedCancellationAction = sheetAction; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, cancelPreorderAction, { + ...metricsClickOptions, + actionType: "cancelPreorder", + }); + // Promo codes allow users to install preorder apps before they are available, so we need to + // allow the app to be opened without showing any alert if it is installed. + const openAppAction = new models.OpenAppAction(data.id, "app"); + const offerStateAction = new models.OfferStateAction(data.id, wrappedCancellationAction); + offerStateAction.openAction = openAppAction; + return offerStateAction; +} +function arcadePreOrderBuyCompleteActionForTV(objectGraph, subscribePageAction) { + if (!objectGraph.client.isTV) { + // Use `arcadePreOrderBuyCompleteAction` instead. + return null; + } + // For preorders before Coming Soon Enhancements: + // On TV, we show the pre-order legal terms as part of the confirmation. On all other supported + // platforms, the legal terms are visible on the product page itself, so we don't need to show + // them again. + const subscriberOfferAlertAction = new models.OfferAlertAction(); + if (objectGraph.client.isAutomaticDownloadingEnabled()) { + subscriberOfferAlertAction.title = objectGraph.loc.string("PREORDER_NOTIFY_AUTOMATIC_DOWNLOAD_MESSAGE"); + } + else { + subscriberOfferAlertAction.title = objectGraph.loc.string("PREORDER_NOTIFY_MESSAGE"); + } + const unsubscriberOfferAlertAction = new models.OfferAlertAction(); + unsubscriberOfferAlertAction.title = objectGraph.loc.string("PREORDER_NOTIFY_MESSAGE"); + // Configure both actions + for (const alertAction of [subscriberOfferAlertAction, unsubscriberOfferAlertAction]) { + alertAction.isCancelable = false; + alertAction.shouldCheckForAvailableDiskSpace = false; + alertAction.remoteControllerRequirement = null; + alertAction.shouldCheckForGameController = false; + alertAction.shouldPromptForConfirmation = true; + alertAction.shouldIncludeActiveAccountInFooterMessage = true; + alertAction.completionAction = new models.BlankAction(); + alertAction.completionAction.title = objectGraph.loc.string("Action.OK"); + } + // For those not subscribed, show an upsell (rate limited) and a toast. + const preorderSubscribePageAction = new models.RateLimitedAction("arcade-preorder", new models.CompoundAction([subscribePageAction, unsubscriberOfferAlertAction])); + preorderSubscribePageAction.rateLimit = objectGraph.bag.arcadePreOrderUpsellLimitSeconds; + preorderSubscribePageAction.fallbackAction = unsubscriberOfferAlertAction; + return new models.ArcadeSubscriptionStateAction(preorderSubscribePageAction, subscriberOfferAlertAction, subscriberOfferAlertAction, subscriberOfferAlertAction); +} +function arcadePreOrderBuyCompleteAction(objectGraph, subscribePageAction) { + if (objectGraph.client.isTV) { + // Use `arcadePreOrderBuyCompleteActionForTV` instead. + return null; + } + const checkmarkArtwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://checkmark", 95, 90); + const bellArtwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://bell.fill", 95, 90); + // On completion, we will show a toast (and maybe an upsell if you're unsubscribed from Arcade). + const isVisionOS = objectGraph.client.isVision; + let subscribedAction; + if (isVisionOS) { + subscribedAction = new models.AlertAction("default"); + subscribedAction.title = objectGraph.loc.string("ARCADE_PREORDER_LOCKUP_COMING_SOON"); + subscribedAction.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://bell.badge.fill", 95, 90); + subscribedAction.isCancelable = true; + if (objectGraph.client.isAutomaticDownloadingEnabled()) { + subscribedAction.message = objectGraph.loc.string("PREORDER_NOTIFY_AUTOMATIC_DOWNLOAD_MESSAGE"); + } + else { + subscribedAction.message = objectGraph.loc.string("PREORDER_NOTIFY_MESSAGE"); + } + } + else { + subscribedAction = new models.AlertAction("toast"); + subscribedAction.title = ""; + subscribedAction.artwork = checkmarkArtwork; + // rdar://144833292 (Remove Cancel Preorder Native Loc) + // Once the service strings are localised and deployed, we should remove the native loc strings. + if (preprocessor.GAMES_TARGET) { + if (objectGraph.client.isAutomaticDownloadingEnabled()) { + subscribedAction.message = objectGraph.loc.string("PreOrder.Notify.Message.Download"); + subscribedAction.toastDuration = 2.5; + } + else { + subscribedAction.message = objectGraph.loc.string("PreOrder.Notify.Message"); + subscribedAction.toastDuration = 1.5; + } + } + else { + if (objectGraph.client.isAutomaticDownloadingEnabled()) { + subscribedAction.message = objectGraph.loc.string("PREORDER_NOTIFY_AUTOMATIC_DOWNLOAD_MESSAGE"); + subscribedAction.toastDuration = 2.5; + } + else { + subscribedAction.message = objectGraph.loc.string("PREORDER_NOTIFY_MESSAGE"); + subscribedAction.toastDuration = 1.5; + } + } + } + // Users who are not subscribed should never see the 'auto download' message. + let unsubscribedAction; + if (isVisionOS) { + const preorderNotifyAlert = new models.AlertAction("default"); + preorderNotifyAlert.title = objectGraph.loc.string("ARCADE_PREORDER_LOCKUP_COMING_SOON"); + preorderNotifyAlert.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://bell.badge.fill", 95, 90); + preorderNotifyAlert.message = objectGraph.loc.string("PREORDER_NOTIFY_MESSAGE"); + preorderNotifyAlert.isCancelable = true; + const subscribePageRateLimitedAction = new models.RateLimitedAction("arcade-preorder", new models.CompoundAction([subscribePageAction])); + subscribePageRateLimitedAction.title = objectGraph.loc.string("ACTION_OK"); + subscribePageRateLimitedAction.rateLimit = objectGraph.bag.arcadePreOrderUpsellLimitSeconds; + subscribePageRateLimitedAction.fallbackAction = null; + // For those not subscribed, show an alert to confirm preorder first, wait for it dismissed then show an upsell (rate limited). + preorderNotifyAlert.cancelAction = subscribePageRateLimitedAction; + unsubscribedAction = preorderNotifyAlert; + } + else { + const preorderNotifyToast = new models.AlertAction("toast"); + preorderNotifyToast.title = ""; + preorderNotifyToast.artwork = bellArtwork; + // rdar://144833292 (Remove Cancel Preorder Native Loc) + // Once the service strings are localised and deployed, we should remove the native loc strings. + preorderNotifyToast.message = preprocessor.GAMES_TARGET + ? objectGraph.loc.string("PreOrder.Notify.Message") + : objectGraph.loc.string("PREORDER_NOTIFY_MESSAGE"); + preorderNotifyToast.toastDuration = 1.5; + // For those not subscribed, show an upsell (rate limited) and a toast. + const compoundRateLimitedAction = new models.RateLimitedAction("arcade-preorder", new models.CompoundAction([subscribePageAction, preorderNotifyToast])); + compoundRateLimitedAction.rateLimit = objectGraph.bag.arcadePreOrderUpsellLimitSeconds; + compoundRateLimitedAction.fallbackAction = preorderNotifyToast; + unsubscribedAction = compoundRateLimitedAction; + } + // Users who have not opted into notifications on their device will be shown a full screen prompt to turn notifications on. To facilitate this we swap out toast and upsell actions for a blank action + // - For Arcade subscribed users we don't want to overlay the toast in this situation + // - For Arcade unsubscribed users it was decided that the notifications prompt should take precedence and we shouldn't show that either + // - To help keep co-ordination of prompts/toasts/sheets simpler this toast and arcade upsell should/will ultimately be moved to be part of an ODJ + // Until rdar://144839099 (TCC Notification for Moltres), we assume the user is authorized for notifications. + const isUnauthorizedForUserNotifications = !(preprocessor.GAMES_TARGET || objectGraph.client.isAuthorizedForUserNotifications()); + if (objectGraph.bag.newEventsForODJAreEnabled && isUnauthorizedForUserNotifications) { + return new models.ArcadeSubscriptionStateAction(new models.BlankAction(), new models.BlankAction(), new models.BlankAction(), new models.BlankAction()); + } + else { + return new models.ArcadeSubscriptionStateAction(unsubscribedAction, subscribedAction, subscribedAction, subscribedAction); + } +} +/** + * Wrap the offer in an `OfferAlertAction` for tvOS in order to check for requirements and confirm purchase. + */ +function createOfferAlertActionIfNeeded(objectGraph, offerAction, data, isPreorder, metricsOptions) { + if (objectGraph.client.deviceType === "tv") { + return createTVOfferAlertActionIfNeeded(objectGraph, offerAction, null, data, isPreorder, metricsOptions); + } + else if (objectGraph.client.isVision) { + return createVisionOfferAlertActionIfNeeded(objectGraph, offerAction, null, data, isPreorder, metricsOptions); + } + else { + return { + startAction: serverData.isNull(offerAction) ? null : offerAction, + underlyingOfferAction: null, + }; + } +} +function createTVOfferAlertActionIfNeeded(objectGraph, offerAction, alertCompletionActionOverride, data, isPreorder, metricsOptions) { + if (serverData.isNull(offerAction)) { + return { + startAction: null, + underlyingOfferAction: null, + }; + } + const offerAlertAction = new models.OfferAlertAction(); + const isFree = isFreeFromOfferAction(objectGraph, offerAction); + const appName = offerAction.purchaseConfiguration.appName; + // Configure requirement checks + offerAlertAction.shouldCheckForAvailableDiskSpace = !isPreorder; + if (objectGraph.host.isTV) { + offerAlertAction.remoteControllerRequirement = gameController.controllerRequirementFromData(objectGraph, data); + offerAlertAction.shouldCheckForGameController = false; + } + else { + offerAlertAction.remoteControllerRequirement = "NO_BADGE"; + offerAlertAction.shouldCheckForGameController = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresGameController"); + } + // Configure restrictions check + const contentRating = ageRatings.value(objectGraph, data, true); + if (serverData.isDefinedNonNull(contentRating)) { + offerAlertAction.checkRestrictionsForContentRating = contentRating; + } + // Configure confirmation alert title + if (isFree) { + const titleFormat = isPreorder + ? objectGraph.loc.string("OfferAlert.TV.Title.PredorderFree") + : objectGraph.loc.string("OfferAlert.TV.Title.Free"); + offerAlertAction.title = titleFormat.replace("{title}", appName); + } + else { + const titleFormat = isPreorder + ? objectGraph.loc.string("OfferAlert.TV.Title.PreorderPaid") + : objectGraph.loc.string("OfferAlert.TV.Title.Paid"); + offerAlertAction.title = titleFormat.replace("{title}", appName).replace("{price}", offerAction.priceFormatted); + } + // Configure confirmation action title + const buyAction = objects.shallowCopyOf(offerAction); + if (isPreorder) { + buyAction.title = objectGraph.loc.string("OfferButton.Title.Preorder"); + } + else if (isFree) { + buyAction.title = objectGraph.loc.string("OfferButton.Title.Get"); + } + else { + buyAction.title = objectGraph.loc.string("OfferButton.Title.Buy"); + } + // Configure completion action + const alertCompletionAction = isNothing(alertCompletionActionOverride) + ? buyAction + : alertCompletionActionOverride; + offerAlertAction.completionAction = alertCompletionAction; + // Configure footer message + offerAlertAction.shouldIncludeActiveAccountInFooterMessage = true; + const footerMessage = []; + const hasInAppPurchases = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasInAppPurchases"); + if (hasInAppPurchases) { + footerMessage.push(objectGraph.loc.string("OFFERS_IN_APP_PURCHASES", "Offers In-App Purchases")); + } + if (footerMessage.length > 0) { + offerAlertAction.footerMessage = footerMessage.join(objectGraph.loc.string("TV_OFFER_ALERT_FOOTER_LINE_BREAK")); + } + // Add metrics event for initiating purchase + offerAlertAction.impressionMetrics = buyAction.impressionMetrics; + // Create an instance of offer alert with requirements checks, + // but no confirmation prompt for redownloads and updates + const offerAlertActionWithoutConfirmationPrompt = objects.shallowCopyOf(offerAlertAction); + offerAlertActionWithoutConfirmationPrompt.shouldPromptForConfirmation = false; + offerAlertActionWithoutConfirmationPrompt.title = null; + offerAlertActionWithoutConfirmationPrompt.footerMessage = null; + // By default, perform requirements checks, but avoid confirmation prompt + const offerStateAction = new models.OfferStateAction(offerAction.adamId, offerAlertActionWithoutConfirmationPrompt); + // Perform requirements checks and show confirmation for buys + if (!content.isArcadeSupported(objectGraph, data)) { + offerStateAction.buyAction = offerAlertAction; + } + // Run offer action directly for opens, bypassing all prompts + offerStateAction.openAction = offerAction; + return { + startAction: offerStateAction, + underlyingOfferAction: buyAction, + }; +} +function createVisionOfferAlertActionIfNeeded(objectGraph, offerAction, alertCompletionActionOverride, data, isPreorder, metricsOptions) { + if (serverData.isNull(offerAction)) { + return { + startAction: null, + underlyingOfferAction: null, + }; + } + const offerAlertAction = new models.OfferAlertAction(); + const isFree = isFreeFromOfferAction(objectGraph, offerAction); + offerAlertAction.remoteControllerRequirement = gameController.controllerRequirementFromData(objectGraph, data); + offerAlertAction.spatialControllerRequirement = gameController.spatialControllerRequirementFromData(objectGraph, data); + // Configure confirmation action title + const buyAction = objects.shallowCopyOf(offerAction); + if (isPreorder) { + buyAction.title = objectGraph.loc.string("OFFER_BUTTON_TITLE_PREORDER"); + } + else if (isFree) { + buyAction.title = objectGraph.loc.string("OFFER_BUTTON_TITLE_GET"); + } + else { + buyAction.title = objectGraph.loc.string("OFFER_BUTTON_TITLE_BUY"); + } + // Configure completion action + const alertCompletionAction = isNothing(alertCompletionActionOverride) + ? buyAction + : alertCompletionActionOverride; + offerAlertAction.completionAction = alertCompletionAction; + // Add metrics event for initiating purchase + offerAlertAction.impressionMetrics = buyAction.impressionMetrics; + // Create an instance of offer alert with requirements checks, + // but no confirmation prompt for redownloads and updates + const offerAlertActionWithoutConfirmationPrompt = objects.shallowCopyOf(offerAlertAction); + offerAlertActionWithoutConfirmationPrompt.shouldPromptForConfirmation = false; + offerAlertActionWithoutConfirmationPrompt.title = null; + offerAlertActionWithoutConfirmationPrompt.footerMessage = null; + /** + * - If the app is buyable or updatable: perform requirement checks, then perform the `buyAction` + * - If the app is cancellable: perform the `buyAction` which will cancel the downloading + * - If the app is openable: perform the `buyAction` which will opening the app + * - Otherwise by default perform the offer alert action without confirmation prompt + */ + const offerStateAction = new models.OfferStateAction(offerAction.adamId, offerAlertActionWithoutConfirmationPrompt); + offerStateAction.buyAction = offerAlertAction; + offerStateAction.cancelAction = buyAction; + offerStateAction.openAction = buyAction; + return { + startAction: offerStateAction, + underlyingOfferAction: buyAction, + }; +} +/** + * Add two-phased confirmation to the buy action. + * @note This function should only be used on clients that need two-phased confirmation, at the moment, macOS. + * @param buyAction The action to perform buy. + * @param isPreorder Whether the buy is for a preorder. + * @param metricsOptions The metrics options to use for reporting metrics actions. + * @returns {Action} A configured confirmation action, or the original action if wrapping was not needed. + */ +export function wrapOfferInTwoPhasedConfirmationActionIfNeeded(objectGraph, buyAction, isPreorder, metricsOptions) { + if (serverData.isNull(buyAction)) { + return null; + } + if (preprocessor.GAMES_TARGET) { + // There's a confirmation built-in to the flow in Cheer, + // thus two-phased confirmation is not necessary. + return buyAction; + } + // There's a confirmation built-in to the flow for the new payment method as well as pre-orders, + // thus two-phased confirmation is not necessary. + if (!objectGraph.bag.enableTwoPhaseOfferConfirmation || isPreorder) { + return buyAction; + } + // Create metrics event for initiating confirmation + const confirmationInitiationAction = new models.BlankAction(); + confirmationInitiationAction.impressionMetrics = buyAction.impressionMetrics; + const confirmationMetricsOptions = objects.shallowCopyOf(metricsOptions); + if (!serverData.isNull(confirmationMetricsOptions)) { + confirmationMetricsOptions.actionType = "buyInitiate"; + confirmationMetricsOptions.targetType = "button"; + metricsHelpersClicks.addClickEventToAction(objectGraph, confirmationInitiationAction, confirmationMetricsOptions); + } + // Create confirmation action + const confirmationAction = new models.OfferConfirmationAction(buyAction, confirmationInitiationAction); + // Add accessibility confirmation action + confirmationAction.confirmationAccessibilityAction = wrapOfferActionInConfirmAlertAction(objectGraph, buyAction); + return confirmationAction; +} +/** + * Configures a tvOS-only intermediate action for an offer action. + * @param offerAction The underlying offer action to perform the buy with. + * @param requiresGameController Whether or not the product requires a game controller. + * @returns {OfferStateAction} The tvOS-only-wrapped action. + */ +function tvOnlyAppActionForBuyAction(objectGraph, offerAction, requiresGameController) { + const tvOnlyBuyAlert = new models.AlertAction("default"); + tvOnlyBuyAlert.title = objectGraph.loc.string("Alert.Buy.TvOnly.Title"); + tvOnlyBuyAlert.message = objectGraph.loc.string("Alert.Buy.TvOnly.Message"); + tvOnlyBuyAlert.isCancelable = true; + if (requiresGameController) { + tvOnlyBuyAlert.buttonActions = [requiresGameControllerActionForBuyAction(objectGraph, offerAction)]; + } + else { + tvOnlyBuyAlert.buttonActions = [offerAction]; + } + tvOnlyBuyAlert.buttonTitles = [objectGraph.loc.string("Alert.Buy.TvOnly.ButtonTitle")]; + const offerStateAction = new models.OfferStateAction(offerAction.adamId, tvOnlyBuyAlert); + offerStateAction.title = offerAction.title; + return offerStateAction; +} +/** + * Configures a visionOS intermediate action for an offer action. + * @param {AppStoreObjectGraph} objectGraph The object graph associated with this purchase. + * @param {models.OfferAction} offerAction The underlying offer action to perform the buy with. + * @param {mediaDataStructure.Data} data MAPI data blob describing the content model which offer belongs to. + * @param {boolean} isFree Whether the buy is for a free product. + * @param {boolean} isArcade Whether the buy is for an Arcade app. + * @returns {OfferStateAction} The vision-only-wrapped action. + */ +function visionAppActionForBuyAction(objectGraph, offerAction, data, isFree, isArcade) { + const visionOnlyBuyAlert = visionAppAlertForBuyAction(objectGraph, offerAction, data, isFree, isArcade); + const offerStateAction = new models.OfferStateAction(offerAction.adamId, visionOnlyBuyAlert); + offerStateAction.title = offerAction.title; + return offerStateAction; +} +/** + * Configures an alert action for vision-only intermediate action. + * @param {AppStoreObjectGraph} objectGraph The object graph associated with this purchase. + * @param {models.Action} buttonAction The underlying action to perform. + * @param {mediaDataStructure.Data} data MAPI data blob describing the content model which offer belongs to. + * @param {boolean} isFree Whether the buy is for a free product. + * @param {boolean} isArcade Whether the buy is for an Arcade app. + * @returns {AlertAction} The alert action for the vision-only buy action. + */ +function visionAppAlertForBuyAction(objectGraph, buttonAction, data, isFree, isArcade) { + const visionOnlyBuyAlert = new models.AlertAction("default"); + visionOnlyBuyAlert.isCancelable = true; + visionOnlyBuyAlert.buttonActions = [buttonAction]; + visionOnlyBuyAlert.imageName = "vision.pro"; + if (supportsVisionDownloadFromVisionCompanion(objectGraph, data)) { + // Shows the purchase details from the Companion app + if (isFree) { + visionOnlyBuyAlert.title = objectGraph.loc.string("Alert.Buy.VisionOnly.Free.RemoteDownloads.Title"); + visionOnlyBuyAlert.message = objectGraph.loc.string("Alert.Buy.VisionOnly.Free.RemoteDownloads.Message"); + visionOnlyBuyAlert.buttonTitles = [ + objectGraph.loc.string("Alert.Buy.VisionOnly.Free.RemoteDownloads.ButtonTitle"), + ]; + } + else { + visionOnlyBuyAlert.title = objectGraph.loc.string("Alert.Buy.VisionOnly.Paid.RemoteDownloads.Title"); + visionOnlyBuyAlert.message = objectGraph.loc.string("Alert.Buy.VisionOnly.Paid.RemoteDownloads.Message"); + visionOnlyBuyAlert.buttonTitles = [ + objectGraph.loc.string("Alert.Buy.VisionOnly.Paid.RemoteDownloads.ButtonTitle"), + ]; + } + } + else { + // Show the default vision only purchase. + visionOnlyBuyAlert.title = objectGraph.loc.string("Alert.Buy.VisionOnly.Title"); + if (isArcade) { + visionOnlyBuyAlert.message = objectGraph.loc.string("Alert.Buy.VisionOnly.Message.Arcade"); + } + else { + visionOnlyBuyAlert.message = objectGraph.loc.string("Alert.Buy.VisionOnly.Message"); + } + visionOnlyBuyAlert.buttonTitles = [objectGraph.loc.string("Alert.Buy.VisionOnly.ButtonTitle")]; + } + return visionOnlyBuyAlert; +} +/** + * This will return true if we are running in the companion app and the purchase we are working with + * will successfully download and run on a vision pro. + * @param {AppStoreObjectGraph} objectGraph The object graph associated with this purchase. + * @param {mediaDataStructure.Data} data MAPI data blob describing the content model which offer belongs to. + * @returns {boolean} true if the data/product can be downloaded onto vision pro from within companion. + */ +export function supportsVisionPlatformForVisionCompanion(objectGraph, data) { + if (!objectGraph.client.isCompanionVisionApp) { + return false; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const purchaseSupportsVision = appPlatforms.includes("vision") || content.supportsVisionOSCompatibleIOSBinaryOnAnyClient(data); + if (!purchaseSupportsVision) { + return false; + } + return true; +} +/** + * This will return true if we are running in the Vision companion app and the purchase we are working with + * will successfully download and run on a vision pro and the user has associated vision pros to download + * this purchase to. + * @param {AppStoreObjectGraph} objectGraph The object graph associated with this purchase. + * @param {mediaDataStructure.Data} data MAPI data blob describing the content model which offer belongs to. + * @returns {boolean} true if the data/product can be downloaded onto vision pro from within companion. + */ +export function supportsVisionDownloadFromVisionCompanion(objectGraph, data) { + if (!supportsVisionPlatformForVisionCompanion(objectGraph, data)) { + return false; + } + return hasRemoteDownloadIdentifiers(objectGraph); +} +/** + * This will return true if the client passed in any remoteDownloadIdentifiers and false otherwise. + * @param {AppStoreObjectGraph} objectGraph The object graph associated with this purchase. + * @returns {boolean} true if remoteDownloadIdentifiers contains anything. + */ +export function hasRemoteDownloadIdentifiers(objectGraph) { + if (isNothing(objectGraph.client.remoteDownloadIdentifiers)) { + return false; + } + const hasNoDevicesForRemoteDownload = objectGraph.client.remoteDownloadIdentifiers.length === 0; + if (hasNoDevicesForRemoteDownload) { + return false; + } + return true; +} +/** + * Determines whether the product contains a macOS IPA install + * @param objectGraph Current object graph + * @param data The product data + * @returns True if the product has a macOS IPA installer + */ +export function hasMacIPAPackageForData(objectGraph, data) { + return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasMacIPAPackage"); +} +/** + * Configures a game-controller-required action. + * @param action The action to carry out on the alert confirmation. + * @returns {AlertAction} The alert action to use for the game-controller-required dialog. + */ +function requiresGameControllerActionForBuyAction(objectGraph, action) { + const requiresControllerAlert = new models.AlertAction("default"); + requiresControllerAlert.title = objectGraph.loc.string("Alert.Buy.TvGameControllerRequired.Title"); + requiresControllerAlert.message = objectGraph.loc.string("Alert.Buy.TvGameControllerRequired.Message"); + requiresControllerAlert.buttonTitles = [objectGraph.loc.string("Alert.Buy.TvGameControllerRequired.ButtonTitle")]; + requiresControllerAlert.isCancelable = true; + requiresControllerAlert.buttonActions = [action]; + return requiresControllerAlert; +} +/** + * Configures a watchOS-only intermediate action for an offer action. + * @param offerAction The underlying offer action to perform the buy with. + * @returns {OfferStateAction} The watchOS-only-wrapped action. + */ +function watchOnlyAppActionForBuyAction(objectGraph, offerAction) { + const tvOnlyBuyAlert = new models.AlertAction("default"); + tvOnlyBuyAlert.title = objectGraph.loc.string("Alert.Buy.WatchOnly.Title"); + tvOnlyBuyAlert.message = objectGraph.loc.string("Alert.Buy.WatchOnly.Message"); + tvOnlyBuyAlert.isCancelable = true; + tvOnlyBuyAlert.buttonActions = [offerAction]; + tvOnlyBuyAlert.buttonTitles = [objectGraph.loc.string("Alert.Buy.WatchOnly.ButtonTitle")]; + const offerStateAction = new models.OfferStateAction(offerAction.adamId, tvOnlyBuyAlert); + offerStateAction.title = offerAction.title; + return offerStateAction; +} +/** + * Configures a watchOS update required intermediate action for an offer action. + * @param offerAction The underlying offer action to perform the buy with. + * @returns {OfferStateAction} The watchOS update required wrapped action. + */ +function watchUpdateRequiredActionForBuyAction(objectGraph, offerAction, minOSVersion) { + const alert = new models.AlertAction("default"); + alert.title = objectGraph.loc + .string("ProductPage.WatchOSUpdateRequired.Title") + .replace("{osVersion}", minOSVersion); + alert.message = objectGraph.loc + .string("ProductPage.WatchOSUpdateRequired.Message") + .replace("{osVersion}", minOSVersion); + alert.buttonActions = [offerAction]; + alert.buttonTitles = [objectGraph.loc.string("Action.OK")]; + const offerStateAction = new models.OfferStateAction(offerAction.adamId, offerAction); + offerStateAction.buyAction = alert; + return offerStateAction; +} +function wrapOfferActionInConfirmAlertAction(objectGraph, action) { + if (serverData.isNull(action)) { + return null; + } + const alert = new models.AlertAction("default"); + if (serverData.isNull(action.priceFormatted)) { + alert.title = objectGraph.loc.string("GET"); + alert.message = "Are you sure you want to get " + action.purchaseConfiguration.appName; + } + else { + alert.title = "Buy App"; + alert.message = `Are you sure you want to buy ${action.purchaseConfiguration.appName} for ${action.priceFormatted}`; + } + alert.isCancelable = true; + alert.buttonActions = [action]; + const offerStateAction = new models.OfferStateAction(action.adamId, action); + offerStateAction.buyAction = alert; + return offerStateAction; +} +/** + * Create object describing the visual aspects of an offer action. + * + * @param {AppStoreObjectGraph} objectGraph Object graph. + * @param {OfferAction} action The offer action to create display properties for. + * @param {OfferType} type The type of offer to display. + * @param {Data} data MAPI data blob describing the content model which offer belongs to. + * @param {boolean} isPreorder Whether the buy is for a preorder. + * @param {boolean} isContainedInPreorderExclusiveShelf Whether this offer is displayed in a coming soon shelf. + * @param {OfferStyle} style The style of offer button. Returned display property may have different style, depending on filters. + * @param {OfferEnvironment} environment The environment which offer button will be displayed in. + * @param {JSONData} discountData The JSON blob describing discounts to visualize. + * @param {boolean} isParentAppFree Flag indicating whether parent app was free app. + * @param {OfferContext} context Contextual info about offer. + * @param {boolean} shouldNavigateToProductPage Whether this offer should navigate to the product page. + * @param {boolean} isAd Whether this offer is for an ad. + * @param {ClientIdentifier} An optional override of the client identifier + * @param {Data} An optional fallback of the parent App Data used when constructing IAP offers + * @returns {OfferDisplayProperties} The display properties for offer button for given product and offer. + * + * @seealso `personalizedCMCDisplayPropertiesFromBuyButtonMetadata`, the display properties created from the old-school `buyButtonMetadataUrl` endpoint. + */ +export function displayPropertiesFromOfferAction(objectGraph, action, type, data, isPreorder, isContainedInPreorderExclusiveShelf, style, environment, discountData, isParentAppFree, context = "default", shouldNavigateToProductPage = false, isAd = false, clientIdentifierOverride = undefined, parentAppData = undefined, isBuyDisallowed = false) { + if (serverData.isNull(action)) { + return null; + } + return validation.context("displayPropertiesFromOfferAction", () => { + var _a; + let derivedStyle = style; + // We can't prevent deep links into apps that are filtered, + // but we should disable the buy button (if present) to prevent purchases + if (!isBuyDisallowed && filtering.shouldFilter(objectGraph, data, 77238 /* filtering.Filter.Offers */)) { + derivedStyle = "disabled"; + } + // Disable preorders on unsupported OS' (if we have a buy button) + if (!isBuyDisallowed && !lockups.deviceHasCapabilitiesFromData(objectGraph, data)) { + derivedStyle = "disabled"; + } + // Disable buy button when we are in companion but cannot install app onto visionOS (if we have a buy button) + if (!isBuyDisallowed && + objectGraph.client.isCompanionVisionApp && + !supportsVisionDownloadFromVisionCompanion(objectGraph, data)) { + derivedStyle = "disabled"; + } + const parentData = (_a = lockups.parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : parentAppData; + let parentAdamId; + if (parentData) { + parentAdamId = parentData.id; + } + let displayProperties = new models.OfferDisplayProperties(type, action.adamId, action.bundleId, derivedStyle, parentAdamId, environment); + // Configure complementary offer label for preorders + displayProperties.isPreorder = isPreorder; + // If this is for an ad allow localization overrides if one is in the bag. + const useAdsLocale = isAd && context === "default" && isSome(objectGraph.bag.adsOverrideLanguage); + const adsOverrideLocalizer = useAdsLocale ? objectGraph.adsLoc : objectGraph.loc; + displayProperties.useAdsLocale = useAdsLocale; + // Arcade apps that are "Coming Soon" do not use the `preorder` label style. + if (isPreorder) { + if (content.isArcadeSupported(objectGraph, data)) { + displayProperties.offerLabelStyle = "arcadeComingSoon"; + if (objectGraph.client.isVision && !isContainedInPreorderExclusiveShelf) { + // In visionOS, we would like lockup always display `Coming Soon` instead of release date + // Except for lockup within Coming Soon shelf + displayProperties.subtitles["expectedReleaseDate"] = objectGraph.loc.string("ARCADE_PREORDER_LOCKUP_COMING_SOON"); + } + else { + displayProperties.subtitles["expectedReleaseDate"] = content.dynamicPreorderDateFromData(objectGraph, data, objectGraph.loc.string("ARCADE_PREORDER_LOCKUP_COMING_SOON")); + } + displayProperties.titleSymbolNames["preorderedSubscribed"] = "checkmark"; + displayProperties.titleSymbolNames["preorderedNotSubscribed"] = "bell.fill"; + // If the server-side value is an uppercase "COMING SOON" replace it with the titlecase variant for lockups. + if (displayProperties.subtitles["expectedReleaseDate"] === + objectGraph.loc.string("ARCADE_PREORDER_COMING_SOON")) { + displayProperties.subtitles["expectedReleaseDate"] = objectGraph.loc.string("ARCADE_PREORDER_LOCKUP_COMING_SOON"); + } + } + else { + displayProperties.offerLabelStyle = "preorder"; + displayProperties.titleSymbolNames["standard"] = "checkmark"; + const expectedReleaseDate = content.dynamicPreorderDateFromData(objectGraph, data, ""); + if (isSome(expectedReleaseDate)) { + displayProperties.subtitles["expectedReleaseDate"] = expectedReleaseDate; + } + } + } + // Configure offer titles + const isFree = isFreeFromOfferAction(objectGraph, action); + displayProperties.isFree = isFree; + let standardTitle = null; + if (context === "default" && + (data.type === "app-bundles" || + content.isMacOSInstaller(objectGraph, data) || + content.isUnsupportedByCurrentCompanion(objectGraph, data) || + shouldNavigateToProductPage)) { + standardTitle = objectGraph.loc.string("OfferButton.Title.View"); + } + else if (context === "flowPreview" && + (data.type === "app-bundles" || + content.isMacOSInstaller(objectGraph, data) || + content.isUnsupportedByCurrentCompanion(objectGraph, data))) { + // We do not want to support "View" in flow preview actions + return null; + } + else if (context === "productPageBrowserChoice") { + standardTitle = objectGraph.loc.string("OfferButton.Title.Select"); + } + else if (isFree) { + if (context === "flowPreview") { + if (isPreorder) { + standardTitle = objectGraph.loc.string("OfferButton.FlowPreview.Preorder"); + } + else { + standardTitle = objectGraph.loc.string("OfferButton.FlowPreview.Get"); + } + } + else { + standardTitle = action.title; + } + } + else if (objectGraph.client.isTV && (environment === "productPage" || environment === "arcadeProductPage")) { + standardTitle = objectGraph.loc + .string("OfferButton.Title.BuyWithPrice") + .replace("{price}", action.priceFormatted); + } + else if (context === "flowPreview") { + if (isPreorder) { + standardTitle = objectGraph.loc + .string("OfferButton.FlowPreview.PreorderWithPrice") + .replace("{price}", action.priceFormatted); + } + else { + standardTitle = objectGraph.loc + .string("OfferButton.FlowPreview.BuyWithPrice") + .replace("{price}", action.priceFormatted); + } + } + else { + standardTitle = action.priceFormatted; + } + displayProperties.titles["standard"] = standardTitle; + displayProperties.priceFormatted = action.priceFormatted; + // Confirmation buys are not supported in flow preview + if (objectGraph.bag.enableTwoPhaseOfferConfirmation && context !== "flowPreview") { + displayProperties.titles["confirmation"] = isFree + ? objectGraph.loc.string("OfferButton.Title.ConfirmGet") + : objectGraph.loc.string("OfferButton.Title.ConfirmBuy"); + } + if (content.isArcadeSupported(objectGraph, data)) { + if (context === "flowPreview") { + const standardText = objectGraph.loc.string("OfferButton.FlowPreview.Arcade.Standard"); + displayProperties.titles["standard"] = standardText; + displayProperties.titles["trial"] = standardText; + displayProperties.titles["open"] = objectGraph.loc.string("OfferButton.FlowPreview.Arcade.Open"); + displayProperties.titles["notSubscribed"] = standardText; + if (isPreorder) { + displayProperties.titles["preorderSubscribed"] = objectGraph.loc.string("OfferButton.FlowPreview.Arcade.PreorderSubscribed"); + displayProperties.titles["preorderNotSubscribed"] = objectGraph.loc.string("OfferButton.FlowPreview.Arcade.PreorderNotSubscribed"); + } + } + else { + const standardText = adsOverrideLocalizer.string("OfferButton.Arcade.Title.Standard"); + displayProperties.titles["standard"] = standardText; + displayProperties.titles["trial"] = standardText; + displayProperties.titles["open"] = adsOverrideLocalizer.string("OfferButton.Arcade.Title.Open"); + displayProperties.titles["notSubscribed"] = standardText; + if (isPreorder) { + displayProperties.titles["preorderSubscribed"] = adsOverrideLocalizer.string("OfferButton.Arcade.Title.PreorderSubscribed"); + displayProperties.titles["preorderNotSubscribed"] = adsOverrideLocalizer.string("OfferButton.Arcade.Title.PreorderNotSubscribed"); + displayProperties.titles["preorderedSubscribed"] = adsOverrideLocalizer.string("OfferButton.Arcade.Title.PreorderedSubscribed"); + displayProperties.titles["preorderedNotSubscribed"] = adsOverrideLocalizer.string("OfferButton.Arcade.Title.PreorderedNotSubscribed"); + } + } + } + // Introductory Pricing + // Not supported in flow preview + if (discountData && context !== "flowPreview") { + const discountType = serverData.asString(discountData, "modeType"); + const discountPriceFormatted = serverData.asString(discountData, "priceFormatted"); + if (serverData.isDefinedNonNull(discountPriceFormatted) && serverData.isDefinedNonNull(discountType)) { + let discountOwnedParentTitle = null; + let discountUnownedParentTitle = null; + switch (discountType) { + case "FreeTrial": + if (isParentAppFree) { + discountOwnedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.FreeTrial"); + discountUnownedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.FreeTrial"); + } + else { + discountOwnedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.FreeTrial"); + discountUnownedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.Trial"); + } + break; + case "PayUpFront": + const payUpfrontPrice = objectGraph.loc + .string("OfferButton.IntroPrice.PaidUpfront.Trial") + .replace("{price}", discountPriceFormatted); + if (isParentAppFree) { + discountOwnedParentTitle = payUpfrontPrice; + discountUnownedParentTitle = payUpfrontPrice; + } + else { + discountOwnedParentTitle = payUpfrontPrice; + discountUnownedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.Trial"); + } + break; + case "PayAsYouGo": + discountOwnedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.Trial"); + discountUnownedParentTitle = objectGraph.loc.string("OfferButton.IntroPrice.Trial"); + break; + default: + break; + } + displayProperties.titles["discountOwnedParent"] = discountOwnedParentTitle; + displayProperties.titles["discountUnownedParent"] = discountUnownedParentTitle; + displayProperties.subtitles["discountOwnedParent"] = + objectGraph.loc.string("INTRO_PRICE_OFFER_SUBTITLE"); + displayProperties.subtitles["discountUnownedParent"] = + objectGraph.loc.string("INTRO_PRICE_OFFER_SUBTITLE"); + // Determine whether the button title is too long and the subtitle should be pushed below. + const maxCharacterCount = 10; + let isWidthConstrained = false; + for (const titleKey of Object.keys(displayProperties.titles)) { + const title = displayProperties.titles[titleKey]; + if (title.length > maxCharacterCount) { + isWidthConstrained = true; + break; + } + } + if (isWidthConstrained) { + displayProperties = displayProperties.newOfferDisplayPropertiesChangingAppearance(false, null, "widthConstrainedLockup"); + } + } + } + // iAPs + const hasInAppPurchases = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasInAppPurchases"); + const hasExternalPurchases = hasExternalPurchasesForData(objectGraph, data); + const externalPurchasesEnabled = externalPurchasesPlacementIsEnabled(objectGraph, "lockup"); + const showExternalPurchasesSubtitle = hasExternalPurchases && externalPurchasesEnabled; + displayProperties.hasInAppPurchases = hasInAppPurchases; + displayProperties.hasExternalPurchases = showExternalPurchasesSubtitle; + if (hasInAppPurchases || showExternalPurchasesSubtitle) { + const inAppPurchasesKey = "Offer.InlineInAppPurchases"; + const subtitleKey = showExternalPurchasesSubtitle + ? "OfferButton.ExternalPurchases.Subtitle" + : inAppPurchasesKey; + const standardSubtitle = adsOverrideLocalizer.string(subtitleKey); + displayProperties.subtitles["standard"] = standardSubtitle; + // Confirmation buys are not supported in flow preview + if (objectGraph.bag.enableTwoPhaseOfferConfirmation && context !== "flowPreview") { + displayProperties.subtitles["confirmation"] = standardSubtitle; + } + } + // System Apps + displayProperties.isDeletableSystemApp = + sad.systemApps(objectGraph).isSystemAppFromData(data) && + !content.isUnsupportedByCurrentCompanion(objectGraph, data); + // Content Restrictions + const contentRating = ageRatings.value(objectGraph, data, true); + displayProperties.contentRating = contentRating !== null && contentRating !== void 0 ? contentRating : undefined; + // OS Eligibility + displayProperties.appCapabilities = action.purchaseConfiguration.appCapabilities; + // App bundles + // Not supported in flow preview + if (data.type === "app-bundles" && context !== "flowPreview") { + displayProperties.offerToken = { + offerAction: action, + offerDisplayProperties: objects.shallowCopyOf(displayProperties), + }; + } + return displayProperties; + }); +} +export function macFileSizeInBytesFromData(objectGraph, data) { + const offerData = offerDataFromData(objectGraph, data); + if (serverData.isNull(offerData)) { + return null; + } + const allAssets = serverData.asArrayOrEmpty(offerData, "assets"); + if (!allAssets.length) { + return null; + } + for (const assetData of allAssets) { + const flavor = serverData.asString(assetData, "flavor"); + if (flavor === "macSoftware") { + return serverData.asNumber(assetData, "size"); + } + } + return null; +} +// region CMC Personalization +/** + * Determine the type of offer type given offer metadata from `buyButtonMetadata` endpoint. + * + * @param {JSONData} buyButtonMetadata Metadata for offer returned from personalized offer endpoint. + * @returns {PersonalizedOfferType} Type of personalized offer. + */ +export function personalizedOfferTypeFromBuyButtonMetadata(objectGraph, buyButtonMetadata) { + return validation.context("personalizedOfferTypeFromBuyButtonMetadata", () => { + const offers = serverData.asArrayOrEmpty(buyButtonMetadata, "offers"); + if (offers.length === 0) { + return null; + } + for (const offer of offers) { + const type = serverData.asString(offer, "type"); + if (type) { + return type; + } + } + return "none"; + }); +} +/** + * Create a personalized offer action specific for CMC given personalized offer metadata and original offer. + * + * @param {JSONData} buyButtonMetadata The personalized offer metadata to build offer with. + * @param {OfferAction} originalOfferAction The original offer action to borrow metrics from. + * @returns {OfferAction} A offer action with complete my bundle personalization applied. + */ +export function personalizedCMCOfferActionFromBuyButtonMetadata(objectGraph, buyButtonMetadata, originalOfferAction, isPreorder) { + return validation.context("personalizedCMCOfferActionFromBuyButtonMetadata", () => { + const offers = serverData.asArrayOrEmpty(buyButtonMetadata, "offers"); + if (offers.length === 0) { + return null; + } + let type; + let personalizedOfferData = null; + for (const offer of offers) { + type = serverData.asString(offer, "type"); + switch (type) { + case "complete": + case "purchased": + personalizedOfferData = offer; + break; + default: + personalizedOfferData = null; + break; + } + } + if (!personalizedOfferData) { + return null; + } + /* Buy Params */ + let buyParams = serverData.asString(personalizedOfferData, "buyParams"); + if (type === "complete" && serverData.isNull(buyParams)) { + validation.unexpectedNull("ignoredValue", "string", "item.offer.buyParams"); + return null; + } + else if (!buyParams) { + buyParams = ""; + } + /* Purchase Configuration */ + const originalConfiguration = originalOfferAction.purchaseConfiguration; + const purchaseConfiguration = new models.PurchaseConfiguration(buyParams, originalConfiguration.vendor, originalConfiguration.appName, originalConfiguration.bundleId, originalConfiguration.appPlatforms, originalConfiguration.isPreorder, originalConfiguration.excludeAttribution, originalConfiguration.metricsPlatformDisplayStyle, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, originalConfiguration.appCapabilities, originalConfiguration.isDefaultBrowser, originalConfiguration.remoteDownloadIdentifiers, originalConfiguration.hasMacIPAPackage, originalConfiguration.contentRating); + purchaseConfiguration.pageInformation = originalConfiguration.pageInformation; + const action = internalOfferActionFromOfferData(objectGraph, personalizedOfferData, originalOfferAction.adamId, purchaseConfiguration, false); + metricsHelpersClicks.addBuyEventToOfferActionInheritingMetrics(objectGraph, action, originalOfferAction, isPreorder); + return action; + }); +} +/** + * Create offer display properties specific for CMC offers from personalized action and original display properties. + * + * @param {OfferAction} personalizedAction The personalized CMC offer action created via `personalizedCMCOfferActionFromBuyButtonMetadata`. + * @param {OfferType} type The type of offer to display. + * @param {OfferDisplayProperties} originalOfferDisplayProperties The original offer display property to inherit some values from. + * @param {OfferStyle} style The style of offer button. Returned display property may have different style, depending on filters. + * @returns {OfferDisplayProperties} Offer display properties for a personalized CMC action. + * + * @note This code is similar to `media_displayPropertiesFromOfferAction`, but inserts on old-school non-MAPI data from + * `buyButtonMetadataUrl` endpoint to original offer display properties. It assumes that bundles cannot: + * - Be preordered + * - Have introductory pricing + * - Be filtered + * - Have parent apps + * - Be a deletable system app + * + * @seealso `media_displayPropertiesFromOfferAction`, for how display properties are created through MAPI data. + */ +export function personalizedCMCDisplayPropertiesFromBuyButtonMetadata(objectGraph, personalizedAction, type, originalOfferDisplayProperties, style) { + if (serverData.isNull(personalizedAction)) { + return null; + } + return validation.context("personalizedCMCDisplayPropertiesFromBuyButtonMetadata", () => { + const displayProperties = new models.OfferDisplayProperties(type, personalizedAction.adamId, personalizedAction.bundleId, style); + // Configure offer titles + const isFree = isFreeFromOfferAction(objectGraph, personalizedAction); + displayProperties.isFree = isFree; + let standardTitle = null; + if (isFree) { + standardTitle = personalizedAction.title; + } + else { + standardTitle = personalizedAction.priceFormatted; + } + displayProperties.titles["standard"] = standardTitle; + displayProperties.priceFormatted = personalizedAction.priceFormatted; + // iAPs + const hasInAppPurchases = originalOfferDisplayProperties.hasInAppPurchases; + const hasExternalPurchases = originalOfferDisplayProperties.hasExternalPurchases; + displayProperties.hasInAppPurchases = hasInAppPurchases; + displayProperties.hasExternalPurchases = hasExternalPurchases; + if (hasExternalPurchases) { + const standardSubtitle = objectGraph.loc.string("OfferButton.ExternalPurchases.Subtitle"); + displayProperties.subtitles["standard"] = standardSubtitle; + } + else if (hasInAppPurchases) { + const standardSubtitle = objectGraph.loc.string("Offer.InlineInAppPurchases"); + displayProperties.subtitles["standard"] = standardSubtitle; + } + // Content Restrictions + displayProperties.contentRating = originalOfferDisplayProperties.contentRating; + // OS Eligibility + displayProperties.appCapabilities = personalizedAction.purchaseConfiguration.appCapabilities; + return displayProperties; + }); +} +// endregion +//# sourceMappingURL=offers.js.map \ No newline at end of file -- cgit v1.2.3