summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/offers
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/offers')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/offers/external-purchases.js65
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/offers/offer-formatting.js231
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/offers/offers.js1735
3 files changed, 2031 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/offers/external-purchases.js b/node_modules/@jet-app/app-store/tmp/src/common/offers/external-purchases.js
new file mode 100644
index 0000000..92e3d9d
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/offers/external-purchases.js
@@ -0,0 +1,65 @@
+import * as models from "../../api/models";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import * as contentAttributes from "../content/attributes";
+import * as metricsHelpersClicks from "../metrics/helpers/clicks";
+import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils";
+import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent";
+import { getPlatform } from "../preview-platform";
+import { getLocale } from "../locale";
+/**
+ * Determines whether the product has external purchases.
+ * @param objectGraph Current object graph
+ * @param data The product data
+ * @returns True if the product has external purchases
+ */
+export function hasExternalPurchasesForData(objectGraph, data) {
+ const usesExternalPurchase = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "usesExternalPurchase");
+ const usesExternalLinkPurchase = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "usesExternalLinkPurchase");
+ return usesExternalPurchase || usesExternalLinkPurchase;
+}
+/**
+ * Determines whether external purchases should be indicated for the given client / bag / placement combination.
+ * @param objectGraph Current object graph
+ * @param placement Indicates where the external purchases indicator will be placed
+ * @returns True if the indicator should be displayed in the given placement.
+ */
+export function externalPurchasesPlacementIsEnabled(objectGraph, placement) {
+ return (objectGraph.bag.enableExternalPurchases &&
+ objectGraph.bag.enabledExternalPurchasesPlacements.includes(placement));
+}
+/**
+ * Creates a flow action for the external purchases story.
+ * @param objectGraph Current object graph
+ * @param title The title to use for the action
+ * @param metricsPageInformation Current metrics page information
+ * @param metricsLocationTracker Current metrics location tracker
+ * @returns A flow action for the story, or null
+ */
+export function externalPurchasesLearnMoreAction(objectGraph, title, metricsPageInformation, metricsLocationTracker) {
+ const editorialItemId = objectGraph.bag.externalPurchasesLearnMoreEditorialItemId;
+ if (serverData.isNullOrEmpty(editorialItemId) || !objectGraph.bag.enableExternalPurchases) {
+ return null;
+ }
+ const flowAction = new models.FlowAction("article");
+ flowAction.title = title;
+ flowAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`;
+ if (editorialItemId && objectGraph.client.isWeb) {
+ const destination = makeRoutableArticlePageIntent({
+ ...getLocale(objectGraph),
+ ...getPlatform(objectGraph),
+ id: editorialItemId,
+ });
+ const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination);
+ flowAction.pageUrl = pageUrlString;
+ flowAction.destination = destination;
+ }
+ metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, {
+ id: "LearnMore",
+ targetType: "link",
+ actionType: "navigate",
+ pageInformation: metricsPageInformation,
+ locationTracker: metricsLocationTracker,
+ });
+ return flowAction;
+}
+//# sourceMappingURL=external-purchases.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/offers/offer-formatting.js b/node_modules/@jet-app/app-store/tmp/src/common/offers/offer-formatting.js
new file mode 100644
index 0000000..c085065
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/offers/offer-formatting.js
@@ -0,0 +1,231 @@
+//
+// offer-formatting.ts
+// AppStoreKit
+//
+// Created by Dersu Abolfathi on 8/8/19.
+// Copyright (c) 2019 Apple Inc. All rights reserved.
+//
+import { isNothing } from "@jet/environment";
+import * as serverData from "../../foundation/json-parsing/server-data";
+export class SubscriptionRecurrence {
+ constructor(periodDuration, periodCount, type) {
+ this.periodDuration = periodDuration;
+ this.periodCount = periodCount;
+ this.type = type;
+ }
+ isEqualTo(otherRecurrence) {
+ return (otherRecurrence.periodDuration === this.periodDuration &&
+ otherRecurrence.periodCount === this.periodCount &&
+ otherRecurrence.type === this.type);
+ }
+}
+const DAYS_PER_WEEK = 7;
+/**
+ * Determines the recurrence type for the server-vended subscription recurrence string.
+ * @param serverRecurrence The string denoting the recurring subscription recurrence (i.e. 'P1M', 'P1Y', etc.).
+ * @param numberOfPeriods - The number of periods for the subscription.
+ * @returns {SubscriptionRecurrence} The recurrence value.
+ */
+export function subscriptionRecurrenceForServerRecurrence(objectGraph, serverRecurrence, numberOfPeriods) {
+ if (isNothing(serverRecurrence)) {
+ return null;
+ }
+ const match = serverRecurrence.match(/P(\d+)([A-Z]+)/);
+ if (!match || match.length !== 3) {
+ return null;
+ }
+ let periodDuration = serverData.asNumber(match[1]);
+ let type = match[2];
+ if (!periodDuration || !type) {
+ return null;
+ }
+ // Unfortunately, the server will give us weekly recurrences in days. So, we need to transform a days-based
+ // recurrence that lands on week boundaries to a week-based recurrence.
+ if (type === "D" && periodDuration > 0 && periodDuration % DAYS_PER_WEEK === 0) {
+ type = "W";
+ periodDuration = periodDuration / DAYS_PER_WEEK;
+ }
+ return new SubscriptionRecurrence(periodDuration, numberOfPeriods !== null && numberOfPeriods !== void 0 ? numberOfPeriods : 1, type);
+}
+// endregion
+// region IAP Subscription Trials
+/**
+ * The pre-install description for a subscription trial offer.
+ * @param inAppOfferData The overall data for the subscription, including the discount.
+ * @returns {string} The fully-localized formatted string for the subscription trial description.
+ */
+export function installPagePreInstallTrialDescription(objectGraph, inAppOfferData) {
+ const discountData = serverData.asArrayOrEmpty(inAppOfferData, "discounts")[0];
+ if (!discountData) {
+ return null;
+ }
+ const trialRecurrencePeriod = serverData.asString(discountData, "recurringSubscriptionPeriod");
+ const trialNumberOfPeriods = serverData.asNumber(discountData, "numOfPeriods");
+ const postTrialRecurrencePeriod = serverData.asString(inAppOfferData, "recurringSubscriptionPeriod");
+ const postTrialNumberOfPeriods = serverData.asNumber(inAppOfferData, "numOfPeriods");
+ if (!trialRecurrencePeriod || !postTrialRecurrencePeriod) {
+ return null;
+ }
+ const trialType = serverData.asString(discountData, "modeType");
+ const trialRecurrence = subscriptionRecurrenceForServerRecurrence(objectGraph, trialRecurrencePeriod, trialNumberOfPeriods);
+ const postTrialRecurrence = subscriptionRecurrenceForServerRecurrence(objectGraph, postTrialRecurrencePeriod, postTrialNumberOfPeriods);
+ // Replace any spaces within the formatted prices with non-breaking spaces to avoid prices breaking across multiple lines
+ const trialPriceFormatted = serverData.asString(discountData, "priceFormatted").replace(/ /g, "\u00a0");
+ const postTrialPriceFormatted = serverData.asString(inAppOfferData, "priceFormatted").replace(/ /g, "\u00a0");
+ let postTrialPriceDuration = priceDurationString(objectGraph, postTrialRecurrence.type, postTrialRecurrence.periodDuration, postTrialPriceFormatted);
+ // Update any slash (/) characters to be non-breaking by wrapping with u2060 word-joiner unicode characters.
+ postTrialPriceDuration = postTrialPriceDuration.replace(/\//g, "\u2060/\u2060");
+ switch (trialType) {
+ case "FreeTrial":
+ // template: "Free for @@durationCount@@, then @@postTrialPriceDuration@@ after trial."
+ // result: "Free for 6 months, then $4.99/month after trial."
+ const freeTrialDurationCount = durationCountString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration * trialRecurrence.periodCount);
+ if (freeTrialDurationCount && postTrialPriceDuration) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.FreeTrialTemplate")
+ .replace("@@durationCount@@", tokenReplacer(freeTrialDurationCount))
+ .replace("@@postTrialPriceDuration@@", tokenReplacer(postTrialPriceDuration));
+ }
+ break;
+ case "PayUpFront":
+ // template: "@@durationCount@@ for @@trialPrice@@, then @@postTrialPriceDuration@@ after trial."
+ // result: "3 months for $9.99, then $19.99/year after trial."
+ const payUpFrontDurationCount = durationCountString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration * trialRecurrence.periodCount);
+ if (payUpFrontDurationCount && postTrialPriceDuration) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.PaidUpFrontTemplate")
+ .replace("@@durationCount@@", tokenReplacer(payUpFrontDurationCount))
+ .replace("@@trialPrice@@", tokenReplacer(trialPriceFormatted))
+ .replace("@@postTrialPriceDuration@@", tokenReplacer(postTrialPriceDuration));
+ }
+ break;
+ case "PayAsYouGo":
+ // template: "@@trialPriceDuration@@ for @@durationCount@@, then @@postTrialPriceDuration@@ after trial."
+ // result: "$1.99/week for 3 weeks, then $9.99/month after trial."
+ const trialPriceDuration = priceDurationString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration, trialPriceFormatted);
+ const durationCount = durationCountString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration * trialRecurrence.periodCount);
+ if (durationCount && postTrialPriceDuration) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.PaidTrialTemplate")
+ .replace("@@trialPriceDuration@@", tokenReplacer(trialPriceDuration))
+ .replace("@@durationCount@@", tokenReplacer(durationCount))
+ .replace("@@postTrialPriceDuration@@", tokenReplacer(postTrialPriceDuration));
+ }
+ break;
+ default:
+ return null;
+ }
+ return null;
+}
+// <rdar://problem/55389195> Arcade grouping upsell text showing pricing token
+// We use a function replacer instead of a string replacer, since `$` is interpreted as a special substitution token.
+// Function replacers have less special semantics.
+function tokenReplacer(replacementString) {
+ return replacementString;
+}
+/**
+ * Returns a string representing a formatted price per duration of time.
+ * e.g. "$3.99/day" if the recurrenceType is `days` and durationCount is `1`.
+ * e.g. "$3.99 every 3 months" the recurrenceType is `months` and durationCount is `3`.
+ * @param recurrenceType: The recurrence type - e.g. days, weeks, months, years.
+ * @param durationCount The count of the duration that will be represented in the string.
+ * @param formattedPrice The formatted price that will be substituted in the returned string.
+ * @returns {string} The localized string for the price per duration.
+ */
+export function priceDurationString(objectGraph, recurrenceType, durationCount, formattedPrice) {
+ // template: "@@price@@ every @@count@@ days"
+ // result: "$1.99 every 3 days"
+ let template;
+ switch (recurrenceType) {
+ // NOTE: Below we have added a workaround to enforce use of the .one plural string variation when count is exactly 1, as jet localizer doesn't do this by default for all languages.
+ // In rdar://113586253 we will adopt newer API from jet that allows us to do this globally, and then this workaround can be removed.
+ case "D":
+ if (durationCount === 1) {
+ template = objectGraph.loc
+ .string("InAppOfferPage.Description.PriceDuration.Days.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ else {
+ template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Days", durationCount);
+ }
+ break;
+ case "W":
+ if (durationCount === 1) {
+ template = objectGraph.loc
+ .string("InAppOfferPage.Description.PriceDuration.Weeks.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ else {
+ template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Weeks", durationCount);
+ }
+ break;
+ case "M":
+ if (durationCount === 1) {
+ template = objectGraph.loc
+ .string("InAppOfferPage.Description.PriceDuration.Months.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ else {
+ template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Months", durationCount);
+ }
+ break;
+ case "Y":
+ if (durationCount === 1) {
+ template = objectGraph.loc
+ .string("InAppOfferPage.Description.PriceDuration.Years.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ else {
+ template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Years", durationCount);
+ }
+ break;
+ default:
+ break;
+ }
+ return template.replace("@@price@@", tokenReplacer(formattedPrice));
+}
+/**
+ * Returns a string representing the duration count of the specified recurrence type.
+ * e.g. "3 days" if recurrenceType is `days` and durationCount is `3`.
+ * @param recurrenceType: The recurrence type - e.g. days, weeks, months, years.
+ * @param durationCount The count of the duration that will be represented in the string.
+ * @returns {string} The localized duration count string.
+ */
+export function durationCountString(objectGraph, recurrenceType, durationCount) {
+ switch (recurrenceType) {
+ // NOTE: Below we have added a workaround to enforce use of the .one plural string variation when count is exactly 1, as jet localizer doesn't do this by default for all languages.
+ // In rdar://113586253 we will adopt newer API from jet that allows us to do this globally, and then this workaround can be removed.
+ case "D":
+ if (durationCount === 1) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.DurationCount.Days.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Days", durationCount);
+ case "W":
+ if (durationCount === 1) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.DurationCount.Weeks.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Weeks", durationCount);
+ case "M":
+ if (durationCount === 1) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.DurationCount.Months.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Months", durationCount);
+ case "Y":
+ if (durationCount === 1) {
+ return objectGraph.loc
+ .string("InAppOfferPage.Description.DurationCount.Years.one")
+ .replace("@@count@@", objectGraph.loc.formattedCount(durationCount));
+ }
+ return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Years", durationCount);
+ default:
+ break;
+ }
+ return null;
+}
+//# sourceMappingURL=offer-formatting.js.map \ No newline at end of file
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:
+ // <rdar://problem/33567931> Bundles: Suppress CMB dialog
+ //
+ // TODO: We need to suppress this dialog until CmB is completed:
+ // <rdar://problem/32007058> Bundles: Implement complete my bundle
+ //
+ // TODO: Unsuppress when commerce has new intrim alerts ready:
+ // <rdar://problem/33732045> 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