// // 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